@stonecrop/casl-middleware 0.7.0
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/README.md +149 -0
- package/dist/casl-middleware.d.ts +158 -0
- package/dist/casl-middleware.js +40571 -0
- package/dist/casl-middleware.js.map +1 -0
- package/dist/casl_middleware.tsbuildinfo +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +3 -0
- package/dist/src/middleware/ability.d.ts +55 -0
- package/dist/src/middleware/ability.d.ts.map +1 -0
- package/dist/src/middleware/ability.js +139 -0
- package/dist/src/middleware/graphql.d.ts +11 -0
- package/dist/src/middleware/graphql.d.ts.map +1 -0
- package/dist/src/middleware/graphql.js +120 -0
- package/dist/src/middleware/introspection.d.ts +71 -0
- package/dist/src/middleware/introspection.d.ts.map +1 -0
- package/dist/src/middleware/introspection.js +169 -0
- package/dist/src/middleware/jwt.d.ts +114 -0
- package/dist/src/middleware/jwt.d.ts.map +1 -0
- package/dist/src/middleware/jwt.js +291 -0
- package/dist/src/middleware/postgraphile.d.ts +7 -0
- package/dist/src/middleware/postgraphile.d.ts.map +1 -0
- package/dist/src/middleware/postgraphile.js +80 -0
- package/dist/src/middleware/yoga.d.ts +15 -0
- package/dist/src/middleware/yoga.d.ts.map +1 -0
- package/dist/src/middleware/yoga.js +32 -0
- package/dist/src/tsdoc-metadata.json +11 -0
- package/dist/src/types/index.d.ts +114 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +0 -0
- package/dist/tests/ability.test.d.ts +2 -0
- package/dist/tests/ability.test.d.ts.map +1 -0
- package/dist/tests/ability.test.js +125 -0
- package/dist/tests/helpers/test-utils.d.ts +46 -0
- package/dist/tests/helpers/test-utils.d.ts.map +1 -0
- package/dist/tests/helpers/test-utils.js +92 -0
- package/dist/tests/introspection.test.d.ts +2 -0
- package/dist/tests/introspection.test.d.ts.map +1 -0
- package/dist/tests/introspection.test.js +368 -0
- package/dist/tests/jwt.test.d.ts +2 -0
- package/dist/tests/jwt.test.d.ts.map +1 -0
- package/dist/tests/jwt.test.js +371 -0
- package/dist/tests/middleware.test.d.ts +2 -0
- package/dist/tests/middleware.test.d.ts.map +1 -0
- package/dist/tests/middleware.test.js +184 -0
- package/dist/tests/postgraphile-plugin.test.d.ts +2 -0
- package/dist/tests/postgraphile-plugin.test.d.ts.map +1 -0
- package/dist/tests/postgraphile-plugin.test.js +56 -0
- package/dist/tests/setup.d.ts +2 -0
- package/dist/tests/setup.d.ts.map +1 -0
- package/dist/tests/setup.js +11 -0
- package/dist/tests/user-roles.test.d.ts +2 -0
- package/dist/tests/user-roles.test.d.ts.map +1 -0
- package/dist/tests/user-roles.test.js +157 -0
- package/dist/tests/yoga-plugin.test.d.ts +2 -0
- package/dist/tests/yoga-plugin.test.d.ts.map +1 -0
- package/dist/tests/yoga-plugin.test.js +47 -0
- package/package.json +91 -0
- package/src/index.ts +15 -0
- package/src/middleware/ability.ts +191 -0
- package/src/middleware/graphql.ts +157 -0
- package/src/middleware/introspection.ts +258 -0
- package/src/middleware/jwt.ts +394 -0
- package/src/middleware/postgraphile.ts +93 -0
- package/src/middleware/yoga.ts +39 -0
- package/src/types/index.ts +133 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import type { Context, User } from '../types';
|
|
3
|
+
export interface JWTConfig {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
secret?: string;
|
|
6
|
+
publicKey?: string;
|
|
7
|
+
algorithms?: jwt.Algorithm[];
|
|
8
|
+
issuer?: string;
|
|
9
|
+
audience?: string;
|
|
10
|
+
extractUser?: (payload: any) => User | undefined;
|
|
11
|
+
headerName?: string;
|
|
12
|
+
tokenPrefix?: string;
|
|
13
|
+
optional?: boolean;
|
|
14
|
+
maxAge?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface JWTPayload extends jwt.JwtPayload {
|
|
17
|
+
sub?: string;
|
|
18
|
+
roles?: string[];
|
|
19
|
+
permissions?: Array<{
|
|
20
|
+
action: string;
|
|
21
|
+
subject: string;
|
|
22
|
+
conditions?: any;
|
|
23
|
+
}>;
|
|
24
|
+
[key: string]: any;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* JWT middleware factory for GraphQL servers
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* // In Nuxt Yoga
|
|
32
|
+
* export default defineNuxtConfig({
|
|
33
|
+
* yoga: {
|
|
34
|
+
* middleware: [
|
|
35
|
+
* createJWTMiddleware({
|
|
36
|
+
* enabled: true,
|
|
37
|
+
* secret: process.env.JWT_SECRET,
|
|
38
|
+
* optional: true // Don't fail if no token
|
|
39
|
+
* })
|
|
40
|
+
* ]
|
|
41
|
+
* }
|
|
42
|
+
* })
|
|
43
|
+
*
|
|
44
|
+
* // In Postgraphile
|
|
45
|
+
* const jwtPlugin = createPostgraphileJWTPlugin({
|
|
46
|
+
* secret: process.env.JWT_SECRET,
|
|
47
|
+
* extractUser: (payload) => ({
|
|
48
|
+
* id: payload.user_id,
|
|
49
|
+
* roles: payload.user_roles
|
|
50
|
+
* })
|
|
51
|
+
* })
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare const createJWTMiddleware: (config?: JWTConfig) => (context: Context, next: () => Promise<any>) => Promise<any>;
|
|
55
|
+
/**
|
|
56
|
+
* Create a JWT token with user data
|
|
57
|
+
*/
|
|
58
|
+
export declare const createJWT: (user: User, config: {
|
|
59
|
+
secret: string;
|
|
60
|
+
expiresIn?: string | number;
|
|
61
|
+
issuer?: string;
|
|
62
|
+
audience?: string;
|
|
63
|
+
additionalClaims?: Record<string, any>;
|
|
64
|
+
}) => string;
|
|
65
|
+
/**
|
|
66
|
+
* Integration with CASL ability builder
|
|
67
|
+
*/
|
|
68
|
+
export declare const createJWTAbilityBuilder: (config?: JWTConfig) => (user?: User) => Promise<import("./ability").AppAbility>;
|
|
69
|
+
/**
|
|
70
|
+
* Postgraphile-specific JWT plugin
|
|
71
|
+
*/
|
|
72
|
+
export declare const createPostgraphileJWTPlugin: (config: JWTConfig) => {
|
|
73
|
+
name: string;
|
|
74
|
+
version: string;
|
|
75
|
+
grafast: {
|
|
76
|
+
hooks: {
|
|
77
|
+
context(ctx: any, build: any): Promise<any>;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Express/Koa middleware for REST endpoints
|
|
83
|
+
*/
|
|
84
|
+
export declare const createHTTPJWTMiddleware: (config: JWTConfig) => (req: any, res: any, next: any) => Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Refresh token utilities
|
|
87
|
+
*/
|
|
88
|
+
export declare const refreshTokenUtils: {
|
|
89
|
+
/**
|
|
90
|
+
* Create access and refresh tokens
|
|
91
|
+
*/
|
|
92
|
+
createTokenPair: (user: User, config: {
|
|
93
|
+
accessSecret: string;
|
|
94
|
+
refreshSecret: string;
|
|
95
|
+
accessExpiresIn?: string;
|
|
96
|
+
refreshExpiresIn?: string;
|
|
97
|
+
}) => {
|
|
98
|
+
accessToken: string;
|
|
99
|
+
refreshToken: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Verify refresh token and create new access token
|
|
103
|
+
*/
|
|
104
|
+
refreshAccessToken: (refreshToken: string, config: {
|
|
105
|
+
accessSecret: string;
|
|
106
|
+
refreshSecret: string;
|
|
107
|
+
getUserById: (id: string) => Promise<User | null>;
|
|
108
|
+
accessExpiresIn?: string;
|
|
109
|
+
}) => Promise<{
|
|
110
|
+
accessToken: string;
|
|
111
|
+
user: User;
|
|
112
|
+
}>;
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=jwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../src/middleware/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,cAAc,CAAA;AAE9B,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAG7C,MAAM,WAAW,SAAS;IACzB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,GAAG,CAAC,SAAS,EAAE,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,SAAS,CAAA;IAChD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,UAAW,SAAQ,GAAG,CAAC,UAAU;IACjD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,WAAW,CAAC,EAAE,KAAK,CAAC;QACnB,MAAM,EAAE,MAAM,CAAA;QACd,OAAO,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,GAAG,CAAA;KAChB,CAAC,CAAA;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAClB;AAeD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAQ,SAAc,MAoB3C,SAAS,OAAO,EAAE,MAAM,MAAM,OAAO,CAAC,GAAG,CAAC,iBAoExD,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,GACrB,MAAM,IAAI,EACV,QAAQ;IACP,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACtC,KACC,MAiBF,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAQ,SAAc,MAC/C,OAAO,IAAI,4CAsBzB,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,2BAA2B,GAAI,QAAQ,SAAS;;;;;yBAQtC,GAAG,SAAS,GAAG;;;CAwBrC,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,uBAAuB,GAAI,QAAQ,SAAS,MAI1C,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,GAAG,kBA4B3C,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,iBAAiB;IAC7B;;OAEG;4BAEI,IAAI,UACF;QACP,YAAY,EAAE,MAAM,CAAA;QACpB,aAAa,EAAE,MAAM,CAAA;QACrB,eAAe,CAAC,EAAE,MAAM,CAAA;QACxB,gBAAgB,CAAC,EAAE,MAAM,CAAA;KACzB;;;;IAkCF;;OAEG;uCAEY,MAAM,UACZ;QACP,YAAY,EAAE,MAAM,CAAA;QACpB,aAAa,EAAE,MAAM,CAAA;QACrB,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAA;QACjD,eAAe,CAAC,EAAE,MAAM,CAAA;KACxB;;;;CAyCF,CAAA"}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import { AbilityBuilder, PureAbility } from '@casl/ability';
|
|
3
|
+
import { defaultAbilityBuilder } from './ability';
|
|
4
|
+
/**
|
|
5
|
+
* Default user extractor from JWT payload
|
|
6
|
+
*/
|
|
7
|
+
const defaultUserExtractor = (payload) => {
|
|
8
|
+
if (!payload.sub)
|
|
9
|
+
return undefined;
|
|
10
|
+
return {
|
|
11
|
+
id: payload.sub,
|
|
12
|
+
roles: payload.roles || [],
|
|
13
|
+
...payload, // Include any additional claims
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* JWT middleware factory for GraphQL servers
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* // In Nuxt Yoga
|
|
22
|
+
* export default defineNuxtConfig({
|
|
23
|
+
* yoga: {
|
|
24
|
+
* middleware: [
|
|
25
|
+
* createJWTMiddleware({
|
|
26
|
+
* enabled: true,
|
|
27
|
+
* secret: process.env.JWT_SECRET,
|
|
28
|
+
* optional: true // Don't fail if no token
|
|
29
|
+
* })
|
|
30
|
+
* ]
|
|
31
|
+
* }
|
|
32
|
+
* })
|
|
33
|
+
*
|
|
34
|
+
* // In Postgraphile
|
|
35
|
+
* const jwtPlugin = createPostgraphileJWTPlugin({
|
|
36
|
+
* secret: process.env.JWT_SECRET,
|
|
37
|
+
* extractUser: (payload) => ({
|
|
38
|
+
* id: payload.user_id,
|
|
39
|
+
* roles: payload.user_roles
|
|
40
|
+
* })
|
|
41
|
+
* })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export const createJWTMiddleware = (config = {}) => {
|
|
45
|
+
const { enabled = true, secret, publicKey, algorithms = ['HS256'], issuer, audience, headerName = 'authorization', tokenPrefix = 'Bearer ', optional = false, extractUser = defaultUserExtractor, maxAge, } = config;
|
|
46
|
+
// Validate configuration
|
|
47
|
+
if (enabled && !secret && !publicKey) {
|
|
48
|
+
throw new Error('JWT middleware requires either secret or publicKey');
|
|
49
|
+
}
|
|
50
|
+
return async (context, next) => {
|
|
51
|
+
// Skip if JWT is disabled
|
|
52
|
+
if (!enabled) {
|
|
53
|
+
return next();
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
// Extract token from request headers
|
|
57
|
+
const authHeader = context.req?.headers?.get?.(headerName) ||
|
|
58
|
+
context.request?.headers?.get?.(headerName) ||
|
|
59
|
+
context.headers?.[headerName];
|
|
60
|
+
if (!authHeader) {
|
|
61
|
+
if (optional) {
|
|
62
|
+
return next();
|
|
63
|
+
}
|
|
64
|
+
throw new Error('No authorization header found');
|
|
65
|
+
}
|
|
66
|
+
// Remove token prefix
|
|
67
|
+
const token = authHeader.startsWith(tokenPrefix) ? authHeader.slice(tokenPrefix.length) : authHeader;
|
|
68
|
+
// Prepare verification options
|
|
69
|
+
const verifyOptions = {
|
|
70
|
+
algorithms,
|
|
71
|
+
...(issuer && { issuer }),
|
|
72
|
+
...(audience && { audience }),
|
|
73
|
+
...(maxAge && { maxAge }),
|
|
74
|
+
};
|
|
75
|
+
// Verify and decode token
|
|
76
|
+
const secretOrPublicKey = publicKey || secret;
|
|
77
|
+
const payload = jwt.verify(token, secretOrPublicKey, verifyOptions);
|
|
78
|
+
// Extract user from payload
|
|
79
|
+
const user = extractUser(payload);
|
|
80
|
+
if (user) {
|
|
81
|
+
context.user = user;
|
|
82
|
+
// Store the raw payload for potential use
|
|
83
|
+
context.jwtPayload = payload;
|
|
84
|
+
}
|
|
85
|
+
// Continue to next middleware
|
|
86
|
+
return next();
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (optional) {
|
|
90
|
+
// Log error in development
|
|
91
|
+
if (process.env.NODE_ENV === 'development') {
|
|
92
|
+
console.warn('JWT verification failed (optional):', error.message);
|
|
93
|
+
}
|
|
94
|
+
// Continue without user if optional
|
|
95
|
+
return next();
|
|
96
|
+
}
|
|
97
|
+
// Re-throw with more specific error messages
|
|
98
|
+
if (error.name === 'TokenExpiredError') {
|
|
99
|
+
throw new Error('Token has expired');
|
|
100
|
+
}
|
|
101
|
+
else if (error.name === 'JsonWebTokenError') {
|
|
102
|
+
throw new Error('Invalid token');
|
|
103
|
+
}
|
|
104
|
+
else if (error.name === 'NotBeforeError') {
|
|
105
|
+
throw new Error('Token not active yet');
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Create a JWT token with user data
|
|
113
|
+
*/
|
|
114
|
+
export const createJWT = (user, config) => {
|
|
115
|
+
const { secret, expiresIn = '1h', issuer, audience, additionalClaims = {} } = config;
|
|
116
|
+
const payload = {
|
|
117
|
+
sub: user.id,
|
|
118
|
+
roles: user.roles || [],
|
|
119
|
+
...additionalClaims,
|
|
120
|
+
};
|
|
121
|
+
const signOptions = {};
|
|
122
|
+
// Add optional fields only if they exist
|
|
123
|
+
if (issuer !== undefined)
|
|
124
|
+
signOptions.issuer = issuer;
|
|
125
|
+
if (audience !== undefined)
|
|
126
|
+
signOptions.audience = audience;
|
|
127
|
+
if (expiresIn !== undefined)
|
|
128
|
+
signOptions.expiresIn = expiresIn; // Type cast to avoid TS issues
|
|
129
|
+
return jwt.sign(payload, secret, signOptions);
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Integration with CASL ability builder
|
|
133
|
+
*/
|
|
134
|
+
export const createJWTAbilityBuilder = (config = {}) => {
|
|
135
|
+
return async (user) => {
|
|
136
|
+
// If user has direct permissions in JWT, use those
|
|
137
|
+
const jwtPermissions = user?.permissions;
|
|
138
|
+
if (jwtPermissions && Array.isArray(jwtPermissions)) {
|
|
139
|
+
// Build ability from JWT permissions
|
|
140
|
+
const { can, cannot, build } = new AbilityBuilder(PureAbility);
|
|
141
|
+
jwtPermissions.forEach((permission) => {
|
|
142
|
+
if (permission.inverted) {
|
|
143
|
+
cannot(permission.action, permission.subject, permission.conditions);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
can(permission.action, permission.subject, permission.conditions);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return build();
|
|
150
|
+
}
|
|
151
|
+
// Fall back to role-based abilities
|
|
152
|
+
return defaultAbilityBuilder(user);
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Postgraphile-specific JWT plugin
|
|
157
|
+
*/
|
|
158
|
+
export const createPostgraphileJWTPlugin = (config) => {
|
|
159
|
+
return {
|
|
160
|
+
name: 'JWTAuthPlugin',
|
|
161
|
+
version: '1.0.0',
|
|
162
|
+
// Hook into Postgraphile's context building
|
|
163
|
+
grafast: {
|
|
164
|
+
hooks: {
|
|
165
|
+
async context(ctx, build) {
|
|
166
|
+
const middleware = createJWTMiddleware(config);
|
|
167
|
+
// Create a simple context object that the middleware can work with
|
|
168
|
+
const context = {
|
|
169
|
+
req: ctx.req,
|
|
170
|
+
headers: ctx.req?.headers,
|
|
171
|
+
user: undefined,
|
|
172
|
+
jwtPayload: undefined,
|
|
173
|
+
};
|
|
174
|
+
// Run the JWT middleware
|
|
175
|
+
await middleware(context, async () => { });
|
|
176
|
+
// Add user to Postgraphile context
|
|
177
|
+
if (context.user) {
|
|
178
|
+
return { ...ctx, user: context.user, jwtPayload: context.jwtPayload };
|
|
179
|
+
}
|
|
180
|
+
return ctx;
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Express/Koa middleware for REST endpoints
|
|
188
|
+
*/
|
|
189
|
+
export const createHTTPJWTMiddleware = (config) => {
|
|
190
|
+
const jwtMiddleware = createJWTMiddleware(config);
|
|
191
|
+
// Express middleware
|
|
192
|
+
return async (req, res, next) => {
|
|
193
|
+
const context = {
|
|
194
|
+
req: {
|
|
195
|
+
headers: {
|
|
196
|
+
get: (name) => req.headers[name],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
headers: req.headers,
|
|
200
|
+
user: undefined,
|
|
201
|
+
jwtPayload: undefined,
|
|
202
|
+
};
|
|
203
|
+
try {
|
|
204
|
+
await jwtMiddleware(context, async () => { });
|
|
205
|
+
req.user = context.user;
|
|
206
|
+
req.jwtPayload = context.jwtPayload;
|
|
207
|
+
next();
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (config.optional) {
|
|
211
|
+
next();
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
res.status(401).json({
|
|
215
|
+
error: error.message,
|
|
216
|
+
code: 'UNAUTHORIZED',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Refresh token utilities
|
|
224
|
+
*/
|
|
225
|
+
export const refreshTokenUtils = {
|
|
226
|
+
/**
|
|
227
|
+
* Create access and refresh tokens
|
|
228
|
+
*/
|
|
229
|
+
createTokenPair: (user, config) => {
|
|
230
|
+
const { accessSecret, refreshSecret, accessExpiresIn = '15m', refreshExpiresIn = '7d' } = config;
|
|
231
|
+
const accessPayload = {
|
|
232
|
+
sub: user.id,
|
|
233
|
+
roles: user.roles,
|
|
234
|
+
type: 'access',
|
|
235
|
+
};
|
|
236
|
+
const refreshPayload = {
|
|
237
|
+
sub: user.id,
|
|
238
|
+
type: 'refresh',
|
|
239
|
+
};
|
|
240
|
+
// Create access token with proper options
|
|
241
|
+
const accessOptions = {};
|
|
242
|
+
if (accessExpiresIn) {
|
|
243
|
+
accessOptions.expiresIn = accessExpiresIn; // Type cast to avoid TS issues
|
|
244
|
+
}
|
|
245
|
+
const accessToken = jwt.sign(accessPayload, accessSecret, accessOptions);
|
|
246
|
+
// Create refresh token with proper options
|
|
247
|
+
const refreshOptions = {};
|
|
248
|
+
if (refreshExpiresIn) {
|
|
249
|
+
refreshOptions.expiresIn = refreshExpiresIn; // Type cast to avoid TS issues
|
|
250
|
+
}
|
|
251
|
+
const refreshToken = jwt.sign(refreshPayload, refreshSecret, refreshOptions);
|
|
252
|
+
return { accessToken, refreshToken };
|
|
253
|
+
},
|
|
254
|
+
/**
|
|
255
|
+
* Verify refresh token and create new access token
|
|
256
|
+
*/
|
|
257
|
+
refreshAccessToken: async (refreshToken, config) => {
|
|
258
|
+
const { accessSecret, refreshSecret, getUserById, accessExpiresIn = '15m' } = config;
|
|
259
|
+
try {
|
|
260
|
+
// Verify refresh token
|
|
261
|
+
const payload = jwt.verify(refreshToken, refreshSecret);
|
|
262
|
+
if (payload.type !== 'refresh') {
|
|
263
|
+
throw new Error('Invalid token type');
|
|
264
|
+
}
|
|
265
|
+
// Get fresh user data
|
|
266
|
+
const user = await getUserById(payload.sub);
|
|
267
|
+
if (!user) {
|
|
268
|
+
throw new Error('User not found');
|
|
269
|
+
}
|
|
270
|
+
// Create new access token
|
|
271
|
+
const accessPayload = {
|
|
272
|
+
sub: user.id,
|
|
273
|
+
roles: user.roles,
|
|
274
|
+
type: 'access',
|
|
275
|
+
};
|
|
276
|
+
// Create access token with proper options
|
|
277
|
+
const accessOptions = {};
|
|
278
|
+
if (accessExpiresIn) {
|
|
279
|
+
accessOptions.expiresIn = accessExpiresIn; // Type cast to avoid TS issues
|
|
280
|
+
}
|
|
281
|
+
const accessToken = jwt.sign(accessPayload, accessSecret, accessOptions);
|
|
282
|
+
return { accessToken, user };
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (error.name === 'TokenExpiredError') {
|
|
286
|
+
throw new Error('Refresh token expired');
|
|
287
|
+
}
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgraphile.d.ts","sourceRoot":"","sources":["../../../src/middleware/postgraphile.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAI5D;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,cAAc,CAAC,MAkFzC,CAAA"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { extendSchema, gql } from 'postgraphile/utils';
|
|
2
|
+
import { createAbility } from './ability';
|
|
3
|
+
/**
|
|
4
|
+
* PostGraphile plugin for CASL authorization
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export const pglCaslPlugin = extendSchema(build => {
|
|
8
|
+
const { grafast: { constant, object, sideEffect }, } = build;
|
|
9
|
+
return {
|
|
10
|
+
typeDefs: gql `
|
|
11
|
+
input CreateAbilityInput {
|
|
12
|
+
userId: String!
|
|
13
|
+
roles: [String!]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type AbilityResponse {
|
|
17
|
+
success: Boolean!
|
|
18
|
+
ability: JSON
|
|
19
|
+
message: String
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type SecretData {
|
|
23
|
+
id: String!
|
|
24
|
+
content: String!
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
extend type Query {
|
|
28
|
+
getSecretData: SecretData
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
extend type Mutation {
|
|
32
|
+
createAbility(input: CreateAbilityInput!): AbilityResponse!
|
|
33
|
+
}
|
|
34
|
+
`,
|
|
35
|
+
objects: {
|
|
36
|
+
Query: {
|
|
37
|
+
plans: {
|
|
38
|
+
async getSecretData() {
|
|
39
|
+
// TODO: This should be protected by CASL
|
|
40
|
+
// const $ability = context<Context>().get('ability')
|
|
41
|
+
// if (!$ability.can('read', 'SecretData')) {
|
|
42
|
+
// throw new Error('Access denied')
|
|
43
|
+
// }
|
|
44
|
+
return object({
|
|
45
|
+
id: constant('123'),
|
|
46
|
+
content: constant('This is protected content'),
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
Mutation: {
|
|
52
|
+
plans: {
|
|
53
|
+
async createAbility(_plan, fieldArgs) {
|
|
54
|
+
const $input = fieldArgs.getRaw().input;
|
|
55
|
+
const $userId = $input.userId;
|
|
56
|
+
const $roles = $input.roles;
|
|
57
|
+
return sideEffect([$userId, $roles], async ([userId, roles]) => {
|
|
58
|
+
// Make this async
|
|
59
|
+
try {
|
|
60
|
+
const ability = await createAbility({ id: userId, roles }); // Await here
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
ability: ability.rules,
|
|
64
|
+
message: 'Ability created successfully',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
ability: null,
|
|
71
|
+
message: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Plugin } from 'graphql-yoga';
|
|
2
|
+
import type { Context, MiddlewareOptions } from '../types';
|
|
3
|
+
export declare const yogaCaslPlugin: Plugin<Context>;
|
|
4
|
+
/**
|
|
5
|
+
* Create a GraphQL Yoga plugin for CASL authorization
|
|
6
|
+
* Note: This is a placeholder for future implementation
|
|
7
|
+
*
|
|
8
|
+
* @param options - CASL middleware configuration options
|
|
9
|
+
* @returns Yoga plugin
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
export declare const createYogaPlugin: (options?: MiddlewareOptions) => {
|
|
13
|
+
onExecute: ({ args }: any) => Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=yoga.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yoga.d.ts","sourceRoot":"","sources":["../../../src/middleware/yoga.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAI1C,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAO1D,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,OAAO,CAO1C,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,UAAS,iBAAsB;0BAIlC,GAAG;CAMhC,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createAbility } from './ability';
|
|
2
|
+
import { createCaslMiddleware } from './graphql';
|
|
3
|
+
const getLoggedInUser = () => {
|
|
4
|
+
// Mock user for demonstration purposes
|
|
5
|
+
return { id: '1', roles: ['editor'] };
|
|
6
|
+
};
|
|
7
|
+
export const yogaCaslPlugin = {
|
|
8
|
+
onContextBuilding: async ({ context, extendContext }) => {
|
|
9
|
+
// Make this async
|
|
10
|
+
const user = getLoggedInUser();
|
|
11
|
+
const ability = await createAbility(user); // Await here
|
|
12
|
+
extendContext({ ability, user });
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Create a GraphQL Yoga plugin for CASL authorization
|
|
17
|
+
* Note: This is a placeholder for future implementation
|
|
18
|
+
*
|
|
19
|
+
* @param options - CASL middleware configuration options
|
|
20
|
+
* @returns Yoga plugin
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export const createYogaPlugin = (options = {}) => {
|
|
24
|
+
const middleware = createCaslMiddleware(options);
|
|
25
|
+
return {
|
|
26
|
+
onExecute: async ({ args }) => {
|
|
27
|
+
// TODO: Implement Yoga plugin
|
|
28
|
+
// This would integrate with GraphQL Yoga's plugin system
|
|
29
|
+
console.log('Yoga plugin not yet implemented');
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
+
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
+
{
|
|
4
|
+
"tsdocVersion": "0.12",
|
|
5
|
+
"toolPackages": [
|
|
6
|
+
{
|
|
7
|
+
"packageName": "@microsoft/api-extractor",
|
|
8
|
+
"packageVersion": "7.55.2"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { GraphQLResolveInfo } from 'graphql';
|
|
2
|
+
import type { AppAbility, AbilityBuilderFunction } from '../middleware/ability';
|
|
3
|
+
/**
|
|
4
|
+
* User information for authorization
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export interface User {
|
|
8
|
+
/** Unique identifier for the user */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Array of role names assigned to the user */
|
|
11
|
+
roles?: string[];
|
|
12
|
+
/** Additional user properties */
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* GraphQL context with CASL ability and user information
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export interface Context {
|
|
20
|
+
/** CASL ability instance for authorization checks */
|
|
21
|
+
ability?: AppAbility;
|
|
22
|
+
/** Current authenticated user */
|
|
23
|
+
user?: User;
|
|
24
|
+
/** Additional context properties */
|
|
25
|
+
[key: string]: any;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Configuration options for CASL middleware
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
export interface MiddlewareOptions {
|
|
32
|
+
/** Mapping of GraphQL types to authorization subjects */
|
|
33
|
+
subjectMap?: Record<string, string>;
|
|
34
|
+
/** Mapping of GraphQL operations to CASL actions */
|
|
35
|
+
actionMap?: Record<string, string>;
|
|
36
|
+
/** Field-level permission rules */
|
|
37
|
+
fieldPermissions?: Record<string, FieldPermission[]>;
|
|
38
|
+
/** Custom function to build user abilities */
|
|
39
|
+
abilityBuilder?: AbilityBuilderFunction;
|
|
40
|
+
/** Enable debug logging */
|
|
41
|
+
debug?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Field-level permission definition for fine-grained access control
|
|
45
|
+
* @public
|
|
46
|
+
*/
|
|
47
|
+
export interface FieldPermission {
|
|
48
|
+
/** CASL action (e.g., 'read', 'write') */
|
|
49
|
+
action: string;
|
|
50
|
+
/** Subject/resource type to apply permission to */
|
|
51
|
+
subject: string;
|
|
52
|
+
/** Specific field name (optional) */
|
|
53
|
+
field?: string;
|
|
54
|
+
/** Conditional rules for permission */
|
|
55
|
+
conditions?: any;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* GraphQL resolver function type
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
export type ResolverFn = (root: any, args: any, context: Context, info: GraphQLResolveInfo) => Promise<any> | any;
|
|
62
|
+
/**
|
|
63
|
+
* Middleware function that wraps a GraphQL resolver with authorization logic
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
66
|
+
export type MiddlewareFn = (resolve: ResolverFn, root: any, args: any, context: Context, info: GraphQLResolveInfo) => Promise<any> | any;
|
|
67
|
+
/**
|
|
68
|
+
* Plugin configuration options for framework integrations
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
export interface PluginOptions extends MiddlewareOptions {
|
|
72
|
+
/** Custom function to build user abilities */
|
|
73
|
+
abilityBuilder?: AbilityBuilderFunction;
|
|
74
|
+
/** Ability caching configuration */
|
|
75
|
+
cacheOptions?: {
|
|
76
|
+
/** Time-to-live in milliseconds */
|
|
77
|
+
ttl?: number;
|
|
78
|
+
/** Function to generate cache key from user */
|
|
79
|
+
key?: (user?: User) => string;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Response type for ability creation mutations
|
|
84
|
+
* @public
|
|
85
|
+
*/
|
|
86
|
+
export interface AbilityResponse {
|
|
87
|
+
/** Whether the ability was created successfully */
|
|
88
|
+
success: boolean;
|
|
89
|
+
/** The created ability rules */
|
|
90
|
+
ability: any;
|
|
91
|
+
/** Success or error message */
|
|
92
|
+
message: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Input type for creating a new ability
|
|
96
|
+
* @public
|
|
97
|
+
*/
|
|
98
|
+
export interface CreateAbilityInput {
|
|
99
|
+
/** User ID to create ability for */
|
|
100
|
+
userId: string;
|
|
101
|
+
/** Array of role names to assign */
|
|
102
|
+
roles: string[];
|
|
103
|
+
}
|
|
104
|
+
export interface AbilityRule {
|
|
105
|
+
id?: string;
|
|
106
|
+
roleId?: string;
|
|
107
|
+
userId?: string;
|
|
108
|
+
action: string | string[];
|
|
109
|
+
subject: string;
|
|
110
|
+
fields?: string[];
|
|
111
|
+
conditions?: any;
|
|
112
|
+
inverted?: boolean;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAE5C,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAE/E;;;GAGG;AACH,MAAM,WAAW,IAAI;IACpB,qCAAqC;IACrC,EAAE,EAAE,MAAM,CAAA;IACV,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,iCAAiC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,OAAO;IACvB,qDAAqD;IACrD,OAAO,CAAC,EAAE,UAAU,CAAA;IACpB,iCAAiC;IACjC,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,oCAAoC;IACpC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAClB;AAID;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IACjC,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,mCAAmC;IACnC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,CAAA;IACpD,8CAA8C;IAC9C,cAAc,CAAC,EAAE,sBAAsB,CAAA;IACvC,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAA;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC/B,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAA;IACd,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAA;IACf,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,uCAAuC;IACvC,UAAU,CAAC,EAAE,GAAG,CAAA;CAChB;AAED;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,kBAAkB,KAAK,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAA;AAEjH;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,CAC1B,OAAO,EAAE,UAAU,EACnB,IAAI,EAAE,GAAG,EACT,IAAI,EAAE,GAAG,EACT,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,kBAAkB,KACpB,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAA;AAEvB;;;GAGG;AACH,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACvD,8CAA8C;IAC9C,cAAc,CAAC,EAAE,sBAAsB,CAAA;IACvC,oCAAoC;IACpC,YAAY,CAAC,EAAE;QACd,mCAAmC;QACnC,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,+CAA+C;QAC/C,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,KAAK,MAAM,CAAA;KAC7B,CAAA;CACD;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC/B,mDAAmD;IACnD,OAAO,EAAE,OAAO,CAAA;IAChB,gCAAgC;IAChC,OAAO,EAAE,GAAG,CAAA;IACZ,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAA;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IAClC,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,oCAAoC;IACpC,KAAK,EAAE,MAAM,EAAE,CAAA;CACf;AAGD,MAAM,WAAW,WAAW;IAC3B,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,UAAU,CAAC,EAAE,GAAG,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;CAClB"}
|
|
File without changes
|