@qelos/integrator-nuxt 4.0.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/LICENSE +20 -0
- package/README.md +185 -0
- package/dist/event-handler.d.ts +32 -0
- package/dist/event-handler.js +33 -0
- package/dist/event-handler.js.map +1 -0
- package/dist/middleware.d.ts +23 -0
- package/dist/middleware.js +96 -0
- package/dist/middleware.js.map +1 -0
- package/dist/module.d.ts +15 -0
- package/dist/module.js +49 -0
- package/dist/module.js.map +1 -0
- package/dist/runtime/composables/useQelos.d.ts +30 -0
- package/dist/runtime/composables/useQelos.js +38 -0
- package/dist/runtime/composables/useQelos.js.map +1 -0
- package/dist/runtime/server-handler.d.ts +2 -0
- package/dist/runtime/server-handler.js +10 -0
- package/dist/runtime/server-handler.js.map +1 -0
- package/dist/sdk-factory.d.ts +15 -0
- package/dist/sdk-factory.js +84 -0
- package/dist/sdk-factory.js.map +1 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +75 -0
- package/src/event-handler.ts +66 -0
- package/src/middleware.ts +146 -0
- package/src/module.ts +84 -0
- package/src/runtime/composables/useQelos.ts +60 -0
- package/src/runtime/server-handler.ts +12 -0
- package/src/sdk-factory.ts +111 -0
- package/src/types.ts +91 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import QelosSDK from '@qelos/sdk';
|
|
2
|
+
const NO_AUTH_URLS = new Set([
|
|
3
|
+
'/api/token/refresh',
|
|
4
|
+
'/api/cookie/refresh',
|
|
5
|
+
'/api/signin',
|
|
6
|
+
'/api/signup',
|
|
7
|
+
]);
|
|
8
|
+
export function createRequestSdk({ config, tokens, event, onTokenRefresh }) {
|
|
9
|
+
let sdk;
|
|
10
|
+
let refreshInFlight = null;
|
|
11
|
+
const baseOptions = config.sdkOptions || {};
|
|
12
|
+
async function performRefresh() {
|
|
13
|
+
if (!tokens.refreshToken && !tokens.accessToken) {
|
|
14
|
+
throw new Error('no refresh token available');
|
|
15
|
+
}
|
|
16
|
+
const previous = {
|
|
17
|
+
accessToken: tokens.accessToken,
|
|
18
|
+
refreshToken: tokens.refreshToken,
|
|
19
|
+
};
|
|
20
|
+
let refreshed;
|
|
21
|
+
if (tokens.refreshToken) {
|
|
22
|
+
const result = await sdk.authentication.refreshToken(tokens.refreshToken);
|
|
23
|
+
refreshed = {
|
|
24
|
+
accessToken: result.payload.token,
|
|
25
|
+
refreshToken: result.payload.refreshToken,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const result = await sdk.authentication.refreshCookieToken(tokens.accessToken);
|
|
30
|
+
refreshed = {
|
|
31
|
+
accessToken: result.payload.cookieToken,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
tokens.accessToken = refreshed.accessToken;
|
|
35
|
+
tokens.refreshToken = refreshed.refreshToken;
|
|
36
|
+
if (onTokenRefresh) {
|
|
37
|
+
await onTokenRefresh({ event, oldTokens: previous, newTokens: refreshed, sdk });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function ensureRefresh() {
|
|
41
|
+
if (!refreshInFlight) {
|
|
42
|
+
refreshInFlight = performRefresh().finally(() => {
|
|
43
|
+
refreshInFlight = null;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return refreshInFlight;
|
|
47
|
+
}
|
|
48
|
+
const options = {
|
|
49
|
+
appUrl: config.appUrl,
|
|
50
|
+
fetch: globalThis.fetch,
|
|
51
|
+
forceRefresh: !config.apiToken,
|
|
52
|
+
...baseOptions,
|
|
53
|
+
};
|
|
54
|
+
if (config.apiToken) {
|
|
55
|
+
options.apiToken = config.apiToken;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
if (tokens.accessToken) {
|
|
59
|
+
options.accessToken = tokens.accessToken;
|
|
60
|
+
}
|
|
61
|
+
if (tokens.refreshToken) {
|
|
62
|
+
options.refreshToken = tokens.refreshToken;
|
|
63
|
+
}
|
|
64
|
+
options.extraHeaders = async (relativeUrl, forceRefresh) => {
|
|
65
|
+
if (NO_AUTH_URLS.has(relativeUrl)) {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
if (forceRefresh && tokens.refreshToken) {
|
|
69
|
+
await ensureRefresh();
|
|
70
|
+
}
|
|
71
|
+
const token = sdk?.authentication?.accessToken || tokens.accessToken;
|
|
72
|
+
if (token) {
|
|
73
|
+
return { authorization: 'Bearer ' + token };
|
|
74
|
+
}
|
|
75
|
+
return {};
|
|
76
|
+
};
|
|
77
|
+
options.onFailedRefreshToken = async () => {
|
|
78
|
+
await ensureRefresh();
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
sdk = new QelosSDK(options);
|
|
82
|
+
return sdk;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=sdk-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk-factory.js","sourceRoot":"","sources":["../src/sdk-factory.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,YAAY,CAAC;AAsBlC,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;IAC3B,oBAAoB;IACpB,qBAAqB;IACrB,aAAa;IACb,aAAa;CACd,CAAC,CAAC;AAEH,MAAM,UAAU,gBAAgB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,EAAmB;IACzF,IAAI,GAAa,CAAC;IAClB,IAAI,eAAe,GAAyB,IAAI,CAAC;IAEjD,MAAM,WAAW,GAA6B,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC;IAEtE,KAAK,UAAU,cAAc;QAC3B,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YAChD,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAChD,CAAC;QACD,MAAM,QAAQ,GAAmB;YAC/B,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,YAAY,EAAE,MAAM,CAAC,YAAY;SAClC,CAAC;QACF,IAAI,SAAyB,CAAC;QAC9B,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;YACxB,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,cAAc,CAAC,YAAY,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;YAC1E,SAAS,GAAG;gBACV,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK;gBACjC,YAAY,EAAE,MAAM,CAAC,OAAO,CAAC,YAAY;aAC1C,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,cAAc,CAAC,kBAAkB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YAC/E,SAAS,GAAG;gBACV,WAAW,EAAE,MAAM,CAAC,OAAO,CAAC,WAAW;aACxC,CAAC;QACJ,CAAC;QACD,MAAM,CAAC,WAAW,GAAG,SAAS,CAAC,WAAW,CAAC;QAC3C,MAAM,CAAC,YAAY,GAAG,SAAS,CAAC,YAAY,CAAC;QAC7C,IAAI,cAAc,EAAE,CAAC;YACnB,MAAM,cAAc,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;IAED,SAAS,aAAa;QACpB,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,eAAe,GAAG,cAAc,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;gBAC9C,eAAe,GAAG,IAAI,CAAC;YACzB,CAAC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,eAAe,CAAC;IACzB,CAAC;IAED,MAAM,OAAO,GAAoB;QAC/B,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK,EAAG,UAAU,CAAC,KAA8B;QACjD,YAAY,EAAE,CAAC,MAAM,CAAC,QAAQ;QAC9B,GAAG,WAAW;KACf,CAAC;IAEF,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,OAAO,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IACrC,CAAC;SAAM,CAAC;QACN,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;YACvB,OAAO,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;QAC3C,CAAC;QACD,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;YACxB,OAAO,CAAC,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC;QAC7C,CAAC;QAED,OAAO,CAAC,YAAY,GAAG,KAAK,EAAE,WAAmB,EAAE,YAAsB,EAAE,EAAE;YAC3E,IAAI,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;gBAClC,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,IAAI,YAAY,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;gBACxC,MAAM,aAAa,EAAE,CAAC;YACxB,CAAC;YACD,MAAM,KAAK,GAAG,GAAG,EAAE,cAAc,EAAE,WAAW,IAAI,MAAM,CAAC,WAAW,CAAC;YACrE,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,EAAE,aAAa,EAAE,SAAS,GAAG,KAAK,EAAE,CAAC;YAC9C,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC;QAEF,OAAO,CAAC,oBAAoB,GAAG,KAAK,IAAI,EAAE;YACxC,MAAM,aAAa,EAAE,CAAC;QACxB,CAAC,CAAC;IACJ,CAAC;IAED,GAAG,GAAG,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC5B,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type QelosSDK from '@qelos/sdk';
|
|
2
|
+
import type { IUser } from '@qelos/sdk/dist/authentication';
|
|
3
|
+
import type { IWorkspace } from '@qelos/sdk/workspaces';
|
|
4
|
+
import type { QelosSDKOptions } from '@qelos/sdk/types';
|
|
5
|
+
export interface QelosTokenPair {
|
|
6
|
+
accessToken?: string;
|
|
7
|
+
refreshToken?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface QelosNuxtRuntimeConfig {
|
|
10
|
+
/**
|
|
11
|
+
* Base URL of the Qelos backend (e.g. https://yourdomain.com).
|
|
12
|
+
*/
|
|
13
|
+
appUrl: string;
|
|
14
|
+
/**
|
|
15
|
+
* Static API token used for service-to-service calls. When provided, no
|
|
16
|
+
* cookie/refresh-token handling is performed.
|
|
17
|
+
*/
|
|
18
|
+
apiToken?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Cookie name carrying the Qelos access token. Defaults to `q_access_token`.
|
|
21
|
+
*/
|
|
22
|
+
accessTokenCookie?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Cookie name carrying the Qelos refresh token. Defaults to `q_refresh_token`.
|
|
25
|
+
*/
|
|
26
|
+
refreshTokenCookie?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Path to mount the server middleware on. Defaults to `''` (every request).
|
|
29
|
+
*/
|
|
30
|
+
middlewarePath?: string;
|
|
31
|
+
/**
|
|
32
|
+
* If true, the request is rejected (401) when the user cannot be resolved.
|
|
33
|
+
* Defaults to `false` — anonymous requests pass through with `event.context.qelos.user = null`.
|
|
34
|
+
*/
|
|
35
|
+
requireAuth?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Skip the middleware entirely for requests matching any of these path
|
|
38
|
+
* prefixes. Useful for `/api/_auth`, `/health`, etc.
|
|
39
|
+
*/
|
|
40
|
+
skipPaths?: string[];
|
|
41
|
+
/**
|
|
42
|
+
* Optional extra options merged into the per-request SDK instance.
|
|
43
|
+
*/
|
|
44
|
+
sdkOptions?: Partial<QelosSDKOptions>;
|
|
45
|
+
}
|
|
46
|
+
export interface ResolvedTokens {
|
|
47
|
+
accessToken: string;
|
|
48
|
+
refreshToken?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface TokenRefreshContext {
|
|
51
|
+
event: import('h3').H3Event;
|
|
52
|
+
oldTokens: QelosTokenPair;
|
|
53
|
+
newTokens: ResolvedTokens;
|
|
54
|
+
sdk: QelosSDK;
|
|
55
|
+
}
|
|
56
|
+
export type TokenRefreshHook = (ctx: TokenRefreshContext) => void | Promise<void>;
|
|
57
|
+
export interface QelosRequestContext {
|
|
58
|
+
/**
|
|
59
|
+
* The authenticated user, or `null` when anonymous.
|
|
60
|
+
*/
|
|
61
|
+
user: IUser | null;
|
|
62
|
+
/**
|
|
63
|
+
* The active workspace for the request, or `null` when none is active /
|
|
64
|
+
* the user is anonymous.
|
|
65
|
+
*/
|
|
66
|
+
workspace: IWorkspace | null;
|
|
67
|
+
/**
|
|
68
|
+
* The full list of workspaces the user has access to.
|
|
69
|
+
*/
|
|
70
|
+
workspaces: IWorkspace[];
|
|
71
|
+
/**
|
|
72
|
+
* SDK instance bound to the current request's tokens.
|
|
73
|
+
*/
|
|
74
|
+
sdk: QelosSDK;
|
|
75
|
+
/**
|
|
76
|
+
* Tokens read from the request. Mutated when a refresh occurs.
|
|
77
|
+
*/
|
|
78
|
+
tokens: QelosTokenPair;
|
|
79
|
+
}
|
|
80
|
+
declare module 'h3' {
|
|
81
|
+
interface H3EventContext {
|
|
82
|
+
qelos?: QelosRequestContext;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qelos/integrator-nuxt",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Nuxt integrator for Qelos: server middleware/module that identifies the user and workspace from the Qelos SDK before your handlers run.",
|
|
6
|
+
"main": "./dist/module.js",
|
|
7
|
+
"types": "./dist/module.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/module.d.ts",
|
|
11
|
+
"import": "./dist/module.js",
|
|
12
|
+
"require": "./dist/module.js",
|
|
13
|
+
"default": "./dist/module.js"
|
|
14
|
+
},
|
|
15
|
+
"./middleware": {
|
|
16
|
+
"types": "./dist/middleware.d.ts",
|
|
17
|
+
"import": "./dist/middleware.js",
|
|
18
|
+
"require": "./dist/middleware.js",
|
|
19
|
+
"default": "./dist/middleware.js"
|
|
20
|
+
},
|
|
21
|
+
"./event-handler": {
|
|
22
|
+
"types": "./dist/event-handler.d.ts",
|
|
23
|
+
"import": "./dist/event-handler.js",
|
|
24
|
+
"require": "./dist/event-handler.js",
|
|
25
|
+
"default": "./dist/event-handler.js"
|
|
26
|
+
},
|
|
27
|
+
"./types": {
|
|
28
|
+
"types": "./dist/types.d.ts",
|
|
29
|
+
"import": "./dist/types.js",
|
|
30
|
+
"require": "./dist/types.js",
|
|
31
|
+
"default": "./dist/types.js"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src"
|
|
38
|
+
],
|
|
39
|
+
"keywords": [
|
|
40
|
+
"qelos",
|
|
41
|
+
"nuxt",
|
|
42
|
+
"nuxt-module",
|
|
43
|
+
"middleware",
|
|
44
|
+
"auth",
|
|
45
|
+
"sdk"
|
|
46
|
+
],
|
|
47
|
+
"author": "David Meir-Levy <davidmeirlevy@gmail.com>",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"h3": "^1.0.0",
|
|
54
|
+
"nuxt": "^3.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"nuxt": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@qelos/sdk": "4.0.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@nuxt/kit": "^3.13.0",
|
|
66
|
+
"@nuxt/schema": "^3.13.0",
|
|
67
|
+
"h3": "^1.13.0",
|
|
68
|
+
"typescript": "^5.4.5"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"type-check": "tsc --noEmit",
|
|
72
|
+
"build": "npm run type-check && tsc",
|
|
73
|
+
"pre-build": "pnpm run type-check && tsc"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineEventHandler,
|
|
3
|
+
createError,
|
|
4
|
+
type EventHandlerRequest,
|
|
5
|
+
type EventHandlerResponse,
|
|
6
|
+
type H3Event,
|
|
7
|
+
} from 'h3';
|
|
8
|
+
import type { QelosRequestContext } from './types';
|
|
9
|
+
|
|
10
|
+
export interface QelosEventContext<T extends EventHandlerRequest = EventHandlerRequest> {
|
|
11
|
+
event: H3Event<T>;
|
|
12
|
+
qelos: QelosRequestContext;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DefineQelosEventHandlerOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Reject the request with `401 Unauthorized` when no authenticated user is
|
|
18
|
+
* resolved on `event.context.qelos`. Defaults to `false`, mirroring the
|
|
19
|
+
* module-level `qelos.requireAuth` behavior.
|
|
20
|
+
*/
|
|
21
|
+
requireAuth?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type QelosEventHandler<
|
|
25
|
+
Request extends EventHandlerRequest = EventHandlerRequest,
|
|
26
|
+
Response extends EventHandlerResponse = EventHandlerResponse,
|
|
27
|
+
> = (ctx: QelosEventContext<Request>) => Response;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Wrap a Nitro/H3 event handler so that the Qelos request context (user,
|
|
31
|
+
* workspace, workspaces, sdk, tokens) is available as a second argument and
|
|
32
|
+
* already typed. Requires the `@qelos/integrator-nuxt` module to be enabled
|
|
33
|
+
* (or `createQelosMiddleware` to be registered manually) so that
|
|
34
|
+
* `event.context.qelos` is populated.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* // server/api/products.ts
|
|
38
|
+
* import { defineQelosEventHandler } from '@qelos/integrator-nuxt';
|
|
39
|
+
*
|
|
40
|
+
* export default defineQelosEventHandler(async ({ qelos }) => {
|
|
41
|
+
* return qelos.sdk.entities('products').getList();
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function defineQelosEventHandler<
|
|
46
|
+
Request extends EventHandlerRequest = EventHandlerRequest,
|
|
47
|
+
Response extends EventHandlerResponse = EventHandlerResponse,
|
|
48
|
+
>(
|
|
49
|
+
handler: QelosEventHandler<Request, Response>,
|
|
50
|
+
options: DefineQelosEventHandlerOptions = {},
|
|
51
|
+
) {
|
|
52
|
+
return defineEventHandler<Request>(async (event) => {
|
|
53
|
+
const qelos = event.context.qelos;
|
|
54
|
+
if (!qelos) {
|
|
55
|
+
throw createError({
|
|
56
|
+
statusCode: 500,
|
|
57
|
+
statusMessage:
|
|
58
|
+
'[@qelos/integrator-nuxt] event.context.qelos is missing. Add the module to nuxt.config or register createQelosMiddleware manually.',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (options.requireAuth && !qelos.user) {
|
|
62
|
+
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
|
|
63
|
+
}
|
|
64
|
+
return handler({ event, qelos }) as Response;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineEventHandler,
|
|
3
|
+
getCookie,
|
|
4
|
+
getRequestHeader,
|
|
5
|
+
setCookie,
|
|
6
|
+
createError,
|
|
7
|
+
type H3Event,
|
|
8
|
+
} from 'h3';
|
|
9
|
+
import type { IUser } from '@qelos/sdk/dist/authentication';
|
|
10
|
+
import type { IWorkspace } from '@qelos/sdk/workspaces';
|
|
11
|
+
import { createRequestSdk } from './sdk-factory';
|
|
12
|
+
import type {
|
|
13
|
+
QelosNuxtRuntimeConfig,
|
|
14
|
+
QelosRequestContext,
|
|
15
|
+
QelosTokenPair,
|
|
16
|
+
TokenRefreshHook,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
const DEFAULT_ACCESS_COOKIE = 'q_access_token';
|
|
20
|
+
const DEFAULT_REFRESH_COOKIE = 'q_refresh_token';
|
|
21
|
+
|
|
22
|
+
export interface CreateMiddlewareOptions {
|
|
23
|
+
config: QelosNuxtRuntimeConfig;
|
|
24
|
+
/**
|
|
25
|
+
* Hook invoked after a successful token refresh. The default implementation
|
|
26
|
+
* writes the new tokens back to the response cookies.
|
|
27
|
+
*/
|
|
28
|
+
onTokenRefresh?: TokenRefreshHook;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the active workspace for a request. Defaults to picking the first
|
|
31
|
+
* workspace returned from `sdk.workspaces.getList()`.
|
|
32
|
+
*/
|
|
33
|
+
resolveWorkspace?: (params: {
|
|
34
|
+
event: H3Event;
|
|
35
|
+
user: IUser;
|
|
36
|
+
workspaces: IWorkspace[];
|
|
37
|
+
}) => IWorkspace | null | Promise<IWorkspace | null>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readTokens(event: H3Event, config: QelosNuxtRuntimeConfig): QelosTokenPair {
|
|
41
|
+
const accessCookie = config.accessTokenCookie || DEFAULT_ACCESS_COOKIE;
|
|
42
|
+
const refreshCookie = config.refreshTokenCookie || DEFAULT_REFRESH_COOKIE;
|
|
43
|
+
|
|
44
|
+
const cookieAccess = getCookie(event, accessCookie);
|
|
45
|
+
const cookieRefresh = getCookie(event, refreshCookie);
|
|
46
|
+
|
|
47
|
+
const authHeader = getRequestHeader(event, 'authorization');
|
|
48
|
+
const headerAccess = authHeader && authHeader.toLowerCase().startsWith('bearer ')
|
|
49
|
+
? authHeader.slice(7).trim()
|
|
50
|
+
: undefined;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
accessToken: headerAccess || cookieAccess || undefined,
|
|
54
|
+
refreshToken: cookieRefresh || undefined,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeTokensToCookies(
|
|
59
|
+
event: H3Event,
|
|
60
|
+
config: QelosNuxtRuntimeConfig,
|
|
61
|
+
tokens: { accessToken: string; refreshToken?: string },
|
|
62
|
+
) {
|
|
63
|
+
const accessCookie = config.accessTokenCookie || DEFAULT_ACCESS_COOKIE;
|
|
64
|
+
const refreshCookie = config.refreshTokenCookie || DEFAULT_REFRESH_COOKIE;
|
|
65
|
+
const cookieOptions = {
|
|
66
|
+
httpOnly: true,
|
|
67
|
+
secure: true,
|
|
68
|
+
sameSite: 'lax' as const,
|
|
69
|
+
path: '/',
|
|
70
|
+
};
|
|
71
|
+
setCookie(event, accessCookie, tokens.accessToken, cookieOptions);
|
|
72
|
+
if (tokens.refreshToken) {
|
|
73
|
+
setCookie(event, refreshCookie, tokens.refreshToken, cookieOptions);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function shouldSkip(event: H3Event, config: QelosNuxtRuntimeConfig): boolean {
|
|
78
|
+
if (!config.skipPaths?.length) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const path = event.path || event.node?.req?.url || '';
|
|
82
|
+
return config.skipPaths.some((prefix) => path.startsWith(prefix));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createQelosMiddleware(options: CreateMiddlewareOptions) {
|
|
86
|
+
const { config, resolveWorkspace } = options;
|
|
87
|
+
const onTokenRefresh: TokenRefreshHook = options.onTokenRefresh
|
|
88
|
+
|| (async ({ event, newTokens }) => {
|
|
89
|
+
writeTokensToCookies(event, config, newTokens);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return defineEventHandler(async (event) => {
|
|
93
|
+
if (shouldSkip(event, config)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const tokens = readTokens(event, config);
|
|
98
|
+
const sdk = createRequestSdk({ config, tokens, event, onTokenRefresh });
|
|
99
|
+
|
|
100
|
+
const ctx: QelosRequestContext = {
|
|
101
|
+
user: null,
|
|
102
|
+
workspace: null,
|
|
103
|
+
workspaces: [],
|
|
104
|
+
sdk,
|
|
105
|
+
tokens,
|
|
106
|
+
};
|
|
107
|
+
event.context.qelos = ctx;
|
|
108
|
+
|
|
109
|
+
const hasAuthMaterial = Boolean(config.apiToken || tokens.accessToken || tokens.refreshToken);
|
|
110
|
+
if (!hasAuthMaterial) {
|
|
111
|
+
if (config.requireAuth) {
|
|
112
|
+
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
ctx.user = await sdk.authentication.getLoggedInUser();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (config.requireAuth) {
|
|
121
|
+
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
ctx.workspaces = await sdk.workspaces.getList();
|
|
128
|
+
} catch {
|
|
129
|
+
ctx.workspaces = [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (ctx.user && ctx.workspaces.length) {
|
|
133
|
+
if (resolveWorkspace) {
|
|
134
|
+
ctx.workspace = await resolveWorkspace({
|
|
135
|
+
event,
|
|
136
|
+
user: ctx.user,
|
|
137
|
+
workspaces: ctx.workspaces,
|
|
138
|
+
}) || null;
|
|
139
|
+
} else {
|
|
140
|
+
ctx.workspace = ctx.workspaces[0] || null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type QelosMiddleware = ReturnType<typeof createQelosMiddleware>;
|
package/src/module.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineNuxtModule,
|
|
3
|
+
addServerHandler,
|
|
4
|
+
addImportsDir,
|
|
5
|
+
createResolver,
|
|
6
|
+
addTypeTemplate,
|
|
7
|
+
} from '@nuxt/kit';
|
|
8
|
+
import type { QelosNuxtRuntimeConfig } from './types';
|
|
9
|
+
|
|
10
|
+
export interface QelosNuxtModuleOptions extends QelosNuxtRuntimeConfig {
|
|
11
|
+
/**
|
|
12
|
+
* Disable registration of the default server middleware. Use this if you
|
|
13
|
+
* want to call `createQelosMiddleware` yourself from your own server route.
|
|
14
|
+
*/
|
|
15
|
+
disableMiddleware?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default defineNuxtModule<QelosNuxtModuleOptions>({
|
|
19
|
+
meta: {
|
|
20
|
+
name: '@qelos/integrator-nuxt',
|
|
21
|
+
configKey: 'qelos',
|
|
22
|
+
compatibility: {
|
|
23
|
+
nuxt: '^3.0.0',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaults: {
|
|
27
|
+
appUrl: '',
|
|
28
|
+
},
|
|
29
|
+
setup(options, nuxt) {
|
|
30
|
+
if (!options.appUrl) {
|
|
31
|
+
throw new Error('[@qelos/integrator-nuxt] `qelos.appUrl` must be set in nuxt.config');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { resolve } = createResolver(import.meta.url);
|
|
35
|
+
|
|
36
|
+
const { disableMiddleware, ...runtimeConfig } = options;
|
|
37
|
+
|
|
38
|
+
nuxt.options.runtimeConfig.qelos = {
|
|
39
|
+
...(nuxt.options.runtimeConfig.qelos as Record<string, unknown> | undefined),
|
|
40
|
+
...runtimeConfig,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (!disableMiddleware) {
|
|
44
|
+
addServerHandler({
|
|
45
|
+
handler: resolve('./runtime/server-handler'),
|
|
46
|
+
middleware: true,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
addImportsDir(resolve('./runtime/composables'));
|
|
51
|
+
|
|
52
|
+
addTypeTemplate({
|
|
53
|
+
filename: 'types/qelos-nuxt.d.ts',
|
|
54
|
+
getContents: () => `
|
|
55
|
+
import type { QelosRequestContext } from '@qelos/integrator-nuxt/types';
|
|
56
|
+
|
|
57
|
+
declare module 'h3' {
|
|
58
|
+
interface H3EventContext {
|
|
59
|
+
qelos?: QelosRequestContext;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export {};
|
|
64
|
+
`,
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export { createQelosMiddleware } from './middleware';
|
|
70
|
+
export { createRequestSdk } from './sdk-factory';
|
|
71
|
+
export { defineQelosEventHandler } from './event-handler';
|
|
72
|
+
export type {
|
|
73
|
+
DefineQelosEventHandlerOptions,
|
|
74
|
+
QelosEventContext,
|
|
75
|
+
QelosEventHandler,
|
|
76
|
+
} from './event-handler';
|
|
77
|
+
export type {
|
|
78
|
+
QelosNuxtRuntimeConfig,
|
|
79
|
+
QelosRequestContext,
|
|
80
|
+
QelosTokenPair,
|
|
81
|
+
ResolvedTokens,
|
|
82
|
+
TokenRefreshContext,
|
|
83
|
+
TokenRefreshHook,
|
|
84
|
+
} from './types';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Auto-imported as `useQelos` by the @qelos/integrator-nuxt module. This file
|
|
2
|
+
// is bundled by Nuxt/Nitro in the consuming app, so `#imports` resolves at
|
|
3
|
+
// the consumer's build time.
|
|
4
|
+
// @ts-ignore - resolved by Nuxt
|
|
5
|
+
import { useState, useRequestEvent } from '#imports';
|
|
6
|
+
import type { IUser } from '@qelos/sdk/dist/authentication';
|
|
7
|
+
import type { IWorkspace } from '@qelos/sdk/workspaces';
|
|
8
|
+
import type { QelosRequestContext } from '../../types';
|
|
9
|
+
|
|
10
|
+
// Minimal local Ref type — structurally matches `vue`'s Ref for the consumer.
|
|
11
|
+
// We don't take a `vue` dep at this package's `tsc` build time.
|
|
12
|
+
interface Ref<T> {
|
|
13
|
+
value: T;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseQelosReturn {
|
|
17
|
+
/** The authenticated user (or `null` when anonymous). Hydrated from SSR. */
|
|
18
|
+
user: Ref<IUser | null>;
|
|
19
|
+
/** The active workspace for the user (or `null`). Hydrated from SSR. */
|
|
20
|
+
workspace: Ref<IWorkspace | null>;
|
|
21
|
+
/** All workspaces the user has access to. Hydrated from SSR. */
|
|
22
|
+
workspaces: Ref<IWorkspace[]>;
|
|
23
|
+
/** Convenience: `true` when `user` is set. */
|
|
24
|
+
isAuthenticated: Ref<boolean>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getServerContext(): QelosRequestContext | null {
|
|
28
|
+
if (typeof window !== 'undefined') return null;
|
|
29
|
+
try {
|
|
30
|
+
const event = useRequestEvent();
|
|
31
|
+
return (event && event.context && event.context.qelos) || null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read the current Qelos identity (user / workspace / workspaces) on the
|
|
39
|
+
* client. Values are seeded on the server by the Qelos server middleware and
|
|
40
|
+
* sent to the client via Nuxt's payload. SDK calls from the browser should go
|
|
41
|
+
* through your own server routes (use `defineQelosEventHandler`) rather than
|
|
42
|
+
* directly, so refresh-token rotation stays server-side.
|
|
43
|
+
*
|
|
44
|
+
* ```vue
|
|
45
|
+
* <script setup lang="ts">
|
|
46
|
+
* const { user, workspace, isAuthenticated } = useQelos();
|
|
47
|
+
* </script>
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function useQelos(): UseQelosReturn {
|
|
51
|
+
const ctx = getServerContext();
|
|
52
|
+
const user = useState<IUser | null>('qelos:user', () => ctx?.user ?? null);
|
|
53
|
+
const workspace = useState<IWorkspace | null>('qelos:workspace', () => ctx?.workspace ?? null);
|
|
54
|
+
const workspaces = useState<IWorkspace[]>('qelos:workspaces', () => ctx?.workspaces ?? []);
|
|
55
|
+
const isAuthenticated = useState<boolean>(
|
|
56
|
+
'qelos:isAuthenticated',
|
|
57
|
+
() => Boolean(ctx?.user),
|
|
58
|
+
);
|
|
59
|
+
return { user, workspace, workspaces, isAuthenticated };
|
|
60
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// This file is registered as a Nuxt server middleware by `module.ts`.
|
|
2
|
+
// It is bundled by Nitro, so `#imports` resolves at the consuming-app build
|
|
3
|
+
// time (not at this package's `tsc` build).
|
|
4
|
+
// @ts-ignore - resolved by Nitro
|
|
5
|
+
import { useRuntimeConfig } from '#imports';
|
|
6
|
+
import { createQelosMiddleware } from '../middleware';
|
|
7
|
+
import type { QelosNuxtRuntimeConfig } from '../types';
|
|
8
|
+
|
|
9
|
+
const runtimeConfig = useRuntimeConfig();
|
|
10
|
+
const config = (runtimeConfig.qelos || {}) as QelosNuxtRuntimeConfig;
|
|
11
|
+
|
|
12
|
+
export default createQelosMiddleware({ config });
|