@bloomneo/appkit 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +902 -0
- package/bin/appkit.js +71 -0
- package/bin/commands/generate.js +1050 -0
- package/bin/templates/backend/README.md.template +39 -0
- package/bin/templates/backend/api.http.template +0 -0
- package/bin/templates/backend/docs/APPKIT_CLI.md +507 -0
- package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +61 -0
- package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +2539 -0
- package/bin/templates/backend/package.json.template +34 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.http.template +29 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.route.ts.template +36 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.service.ts.template +88 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.types.ts.template +18 -0
- package/bin/templates/backend/src/api/lib/api-router.ts.template +84 -0
- package/bin/templates/backend/src/api/server.ts.template +188 -0
- package/bin/templates/backend/tsconfig.api.json.template +24 -0
- package/bin/templates/backend/tsconfig.json.template +40 -0
- package/bin/templates/feature/feature.http.template +63 -0
- package/bin/templates/feature/feature.route.ts.template +36 -0
- package/bin/templates/feature/feature.service.ts.template +81 -0
- package/bin/templates/feature/feature.types.ts.template +23 -0
- package/bin/templates/feature-db/feature.http.template +63 -0
- package/bin/templates/feature-db/feature.model.ts.template +74 -0
- package/bin/templates/feature-db/feature.route.ts.template +58 -0
- package/bin/templates/feature-db/feature.service.ts.template +231 -0
- package/bin/templates/feature-db/feature.types.ts.template +25 -0
- package/bin/templates/feature-db/schema-addition.prisma.template +9 -0
- package/bin/templates/feature-db/seeding/README.md.template +57 -0
- package/bin/templates/feature-db/seeding/feature.seed.js.template +67 -0
- package/bin/templates/feature-user/schema-addition.prisma.template +19 -0
- package/bin/templates/feature-user/user.http.template +157 -0
- package/bin/templates/feature-user/user.model.ts.template +244 -0
- package/bin/templates/feature-user/user.route.ts.template +379 -0
- package/bin/templates/feature-user/user.seed.js.template +182 -0
- package/bin/templates/feature-user/user.service.ts.template +426 -0
- package/bin/templates/feature-user/user.types.ts.template +127 -0
- package/dist/auth/auth.d.ts +182 -0
- package/dist/auth/auth.d.ts.map +1 -0
- package/dist/auth/auth.js +477 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/defaults.d.ts +104 -0
- package/dist/auth/defaults.d.ts.map +1 -0
- package/dist/auth/defaults.js +374 -0
- package/dist/auth/defaults.js.map +1 -0
- package/dist/auth/index.d.ts +70 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +94 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/cache/cache.d.ts +118 -0
- package/dist/cache/cache.d.ts.map +1 -0
- package/dist/cache/cache.js +249 -0
- package/dist/cache/cache.js.map +1 -0
- package/dist/cache/defaults.d.ts +63 -0
- package/dist/cache/defaults.d.ts.map +1 -0
- package/dist/cache/defaults.js +193 -0
- package/dist/cache/defaults.js.map +1 -0
- package/dist/cache/index.d.ts +101 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +203 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/strategies/memory.d.ts +138 -0
- package/dist/cache/strategies/memory.d.ts.map +1 -0
- package/dist/cache/strategies/memory.js +348 -0
- package/dist/cache/strategies/memory.js.map +1 -0
- package/dist/cache/strategies/redis.d.ts +105 -0
- package/dist/cache/strategies/redis.d.ts.map +1 -0
- package/dist/cache/strategies/redis.js +318 -0
- package/dist/cache/strategies/redis.js.map +1 -0
- package/dist/config/config.d.ts +62 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +107 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/defaults.d.ts +44 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +217 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +105 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +163 -0
- package/dist/config/index.js.map +1 -0
- package/dist/database/adapters/mongoose.d.ts +106 -0
- package/dist/database/adapters/mongoose.d.ts.map +1 -0
- package/dist/database/adapters/mongoose.js +480 -0
- package/dist/database/adapters/mongoose.js.map +1 -0
- package/dist/database/adapters/prisma.d.ts +106 -0
- package/dist/database/adapters/prisma.d.ts.map +1 -0
- package/dist/database/adapters/prisma.js +494 -0
- package/dist/database/adapters/prisma.js.map +1 -0
- package/dist/database/defaults.d.ts +87 -0
- package/dist/database/defaults.d.ts.map +1 -0
- package/dist/database/defaults.js +271 -0
- package/dist/database/defaults.js.map +1 -0
- package/dist/database/index.d.ts +137 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +490 -0
- package/dist/database/index.js.map +1 -0
- package/dist/email/defaults.d.ts +100 -0
- package/dist/email/defaults.d.ts.map +1 -0
- package/dist/email/defaults.js +400 -0
- package/dist/email/defaults.js.map +1 -0
- package/dist/email/email.d.ts +139 -0
- package/dist/email/email.d.ts.map +1 -0
- package/dist/email/email.js +316 -0
- package/dist/email/email.js.map +1 -0
- package/dist/email/index.d.ts +176 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +251 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/strategies/console.d.ts +90 -0
- package/dist/email/strategies/console.d.ts.map +1 -0
- package/dist/email/strategies/console.js +268 -0
- package/dist/email/strategies/console.js.map +1 -0
- package/dist/email/strategies/resend.d.ts +84 -0
- package/dist/email/strategies/resend.d.ts.map +1 -0
- package/dist/email/strategies/resend.js +266 -0
- package/dist/email/strategies/resend.js.map +1 -0
- package/dist/email/strategies/smtp.d.ts +77 -0
- package/dist/email/strategies/smtp.d.ts.map +1 -0
- package/dist/email/strategies/smtp.js +286 -0
- package/dist/email/strategies/smtp.js.map +1 -0
- package/dist/error/defaults.d.ts +40 -0
- package/dist/error/defaults.d.ts.map +1 -0
- package/dist/error/defaults.js +75 -0
- package/dist/error/defaults.js.map +1 -0
- package/dist/error/error.d.ts +140 -0
- package/dist/error/error.d.ts.map +1 -0
- package/dist/error/error.js +200 -0
- package/dist/error/error.js.map +1 -0
- package/dist/error/index.d.ts +145 -0
- package/dist/error/index.d.ts.map +1 -0
- package/dist/error/index.js +145 -0
- package/dist/error/index.js.map +1 -0
- package/dist/event/defaults.d.ts +111 -0
- package/dist/event/defaults.d.ts.map +1 -0
- package/dist/event/defaults.js +378 -0
- package/dist/event/defaults.js.map +1 -0
- package/dist/event/event.d.ts +171 -0
- package/dist/event/event.d.ts.map +1 -0
- package/dist/event/event.js +391 -0
- package/dist/event/event.js.map +1 -0
- package/dist/event/index.d.ts +173 -0
- package/dist/event/index.d.ts.map +1 -0
- package/dist/event/index.js +302 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/strategies/memory.d.ts +122 -0
- package/dist/event/strategies/memory.d.ts.map +1 -0
- package/dist/event/strategies/memory.js +331 -0
- package/dist/event/strategies/memory.js.map +1 -0
- package/dist/event/strategies/redis.d.ts +115 -0
- package/dist/event/strategies/redis.d.ts.map +1 -0
- package/dist/event/strategies/redis.js +434 -0
- package/dist/event/strategies/redis.js.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/logger/defaults.d.ts +67 -0
- package/dist/logger/defaults.d.ts.map +1 -0
- package/dist/logger/defaults.js +213 -0
- package/dist/logger/defaults.js.map +1 -0
- package/dist/logger/index.d.ts +84 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +101 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/logger.d.ts +165 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +843 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/logger/transports/console.d.ts +102 -0
- package/dist/logger/transports/console.d.ts.map +1 -0
- package/dist/logger/transports/console.js +276 -0
- package/dist/logger/transports/console.js.map +1 -0
- package/dist/logger/transports/database.d.ts +153 -0
- package/dist/logger/transports/database.d.ts.map +1 -0
- package/dist/logger/transports/database.js +539 -0
- package/dist/logger/transports/database.js.map +1 -0
- package/dist/logger/transports/file.d.ts +146 -0
- package/dist/logger/transports/file.d.ts.map +1 -0
- package/dist/logger/transports/file.js +464 -0
- package/dist/logger/transports/file.js.map +1 -0
- package/dist/logger/transports/http.d.ts +128 -0
- package/dist/logger/transports/http.d.ts.map +1 -0
- package/dist/logger/transports/http.js +401 -0
- package/dist/logger/transports/http.js.map +1 -0
- package/dist/logger/transports/webhook.d.ts +152 -0
- package/dist/logger/transports/webhook.d.ts.map +1 -0
- package/dist/logger/transports/webhook.js +485 -0
- package/dist/logger/transports/webhook.js.map +1 -0
- package/dist/queue/defaults.d.ts +66 -0
- package/dist/queue/defaults.d.ts.map +1 -0
- package/dist/queue/defaults.js +205 -0
- package/dist/queue/defaults.js.map +1 -0
- package/dist/queue/index.d.ts +124 -0
- package/dist/queue/index.d.ts.map +1 -0
- package/dist/queue/index.js +116 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/queue.d.ts +156 -0
- package/dist/queue/queue.d.ts.map +1 -0
- package/dist/queue/queue.js +387 -0
- package/dist/queue/queue.js.map +1 -0
- package/dist/queue/transports/database.d.ts +165 -0
- package/dist/queue/transports/database.d.ts.map +1 -0
- package/dist/queue/transports/database.js +595 -0
- package/dist/queue/transports/database.js.map +1 -0
- package/dist/queue/transports/memory.d.ts +143 -0
- package/dist/queue/transports/memory.d.ts.map +1 -0
- package/dist/queue/transports/memory.js +415 -0
- package/dist/queue/transports/memory.js.map +1 -0
- package/dist/queue/transports/redis.d.ts +203 -0
- package/dist/queue/transports/redis.d.ts.map +1 -0
- package/dist/queue/transports/redis.js +744 -0
- package/dist/queue/transports/redis.js.map +1 -0
- package/dist/security/defaults.d.ts +64 -0
- package/dist/security/defaults.d.ts.map +1 -0
- package/dist/security/defaults.js +159 -0
- package/dist/security/defaults.js.map +1 -0
- package/dist/security/index.d.ts +110 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +160 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/security.d.ts +138 -0
- package/dist/security/security.d.ts.map +1 -0
- package/dist/security/security.js +419 -0
- package/dist/security/security.js.map +1 -0
- package/dist/storage/defaults.d.ts +79 -0
- package/dist/storage/defaults.d.ts.map +1 -0
- package/dist/storage/defaults.js +358 -0
- package/dist/storage/defaults.js.map +1 -0
- package/dist/storage/index.d.ts +153 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +242 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/storage.d.ts +151 -0
- package/dist/storage/storage.d.ts.map +1 -0
- package/dist/storage/storage.js +439 -0
- package/dist/storage/storage.js.map +1 -0
- package/dist/storage/strategies/local.d.ts +117 -0
- package/dist/storage/strategies/local.d.ts.map +1 -0
- package/dist/storage/strategies/local.js +368 -0
- package/dist/storage/strategies/local.js.map +1 -0
- package/dist/storage/strategies/r2.d.ts +130 -0
- package/dist/storage/strategies/r2.d.ts.map +1 -0
- package/dist/storage/strategies/r2.js +470 -0
- package/dist/storage/strategies/r2.js.map +1 -0
- package/dist/storage/strategies/s3.d.ts +121 -0
- package/dist/storage/strategies/s3.d.ts.map +1 -0
- package/dist/storage/strategies/s3.js +461 -0
- package/dist/storage/strategies/s3.js.map +1 -0
- package/dist/util/defaults.d.ts +77 -0
- package/dist/util/defaults.d.ts.map +1 -0
- package/dist/util/defaults.js +193 -0
- package/dist/util/defaults.js.map +1 -0
- package/dist/util/index.d.ts +97 -0
- package/dist/util/index.d.ts.map +1 -0
- package/dist/util/index.js +165 -0
- package/dist/util/index.js.map +1 -0
- package/dist/util/util.d.ts +145 -0
- package/dist/util/util.d.ts.map +1 -0
- package/dist/util/util.js +481 -0
- package/dist/util/util.js.map +1 -0
- package/package.json +234 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core authentication class with role-level-permission system
|
|
3
|
+
* @module @bloomneo/appkit/auth
|
|
4
|
+
* @file src/auth/authentication.ts
|
|
5
|
+
*
|
|
6
|
+
* @llm-rule WHEN: Building apps that need JWT operations, password hashing, and role-based middleware
|
|
7
|
+
* @llm-rule AVOID: Using directly - always get instance via auth.get()
|
|
8
|
+
* @llm-rule NOTE: Use requireUserRoles() for hierarchy-based access, requireUserPermissions() for action-specific access
|
|
9
|
+
* @llm-rule NOTE: Uses role.level format (user.basic, admin.tenant) with automatic inheritance
|
|
10
|
+
*/
|
|
11
|
+
import { type AuthConfig } from './defaults.js';
|
|
12
|
+
export interface JwtPayload {
|
|
13
|
+
userId?: string | number;
|
|
14
|
+
keyId?: string;
|
|
15
|
+
type: 'login' | 'api_key';
|
|
16
|
+
role: string;
|
|
17
|
+
level: string;
|
|
18
|
+
permissions?: string[];
|
|
19
|
+
[key: string]: any;
|
|
20
|
+
iat?: number;
|
|
21
|
+
exp?: number;
|
|
22
|
+
iss?: string;
|
|
23
|
+
aud?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface LoginTokenPayload {
|
|
26
|
+
userId: string | number;
|
|
27
|
+
type: 'login';
|
|
28
|
+
role: string;
|
|
29
|
+
level: string;
|
|
30
|
+
permissions?: string[];
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
}
|
|
33
|
+
export interface ApiTokenPayload {
|
|
34
|
+
keyId: string;
|
|
35
|
+
type: 'api_key';
|
|
36
|
+
role: string;
|
|
37
|
+
level: string;
|
|
38
|
+
permissions?: string[];
|
|
39
|
+
[key: string]: any;
|
|
40
|
+
}
|
|
41
|
+
export interface ExpressRequest {
|
|
42
|
+
headers: {
|
|
43
|
+
[key: string]: string | string[] | undefined;
|
|
44
|
+
};
|
|
45
|
+
cookies?: {
|
|
46
|
+
[key: string]: string;
|
|
47
|
+
};
|
|
48
|
+
query?: {
|
|
49
|
+
[key: string]: any;
|
|
50
|
+
};
|
|
51
|
+
user?: JwtPayload;
|
|
52
|
+
token?: JwtPayload;
|
|
53
|
+
[key: string]: any;
|
|
54
|
+
}
|
|
55
|
+
export interface ExpressResponse {
|
|
56
|
+
status: (code: number) => {
|
|
57
|
+
json: (data: any) => void;
|
|
58
|
+
};
|
|
59
|
+
json: (data: any) => void;
|
|
60
|
+
}
|
|
61
|
+
export interface MiddlewareOptions {
|
|
62
|
+
getToken?: (request: ExpressRequest) => string | null;
|
|
63
|
+
}
|
|
64
|
+
export type ExpressMiddleware = (req: ExpressRequest, res: ExpressResponse, next: () => void) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Authentication class with JWT, password, and role-level-permission system
|
|
67
|
+
*/
|
|
68
|
+
export declare class AuthenticationClass {
|
|
69
|
+
config: AuthConfig;
|
|
70
|
+
constructor(config: AuthConfig);
|
|
71
|
+
/**
|
|
72
|
+
* Generates a login JWT token for user authentication
|
|
73
|
+
* @llm-rule WHEN: User successfully logs in to your app (mobile/web)
|
|
74
|
+
* @llm-rule AVOID: Using for API access - use generateApiToken instead
|
|
75
|
+
* @llm-rule NOTE: Creates JWT with userId and type: 'login'
|
|
76
|
+
*/
|
|
77
|
+
generateLoginToken(payload: Omit<LoginTokenPayload, 'type' | 'iat' | 'exp' | 'iss' | 'aud'>, expiresIn?: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* Generates an API JWT token for external access
|
|
80
|
+
* @llm-rule WHEN: Creating API keys for third-party integrations
|
|
81
|
+
* @llm-rule AVOID: Using for user authentication - use generateLoginToken instead
|
|
82
|
+
* @llm-rule NOTE: Creates JWT with keyId and type: 'api_key'
|
|
83
|
+
*/
|
|
84
|
+
generateApiToken(payload: Omit<ApiTokenPayload, 'type' | 'iat' | 'exp' | 'iss' | 'aud'>, expiresIn?: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* Internal method to create and sign JWT tokens
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
private signToken;
|
|
90
|
+
/**
|
|
91
|
+
* Verifies and decodes a JWT token (both login and API tokens)
|
|
92
|
+
* @llm-rule WHEN: Validating incoming tokens from requests
|
|
93
|
+
* @llm-rule AVOID: Using jwt.verify directly - this handles errors and validates structure
|
|
94
|
+
* @llm-rule NOTE: Handles both login tokens (userId) and API tokens (keyId)
|
|
95
|
+
*/
|
|
96
|
+
verifyToken(token: string): JwtPayload;
|
|
97
|
+
/**
|
|
98
|
+
* Hashes a password using bcrypt
|
|
99
|
+
* @llm-rule WHEN: Storing user passwords - always hash before saving to database
|
|
100
|
+
* @llm-rule AVOID: Storing plain text passwords - major security vulnerability
|
|
101
|
+
* @llm-rule NOTE: Takes ~100ms with default 10 rounds - don't call in tight loops
|
|
102
|
+
*/
|
|
103
|
+
hashPassword(password: string, rounds?: number): Promise<string>;
|
|
104
|
+
/**
|
|
105
|
+
* Compares a password with its hash
|
|
106
|
+
* @llm-rule WHEN: Validating user login credentials
|
|
107
|
+
* @llm-rule AVOID: Manual string comparison - timing attacks possible
|
|
108
|
+
* @llm-rule NOTE: Always returns boolean, never throws on comparison failure
|
|
109
|
+
*/
|
|
110
|
+
comparePassword(password: string, hash: string): Promise<boolean>;
|
|
111
|
+
/**
|
|
112
|
+
* Safely extracts user from request - never crashes
|
|
113
|
+
* @llm-rule WHEN: Need to access user data from authenticated requests
|
|
114
|
+
* @llm-rule AVOID: Accessing req.user directly - may be undefined and cause crashes
|
|
115
|
+
* @llm-rule NOTE: Always returns null for unauthenticated requests - safe to use
|
|
116
|
+
* @llm-rule NOTE: Works with both login authentication (req.user) and API tokens (req.token)
|
|
117
|
+
*/
|
|
118
|
+
user(request: ExpressRequest): JwtPayload | null;
|
|
119
|
+
/**
|
|
120
|
+
* Checks if user has specified role with automatic inheritance
|
|
121
|
+
* @llm-rule WHEN: Checking if user can access role-protected resources
|
|
122
|
+
* @llm-rule AVOID: Manual role comparisons - this handles inheritance automatically
|
|
123
|
+
* @llm-rule NOTE: Higher levels inherit lower (admin.org has admin.tenant access)
|
|
124
|
+
* @llm-rule NOTE: INHERITANCE EXAMPLES:
|
|
125
|
+
* @llm-rule NOTE: auth.hasRole('admin.org', 'admin.tenant') → TRUE (org > tenant)
|
|
126
|
+
* @llm-rule NOTE: auth.hasRole('admin.system', 'user.basic') → TRUE (system > basic)
|
|
127
|
+
* @llm-rule NOTE: auth.hasRole('user.basic', 'admin.tenant') → FALSE (basic < tenant)
|
|
128
|
+
* @llm-rule NOTE: Role hierarchy: admin.system > admin.org > admin.tenant > user.max > user.pro > user.basic
|
|
129
|
+
*/
|
|
130
|
+
hasRole(userRoleLevel: string, requiredRoleLevel: string): boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Checks if user has specific permission with automatic action inheritance
|
|
133
|
+
* @llm-rule WHEN: Checking fine-grained permissions for specific actions
|
|
134
|
+
* @llm-rule AVOID: Hardcoding permission checks - this handles action inheritance
|
|
135
|
+
* @llm-rule NOTE: 'manage:scope' includes ALL other actions for that scope
|
|
136
|
+
* @llm-rule NOTE: PERMISSION INHERITANCE EXAMPLES:
|
|
137
|
+
* @llm-rule NOTE: If user has 'manage:tenant' → can('edit:tenant') returns TRUE
|
|
138
|
+
* @llm-rule NOTE: If user has 'manage:tenant' → can('view:tenant') returns TRUE
|
|
139
|
+
* @llm-rule NOTE: If user has 'edit:tenant' → can('manage:tenant') returns FALSE
|
|
140
|
+
* @llm-rule NOTE: Actions hierarchy: manage > delete > edit > create > view
|
|
141
|
+
*/
|
|
142
|
+
can(user: JwtPayload, permission: string): boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Creates Express authentication middleware for login tokens
|
|
145
|
+
* @llm-rule WHEN: Protecting routes that need authenticated users
|
|
146
|
+
* @llm-rule AVOID: Using for API routes - use requireApiToken instead
|
|
147
|
+
* @llm-rule NOTE: Validates login tokens (type: 'login') and sets req.user
|
|
148
|
+
*/
|
|
149
|
+
requireLoginToken(options?: MiddlewareOptions): ExpressMiddleware;
|
|
150
|
+
/**
|
|
151
|
+
* Creates Express role-based authorization middleware for authenticated users
|
|
152
|
+
* @llm-rule WHEN: Protecting routes that require specific user roles
|
|
153
|
+
* @llm-rule AVOID: Using without requireLoginToken - this assumes user is already authenticated
|
|
154
|
+
* @llm-rule AVOID: Using with API tokens - API tokens don't have user roles
|
|
155
|
+
* @llm-rule NOTE: User needs ANY role from the array (OR logic)
|
|
156
|
+
* @llm-rule NOTE: Role inheritance applies - admin.org can access admin.tenant routes
|
|
157
|
+
*/
|
|
158
|
+
requireUserRoles(requiredRoles: string[]): ExpressMiddleware;
|
|
159
|
+
/**
|
|
160
|
+
* Creates Express permission-based authorization middleware for authenticated users
|
|
161
|
+
* @llm-rule WHEN: Protecting routes that require specific user permissions
|
|
162
|
+
* @llm-rule AVOID: Using without requireLoginToken - this assumes user is already authenticated
|
|
163
|
+
* @llm-rule AVOID: Using with API tokens - API tokens don't have user permissions
|
|
164
|
+
* @llm-rule NOTE: User needs ALL permissions from the array (AND logic)
|
|
165
|
+
* @llm-rule NOTE: Permission inheritance applies - manage:tenant can access edit:tenant routes
|
|
166
|
+
*/
|
|
167
|
+
requireUserPermissions(requiredPermissions: string[]): ExpressMiddleware;
|
|
168
|
+
/**
|
|
169
|
+
* Creates Express API token authentication middleware for external access
|
|
170
|
+
* @llm-rule WHEN: Protecting API routes for third-party integrations
|
|
171
|
+
* @llm-rule AVOID: Using for user routes - use requireLoginToken instead
|
|
172
|
+
* @llm-rule NOTE: Validates API tokens (type: 'api_key') and sets req.token
|
|
173
|
+
*/
|
|
174
|
+
requireApiToken(options?: MiddlewareOptions): ExpressMiddleware;
|
|
175
|
+
/**
|
|
176
|
+
* Gets default token extractor that checks headers, cookies, and query params
|
|
177
|
+
* @llm-rule WHEN: Need custom token extraction logic
|
|
178
|
+
* @llm-rule AVOID: Modifying directly - pass custom getToken to middleware options
|
|
179
|
+
*/
|
|
180
|
+
private getDefaultTokenExtractor;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/auth/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core authentication class with role-level-permission system
|
|
3
|
+
* @module @bloomneo/appkit/auth
|
|
4
|
+
* @file src/auth/authentication.ts
|
|
5
|
+
*
|
|
6
|
+
* @llm-rule WHEN: Building apps that need JWT operations, password hashing, and role-based middleware
|
|
7
|
+
* @llm-rule AVOID: Using directly - always get instance via auth.get()
|
|
8
|
+
* @llm-rule NOTE: Use requireUserRoles() for hierarchy-based access, requireUserPermissions() for action-specific access
|
|
9
|
+
* @llm-rule NOTE: Uses role.level format (user.basic, admin.tenant) with automatic inheritance
|
|
10
|
+
*/
|
|
11
|
+
import jwt from 'jsonwebtoken';
|
|
12
|
+
import bcrypt from 'bcrypt';
|
|
13
|
+
import { validateRounds, validateRoleLevel, validatePermission, } from './defaults.js';
|
|
14
|
+
/**
|
|
15
|
+
* Authentication class with JWT, password, and role-level-permission system
|
|
16
|
+
*/
|
|
17
|
+
export class AuthenticationClass {
|
|
18
|
+
config;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Generates a login JWT token for user authentication
|
|
24
|
+
* @llm-rule WHEN: User successfully logs in to your app (mobile/web)
|
|
25
|
+
* @llm-rule AVOID: Using for API access - use generateApiToken instead
|
|
26
|
+
* @llm-rule NOTE: Creates JWT with userId and type: 'login'
|
|
27
|
+
*/
|
|
28
|
+
generateLoginToken(payload, expiresIn) {
|
|
29
|
+
const loginPayload = {
|
|
30
|
+
...payload,
|
|
31
|
+
type: 'login',
|
|
32
|
+
};
|
|
33
|
+
return this.signToken(loginPayload, expiresIn || '7d');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Generates an API JWT token for external access
|
|
37
|
+
* @llm-rule WHEN: Creating API keys for third-party integrations
|
|
38
|
+
* @llm-rule AVOID: Using for user authentication - use generateLoginToken instead
|
|
39
|
+
* @llm-rule NOTE: Creates JWT with keyId and type: 'api_key'
|
|
40
|
+
*/
|
|
41
|
+
generateApiToken(payload, expiresIn) {
|
|
42
|
+
const apiPayload = {
|
|
43
|
+
...payload,
|
|
44
|
+
type: 'api_key',
|
|
45
|
+
};
|
|
46
|
+
return this.signToken(apiPayload, expiresIn || '1y');
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Internal method to create and sign JWT tokens
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
signToken(payload, expiresIn) {
|
|
53
|
+
if (!payload || typeof payload !== 'object') {
|
|
54
|
+
throw new Error('Payload must be an object');
|
|
55
|
+
}
|
|
56
|
+
// Validate based on token type
|
|
57
|
+
if (payload.type === 'login') {
|
|
58
|
+
if (!payload.userId) {
|
|
59
|
+
throw new Error('Login token must include userId');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (payload.type === 'api_key') {
|
|
63
|
+
if (!payload.keyId) {
|
|
64
|
+
throw new Error('API token must include keyId');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
throw new Error('Token type must be "login" or "api_key"');
|
|
69
|
+
}
|
|
70
|
+
if (!payload.role || !payload.level) {
|
|
71
|
+
throw new Error('Payload must include both role and level');
|
|
72
|
+
}
|
|
73
|
+
// Validate role.level exists
|
|
74
|
+
const roleLevel = `${payload.role}.${payload.level}`;
|
|
75
|
+
if (!validateRoleLevel(roleLevel, this.config.roles)) {
|
|
76
|
+
throw new Error(`Invalid role.level: "${roleLevel}"`);
|
|
77
|
+
}
|
|
78
|
+
const jwtSecret = this.config.jwt.secret;
|
|
79
|
+
if (!jwtSecret) {
|
|
80
|
+
throw new Error('JWT secret required. Set VOILA_AUTH_SECRET environment variable');
|
|
81
|
+
}
|
|
82
|
+
const tokenExpiration = expiresIn || this.config.jwt.expiresIn;
|
|
83
|
+
try {
|
|
84
|
+
return jwt.sign(payload, jwtSecret, {
|
|
85
|
+
expiresIn: tokenExpiration,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
throw new Error(`Failed to generate token: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Verifies and decodes a JWT token (both login and API tokens)
|
|
94
|
+
* @llm-rule WHEN: Validating incoming tokens from requests
|
|
95
|
+
* @llm-rule AVOID: Using jwt.verify directly - this handles errors and validates structure
|
|
96
|
+
* @llm-rule NOTE: Handles both login tokens (userId) and API tokens (keyId)
|
|
97
|
+
*/
|
|
98
|
+
verifyToken(token) {
|
|
99
|
+
if (!token || typeof token !== 'string') {
|
|
100
|
+
throw new Error('Token must be a string');
|
|
101
|
+
}
|
|
102
|
+
const jwtSecret = this.config.jwt.secret;
|
|
103
|
+
if (!jwtSecret) {
|
|
104
|
+
throw new Error('JWT secret required. Set VOILA_AUTH_SECRET environment variable');
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const decoded = jwt.verify(token, jwtSecret, {
|
|
108
|
+
algorithms: [this.config.jwt.algorithm],
|
|
109
|
+
});
|
|
110
|
+
// Validate decoded token has required structure
|
|
111
|
+
if (!decoded.role || !decoded.level || !decoded.type) {
|
|
112
|
+
throw new Error('Token missing required role, level, or type information');
|
|
113
|
+
}
|
|
114
|
+
// Validate type-specific requirements
|
|
115
|
+
if (decoded.type === 'login' && !decoded.userId) {
|
|
116
|
+
throw new Error('Login token missing userId');
|
|
117
|
+
}
|
|
118
|
+
if (decoded.type === 'api_key' && !decoded.keyId) {
|
|
119
|
+
throw new Error('API token missing keyId');
|
|
120
|
+
}
|
|
121
|
+
return decoded;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error.name === 'TokenExpiredError') {
|
|
125
|
+
throw new Error('Token has expired');
|
|
126
|
+
}
|
|
127
|
+
if (error.name === 'JsonWebTokenError') {
|
|
128
|
+
throw new Error('Invalid token');
|
|
129
|
+
}
|
|
130
|
+
throw new Error(`Token verification failed: ${error.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Hashes a password using bcrypt
|
|
135
|
+
* @llm-rule WHEN: Storing user passwords - always hash before saving to database
|
|
136
|
+
* @llm-rule AVOID: Storing plain text passwords - major security vulnerability
|
|
137
|
+
* @llm-rule NOTE: Takes ~100ms with default 10 rounds - don't call in tight loops
|
|
138
|
+
*/
|
|
139
|
+
async hashPassword(password, rounds) {
|
|
140
|
+
if (!password || typeof password !== 'string') {
|
|
141
|
+
throw new Error('Password must be a non-empty string');
|
|
142
|
+
}
|
|
143
|
+
const saltRounds = rounds || this.config.password.saltRounds;
|
|
144
|
+
validateRounds(saltRounds);
|
|
145
|
+
try {
|
|
146
|
+
return await bcrypt.hash(password, saltRounds);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
throw new Error(`Password hashing failed: ${error.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Compares a password with its hash
|
|
154
|
+
* @llm-rule WHEN: Validating user login credentials
|
|
155
|
+
* @llm-rule AVOID: Manual string comparison - timing attacks possible
|
|
156
|
+
* @llm-rule NOTE: Always returns boolean, never throws on comparison failure
|
|
157
|
+
*/
|
|
158
|
+
async comparePassword(password, hash) {
|
|
159
|
+
if (!password || typeof password !== 'string') {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
if (!hash || typeof hash !== 'string') {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
return await bcrypt.compare(password, hash);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
// bcrypt.compare can fail on malformed hashes
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Safely extracts user from request - never crashes
|
|
175
|
+
* @llm-rule WHEN: Need to access user data from authenticated requests
|
|
176
|
+
* @llm-rule AVOID: Accessing req.user directly - may be undefined and cause crashes
|
|
177
|
+
* @llm-rule NOTE: Always returns null for unauthenticated requests - safe to use
|
|
178
|
+
* @llm-rule NOTE: Works with both login authentication (req.user) and API tokens (req.token)
|
|
179
|
+
*/
|
|
180
|
+
user(request) {
|
|
181
|
+
if (!request || typeof request !== 'object') {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
// Check for user authentication first (login-based)
|
|
185
|
+
if (request.user && typeof request.user === 'object' && (request.user.userId || request.user.keyId)) {
|
|
186
|
+
return request.user;
|
|
187
|
+
}
|
|
188
|
+
// Check for token authentication (API-based)
|
|
189
|
+
if (request.token && typeof request.token === 'object' && (request.token.userId || request.token.keyId)) {
|
|
190
|
+
return request.token;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Checks if user has specified role with automatic inheritance
|
|
196
|
+
* @llm-rule WHEN: Checking if user can access role-protected resources
|
|
197
|
+
* @llm-rule AVOID: Manual role comparisons - this handles inheritance automatically
|
|
198
|
+
* @llm-rule NOTE: Higher levels inherit lower (admin.org has admin.tenant access)
|
|
199
|
+
* @llm-rule NOTE: INHERITANCE EXAMPLES:
|
|
200
|
+
* @llm-rule NOTE: auth.hasRole('admin.org', 'admin.tenant') → TRUE (org > tenant)
|
|
201
|
+
* @llm-rule NOTE: auth.hasRole('admin.system', 'user.basic') → TRUE (system > basic)
|
|
202
|
+
* @llm-rule NOTE: auth.hasRole('user.basic', 'admin.tenant') → FALSE (basic < tenant)
|
|
203
|
+
* @llm-rule NOTE: Role hierarchy: admin.system > admin.org > admin.tenant > user.max > user.pro > user.basic
|
|
204
|
+
*/
|
|
205
|
+
hasRole(userRoleLevel, requiredRoleLevel) {
|
|
206
|
+
// INHERITANCE RULE: Higher role levels automatically include lower levels
|
|
207
|
+
// Example: admin.org (level 6) includes admin.tenant (level 5) access
|
|
208
|
+
if (!userRoleLevel || !requiredRoleLevel) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
if (!validateRoleLevel(userRoleLevel, this.config.roles)) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
if (!validateRoleLevel(requiredRoleLevel, this.config.roles)) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
const userLevel = this.config.roles[userRoleLevel]?.level;
|
|
218
|
+
const requiredLevel = this.config.roles[requiredRoleLevel]?.level;
|
|
219
|
+
if (userLevel === undefined || requiredLevel === undefined) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
// Higher numeric levels include lower levels
|
|
223
|
+
return userLevel >= requiredLevel;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Checks if user has specific permission with automatic action inheritance
|
|
227
|
+
* @llm-rule WHEN: Checking fine-grained permissions for specific actions
|
|
228
|
+
* @llm-rule AVOID: Hardcoding permission checks - this handles action inheritance
|
|
229
|
+
* @llm-rule NOTE: 'manage:scope' includes ALL other actions for that scope
|
|
230
|
+
* @llm-rule NOTE: PERMISSION INHERITANCE EXAMPLES:
|
|
231
|
+
* @llm-rule NOTE: If user has 'manage:tenant' → can('edit:tenant') returns TRUE
|
|
232
|
+
* @llm-rule NOTE: If user has 'manage:tenant' → can('view:tenant') returns TRUE
|
|
233
|
+
* @llm-rule NOTE: If user has 'edit:tenant' → can('manage:tenant') returns FALSE
|
|
234
|
+
* @llm-rule NOTE: Actions hierarchy: manage > delete > edit > create > view
|
|
235
|
+
*/
|
|
236
|
+
can(user, permission) {
|
|
237
|
+
// PERMISSION INHERITANCE: 'manage:tenant' automatically includes:
|
|
238
|
+
// - view:tenant, create:tenant, edit:tenant, delete:tenant
|
|
239
|
+
// Example: if user has 'manage:tenant', they can do 'edit:tenant'
|
|
240
|
+
if (!user || !permission) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
if (!validatePermission(permission)) {
|
|
244
|
+
throw new Error(`Invalid permission format: "${permission}"`);
|
|
245
|
+
}
|
|
246
|
+
// Check if user has the specific permission
|
|
247
|
+
if (user.permissions && Array.isArray(user.permissions)) {
|
|
248
|
+
if (user.permissions.includes(permission)) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
// Check for manage permission (includes all other actions)
|
|
252
|
+
const [action, scope] = permission.split(':');
|
|
253
|
+
if (action !== 'manage') {
|
|
254
|
+
const managePermission = `manage:${scope}`;
|
|
255
|
+
if (user.permissions.includes(managePermission)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Fallback: check default permissions for user's role.level
|
|
261
|
+
const userRoleLevel = `${user.role}.${user.level}`;
|
|
262
|
+
const defaultPermissions = this.config.permissions.defaults[userRoleLevel];
|
|
263
|
+
if (defaultPermissions && Array.isArray(defaultPermissions)) {
|
|
264
|
+
if (defaultPermissions.includes(permission)) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
// Check for manage permission in defaults
|
|
268
|
+
const [action, scope] = permission.split(':');
|
|
269
|
+
if (action !== 'manage') {
|
|
270
|
+
const managePermission = `manage:${scope}`;
|
|
271
|
+
if (defaultPermissions.includes(managePermission)) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
// ====================================================================
|
|
279
|
+
// EXPRESS MIDDLEWARE
|
|
280
|
+
// ====================================================================
|
|
281
|
+
/**
|
|
282
|
+
* Creates Express authentication middleware for login tokens
|
|
283
|
+
* @llm-rule WHEN: Protecting routes that need authenticated users
|
|
284
|
+
* @llm-rule AVOID: Using for API routes - use requireApiToken instead
|
|
285
|
+
* @llm-rule NOTE: Validates login tokens (type: 'login') and sets req.user
|
|
286
|
+
*/
|
|
287
|
+
requireLoginToken(options = {}) {
|
|
288
|
+
if (!this.config.jwt.secret) {
|
|
289
|
+
throw new Error('JWT secret required for authentication middleware');
|
|
290
|
+
}
|
|
291
|
+
const getToken = options.getToken || this.getDefaultTokenExtractor();
|
|
292
|
+
return (req, res, next) => {
|
|
293
|
+
try {
|
|
294
|
+
const token = getToken(req);
|
|
295
|
+
if (!token) {
|
|
296
|
+
return res.status(401).json({
|
|
297
|
+
error: 'Authentication required',
|
|
298
|
+
message: this.config.middleware.errorMessages.noToken,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
const payload = this.verifyToken(token);
|
|
302
|
+
if (payload.type !== 'login') {
|
|
303
|
+
return res.status(401).json({
|
|
304
|
+
error: 'Invalid token type',
|
|
305
|
+
message: 'Login token required for this endpoint',
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
req.user = payload;
|
|
309
|
+
next();
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
const isExpired = error.message === 'Token has expired';
|
|
313
|
+
const message = isExpired
|
|
314
|
+
? this.config.middleware.errorMessages.expiredToken
|
|
315
|
+
: this.config.middleware.errorMessages.invalidToken;
|
|
316
|
+
return res.status(401).json({
|
|
317
|
+
error: 'Unauthorized',
|
|
318
|
+
message,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Creates Express role-based authorization middleware for authenticated users
|
|
325
|
+
* @llm-rule WHEN: Protecting routes that require specific user roles
|
|
326
|
+
* @llm-rule AVOID: Using without requireLoginToken - this assumes user is already authenticated
|
|
327
|
+
* @llm-rule AVOID: Using with API tokens - API tokens don't have user roles
|
|
328
|
+
* @llm-rule NOTE: User needs ANY role from the array (OR logic)
|
|
329
|
+
* @llm-rule NOTE: Role inheritance applies - admin.org can access admin.tenant routes
|
|
330
|
+
*/
|
|
331
|
+
requireUserRoles(requiredRoles) {
|
|
332
|
+
if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
|
|
333
|
+
throw new Error('requiredRoles must be a non-empty array');
|
|
334
|
+
}
|
|
335
|
+
// Validate all roles exist
|
|
336
|
+
for (const role of requiredRoles) {
|
|
337
|
+
if (!validateRoleLevel(role, this.config.roles)) {
|
|
338
|
+
throw new Error(`Invalid role.level for middleware: "${role}"`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return (req, res, next) => {
|
|
342
|
+
const user = this.user(req);
|
|
343
|
+
if (!user) {
|
|
344
|
+
return res.status(401).json({
|
|
345
|
+
error: 'Authentication required',
|
|
346
|
+
message: this.config.middleware.errorMessages.noToken,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (user.type !== 'login') {
|
|
350
|
+
return res.status(403).json({
|
|
351
|
+
error: 'Access denied',
|
|
352
|
+
message: 'User roles only apply to login tokens',
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const userRoleLevel = `${user.role}.${user.level}`;
|
|
356
|
+
const hasRequiredRole = requiredRoles.some(requiredRole => this.hasRole(userRoleLevel, requiredRole));
|
|
357
|
+
if (!hasRequiredRole) {
|
|
358
|
+
return res.status(403).json({
|
|
359
|
+
error: 'Access denied',
|
|
360
|
+
message: this.config.middleware.errorMessages.insufficientRole,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
next();
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Creates Express permission-based authorization middleware for authenticated users
|
|
368
|
+
* @llm-rule WHEN: Protecting routes that require specific user permissions
|
|
369
|
+
* @llm-rule AVOID: Using without requireLoginToken - this assumes user is already authenticated
|
|
370
|
+
* @llm-rule AVOID: Using with API tokens - API tokens don't have user permissions
|
|
371
|
+
* @llm-rule NOTE: User needs ALL permissions from the array (AND logic)
|
|
372
|
+
* @llm-rule NOTE: Permission inheritance applies - manage:tenant can access edit:tenant routes
|
|
373
|
+
*/
|
|
374
|
+
requireUserPermissions(requiredPermissions) {
|
|
375
|
+
if (!Array.isArray(requiredPermissions) || requiredPermissions.length === 0) {
|
|
376
|
+
throw new Error('requiredPermissions must be a non-empty array');
|
|
377
|
+
}
|
|
378
|
+
// Validate all permissions
|
|
379
|
+
for (const permission of requiredPermissions) {
|
|
380
|
+
if (!validatePermission(permission)) {
|
|
381
|
+
throw new Error(`Invalid permission format for middleware: "${permission}"`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return (req, res, next) => {
|
|
385
|
+
const user = this.user(req);
|
|
386
|
+
if (!user) {
|
|
387
|
+
return res.status(401).json({
|
|
388
|
+
error: 'Authentication required',
|
|
389
|
+
message: this.config.middleware.errorMessages.noToken,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (user.type !== 'login') {
|
|
393
|
+
return res.status(403).json({
|
|
394
|
+
error: 'Access denied',
|
|
395
|
+
message: 'User permissions only apply to login tokens',
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const hasAllPermissions = requiredPermissions.every(permission => this.can(user, permission));
|
|
399
|
+
if (!hasAllPermissions) {
|
|
400
|
+
return res.status(403).json({
|
|
401
|
+
error: 'Access denied',
|
|
402
|
+
message: this.config.middleware.errorMessages.insufficientPermissions,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
next();
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Creates Express API token authentication middleware for external access
|
|
410
|
+
* @llm-rule WHEN: Protecting API routes for third-party integrations
|
|
411
|
+
* @llm-rule AVOID: Using for user routes - use requireLoginToken instead
|
|
412
|
+
* @llm-rule NOTE: Validates API tokens (type: 'api_key') and sets req.token
|
|
413
|
+
*/
|
|
414
|
+
requireApiToken(options = {}) {
|
|
415
|
+
if (!this.config.jwt.secret) {
|
|
416
|
+
throw new Error('JWT secret required for API token authentication middleware');
|
|
417
|
+
}
|
|
418
|
+
const getToken = options.getToken || this.getDefaultTokenExtractor();
|
|
419
|
+
return (req, res, next) => {
|
|
420
|
+
try {
|
|
421
|
+
const token = getToken(req);
|
|
422
|
+
if (!token) {
|
|
423
|
+
return res.status(401).json({
|
|
424
|
+
error: 'API token required',
|
|
425
|
+
message: 'API token required for this endpoint',
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
const payload = this.verifyToken(token);
|
|
429
|
+
if (payload.type !== 'api_key') {
|
|
430
|
+
return res.status(401).json({
|
|
431
|
+
error: 'Invalid token type',
|
|
432
|
+
message: 'API token required for this endpoint',
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
req.token = payload;
|
|
436
|
+
next();
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
const isExpired = error.message === 'Token has expired';
|
|
440
|
+
const message = isExpired
|
|
441
|
+
? 'API token has expired'
|
|
442
|
+
: 'Invalid API token';
|
|
443
|
+
return res.status(401).json({
|
|
444
|
+
error: 'Unauthorized',
|
|
445
|
+
message,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Gets default token extractor that checks headers, cookies, and query params
|
|
452
|
+
* @llm-rule WHEN: Need custom token extraction logic
|
|
453
|
+
* @llm-rule AVOID: Modifying directly - pass custom getToken to middleware options
|
|
454
|
+
*/
|
|
455
|
+
getDefaultTokenExtractor() {
|
|
456
|
+
return (request) => {
|
|
457
|
+
// Check Authorization header (Bearer token)
|
|
458
|
+
const authHeader = request.headers.authorization;
|
|
459
|
+
if (authHeader && typeof authHeader === 'string') {
|
|
460
|
+
const match = authHeader.match(/^Bearer\s+(.+)$/);
|
|
461
|
+
if (match) {
|
|
462
|
+
return match[1];
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// Check cookies
|
|
466
|
+
if (request.cookies?.token) {
|
|
467
|
+
return request.cookies.token;
|
|
468
|
+
}
|
|
469
|
+
// Check query parameter
|
|
470
|
+
if (request.query?.token && typeof request.query.token === 'string') {
|
|
471
|
+
return request.query.token;
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
//# sourceMappingURL=auth.js.map
|