canopycms-auth-dev 0.0.0 → 0.0.2
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/dist/dev-plugin.d.ts +3 -0
- package/dist/dev-plugin.js +11 -0
- package/dist/dev-plugin.js.map +1 -1
- package/dist/jwt-verifier.d.ts +3 -0
- package/dist/jwt-verifier.js +3 -0
- package/dist/jwt-verifier.js.map +1 -1
- package/package.json +3 -4
- package/src/UserSwitcherButton.tsx +0 -46
- package/src/UserSwitcherModal.tsx +0 -63
- package/src/cache-writer.ts +0 -61
- package/src/client.ts +0 -34
- package/src/cookie-utils.ts +0 -44
- package/src/dev-plugin.test.ts +0 -470
- package/src/dev-plugin.ts +0 -241
- package/src/index.ts +0 -11
- package/src/jwt-verifier.ts +0 -46
package/dist/dev-plugin.d.ts
CHANGED
|
@@ -49,6 +49,9 @@ export declare class DevAuthPlugin implements AuthPlugin {
|
|
|
49
49
|
private groups;
|
|
50
50
|
private defaultUserId;
|
|
51
51
|
constructor(config?: DevAuthConfig);
|
|
52
|
+
verifyTokenOnly(context: unknown): Promise<{
|
|
53
|
+
userId: string;
|
|
54
|
+
} | null>;
|
|
52
55
|
authenticate(context: unknown): Promise<AuthenticationResult>;
|
|
53
56
|
/**
|
|
54
57
|
* Map test-app user keys to dev user IDs for backward compatibility
|
package/dist/dev-plugin.js
CHANGED
|
@@ -50,6 +50,17 @@ export class DevAuthPlugin {
|
|
|
50
50
|
this.groups = config.groups ?? DEFAULT_GROUPS;
|
|
51
51
|
this.defaultUserId = config.defaultUserId ?? 'dev_user1_2nK8mP4xL9';
|
|
52
52
|
}
|
|
53
|
+
async verifyTokenOnly(context) {
|
|
54
|
+
const headers = extractHeaders(context);
|
|
55
|
+
if (!headers)
|
|
56
|
+
return null;
|
|
57
|
+
let userId = headers.get('X-Test-User');
|
|
58
|
+
if (!userId)
|
|
59
|
+
userId = headers.get('x-dev-user-id') ?? getDevUserCookieFromHeaders(headers);
|
|
60
|
+
if (!userId)
|
|
61
|
+
userId = this.defaultUserId;
|
|
62
|
+
return { userId: this.mapTestUserKey(userId) };
|
|
63
|
+
}
|
|
53
64
|
async authenticate(context) {
|
|
54
65
|
// 1. Extract headers using extractHeaders() helper
|
|
55
66
|
const headers = extractHeaders(context);
|
package/dist/dev-plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-plugin.js","sourceRoot":"","sources":["../src/dev-plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAE/C,OAAO,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAA;AAqB5D,MAAM,CAAC,MAAM,iBAAiB,GAAiB,sBAAsB,CAAA;AA0BrE,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC;QACE,MAAM,EAAE,sBAAsB;QAC9B,IAAI,EAAE,UAAU;QAChB,KAAK,EAAE,qBAAqB;QAC5B,cAAc,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC;KACrC;IACD;QACE,MAAM,EAAE,sBAAsB;QAC9B,IAAI,EAAE,UAAU;QAChB,KAAK,EAAE,qBAAqB;QAC5B,cAAc,EAAE,CAAC,QAAQ,CAAC;KAC3B;IACD;QACE,MAAM,EAAE,sBAAsB;QAC9B,IAAI,EAAE,YAAY;QAClB,KAAK,EAAE,qBAAqB;QAC5B,cAAc,EAAE,CAAC,QAAQ,CAAC;KAC3B;IACD;QACE,MAAM,EAAE,yBAAyB;QACjC,IAAI,EAAE,cAAc;QACpB,KAAK,EAAE,yBAAyB;QAChC,cAAc,EAAE,CAAC,QAAQ,CAAC;QAC1B,gFAAgF;KACjF;IACD;QACE,MAAM,EAAE,iBAAiB;QACzB,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,sBAAsB;QAC7B,cAAc,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC;QAC9C,+FAA+F;KAChG;CACF,CAAA;AAED,MAAM,CAAC,MAAM,cAAc,GAAe;IACxC,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE;IACvD,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE;IACvD,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE;CACxD,CAAA;AAED;;;GAGG;AACH,MAAM,OAAO,aAAa;IAKxB,YAAY,SAAwB,EAAE;QACpC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,aAAa,CAAA;QAC1C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,cAAc,CAAA;QAC7C,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC,aAAa,IAAI,sBAAsB,CAAA;IACrE,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,OAAgB;QACjC,mDAAmD;QACnD,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;QACvC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAA;QACrD,CAAC;QAED,iEAAiE;QACjE,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;QAEvC,6EAA6E;QAC7E,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,2BAA2B,CAAC,OAAO,CAAC,CAAA;QAC/E,CAAC;QAED,+BAA+B;QAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,IAAI,CAAC,aAAa,CAAA;QAC7B,CAAC;QAED,+DAA+D;QAC/D,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAA;QAEhD,yBAAyB;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,YAAY,CAAC,CAAA;QAC9D,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,uBAAuB,MAAM,EAAE,EAAE,CAAA;QACnE,CAAC;QAED,qDAAqD;QACrD,OAAO;YACL,OAAO,EAAE,IAAI;YACb,IAAI,EAAE;gBACJ,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,cAAc,EAAE,IAAI,CAAC,cAAc;aACpC;SACF,CAAA;IACH,CAAC;IAED;;OAEG;IACK,cAAc,CAAC,GAAW;QAChC,MAAM,WAAW,GAAiC;YAChD,KAAK,EAAE,iBAAiB,EAAE,SAAS;YACnC,MAAM,EAAE,sBAAsB,EAAE,QAAQ;YACxC,MAAM,EAAE,sBAAsB,EAAE,QAAQ;YACxC,QAAQ,EAAE,yBAAyB,EAAE,YAAY;SAClD,CAAA;QACD,OAAO,WAAW,CAAC,GAAG,CAAC,IAAI,GAAG,CAAA;IAChC,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,KAAa,EAAE,KAAc;QAC7C,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAA;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAChC,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAC1F,CAAA;QAED,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;QACxC,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACzB,EAAE,EAAE,CAAC,CAAC,MAAM;YACZ,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,SAAS,EAAE,CAAC,CAAC,SAAS;SACvB,CAAC,CAAC,CAAA;IACL,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,MAAoB;QACxC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAA;QACxD,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAA;QAEtB,OAAO;YACL,EAAE,EAAE,IAAI,CAAC,MAAM;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAA;IACH,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,OAAsB;QAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,CAAA;QACvD,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAA;QAEvB,OAAO;YACL,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,WAAW,EAAE,KAAK,CAAC,WAAW;SAC/B,CAAA;IACH,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,KAAc;QAC7B,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAA;QAChE,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACxB,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,WAAW,EAAE,CAAC,CAAC,WAAW;SAC3B,CAAC,CAAC,CAAA;IACL,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,KAAa;QACtC,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAA;QACtC,OAAO,IAAI,CAAC,MAAM;aACf,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;aACxD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACX,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,IAAI,EAAE,CAAC,CAAC,IAAI;SACb,CAAC,CAAC,CAAA;IACP,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAsB;IACxD,MAAM,mBAAmB,GAAG,MAAM,EAAE,kBAAkB,IAAI,IAAI,CAAA;IAC9D,IAAI,mBAAmB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,CAAC;QACnE,MAAM,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,aAAa,CAAA;QAC5C,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,iBAAiB,CAAC,CAAA;QACnE,IAAI,SAAS,EAAE,CAAC;YACd,OAAO,CAAC,GAAG,CAAC,0BAA0B,GAAG,SAAS,CAAC,MAAM,CAAA;YACzD,OAAO,CAAC,IAAI,CACV,uCAAuC,SAAS,CAAC,IAAI,KAAK,SAAS,CAAC,MAAM,wBAAwB;gBAChG,uFAAuF,CAC1F,CAAA;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,aAAa,CAAC,MAAM,IAAI,EAAE,CAAC,CAAA;AACxC,CAAC"}
|
|
1
|
+
{"version":3,"file":"dev-plugin.js","sourceRoot":"","sources":["../src/dev-plugin.ts"],"names":[],"mappings":"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"}
|
package/dist/jwt-verifier.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ import type { TokenVerifier } from 'canopycms/auth';
|
|
|
6
6
|
*
|
|
7
7
|
* Used with CachingAuthPlugin in prod-sim mode to simulate the prod
|
|
8
8
|
* code path (token verification + cached metadata lookup) using dev users.
|
|
9
|
+
*
|
|
10
|
+
* @deprecated Use `DevAuthPlugin.verifyTokenOnly()` instead. The plugin's method is
|
|
11
|
+
* automatically wired into CachingAuthPlugin by `createNextCanopyContext()` in prod/prod-sim.
|
|
9
12
|
*/
|
|
10
13
|
export declare function createDevTokenVerifier(options?: {
|
|
11
14
|
defaultUserId?: string;
|
package/dist/jwt-verifier.js
CHANGED
|
@@ -18,6 +18,9 @@ const TEST_USER_MAP = {
|
|
|
18
18
|
*
|
|
19
19
|
* Used with CachingAuthPlugin in prod-sim mode to simulate the prod
|
|
20
20
|
* code path (token verification + cached metadata lookup) using dev users.
|
|
21
|
+
*
|
|
22
|
+
* @deprecated Use `DevAuthPlugin.verifyTokenOnly()` instead. The plugin's method is
|
|
23
|
+
* automatically wired into CachingAuthPlugin by `createNextCanopyContext()` in prod/prod-sim.
|
|
21
24
|
*/
|
|
22
25
|
export function createDevTokenVerifier(options) {
|
|
23
26
|
const defaultUserId = options?.defaultUserId ?? DEFAULT_USER_ID;
|
package/dist/jwt-verifier.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt-verifier.js","sourceRoot":"","sources":["../src/jwt-verifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAE/C,OAAO,EAAE,2BAA2B,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAEhD;;;GAGG;AACH,MAAM,aAAa,GAA2B;IAC5C,KAAK,EAAE,iBAAiB;IACxB,MAAM,EAAE,sBAAsB;IAC9B,MAAM,EAAE,sBAAsB;IAC9B,QAAQ,EAAE,yBAAyB;CACpC,CAAA;AAED
|
|
1
|
+
{"version":3,"file":"jwt-verifier.js","sourceRoot":"","sources":["../src/jwt-verifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAE/C,OAAO,EAAE,2BAA2B,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAEhD;;;GAGG;AACH,MAAM,aAAa,GAA2B;IAC5C,KAAK,EAAE,iBAAiB;IACxB,MAAM,EAAE,sBAAsB;IAC9B,MAAM,EAAE,sBAAsB;IAC9B,QAAQ,EAAE,yBAAyB;CACpC,CAAA;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAoC;IACzE,MAAM,aAAa,GAAG,OAAO,EAAE,aAAa,IAAI,eAAe,CAAA;IAE/D,OAAO,KAAK,EAAE,OAAgB,EAAE,EAAE;QAChC,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;QACvC,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAA;QAEzB,wDAAwD;QACxD,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;QACvC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,2BAA2B,CAAC,OAAO,CAAC,CAAA;QAC/E,CAAC;QACD,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,aAAa,CAAA;QACxB,CAAC;QAED,qCAAqC;QACrC,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,MAAM,CAAA;QAE9C,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAA;IAC3B,CAAC,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canopycms-auth-dev",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Development authentication provider for CanopyCMS",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
"./cache-writer": "./src/cache-writer.ts"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
22
|
-
"src"
|
|
21
|
+
"dist"
|
|
23
22
|
],
|
|
24
23
|
"publishConfig": {
|
|
25
24
|
"exports": {
|
|
@@ -46,7 +45,7 @@
|
|
|
46
45
|
"node": ">=18"
|
|
47
46
|
},
|
|
48
47
|
"peerDependencies": {
|
|
49
|
-
"canopycms": "
|
|
48
|
+
"canopycms": "^0.0.2",
|
|
50
49
|
"react": "^18.0.0 || ^19.0.0",
|
|
51
50
|
"@mantine/core": "^7.0.0",
|
|
52
51
|
"@mantine/hooks": "^7.0.0",
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect } from 'react'
|
|
4
|
-
import { ActionIcon, Avatar } from '@mantine/core'
|
|
5
|
-
import { UserSwitcherModal } from './UserSwitcherModal'
|
|
6
|
-
import { DEFAULT_USERS } from './dev-plugin'
|
|
7
|
-
import { getDevUserCookie, DEFAULT_USER_ID } from './cookie-utils'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* User switcher button component that shows current user avatar and opens modal
|
|
11
|
-
*/
|
|
12
|
-
export function UserSwitcherButton() {
|
|
13
|
-
const [opened, setOpened] = useState(false)
|
|
14
|
-
const [mounted, setMounted] = useState(false)
|
|
15
|
-
|
|
16
|
-
// Only read cookie after mount to avoid hydration mismatch
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
setMounted(true)
|
|
19
|
-
}, [])
|
|
20
|
-
|
|
21
|
-
// Read current user from cookie (only on client)
|
|
22
|
-
const currentUserId = mounted ? (getDevUserCookie() ?? DEFAULT_USER_ID) : DEFAULT_USER_ID
|
|
23
|
-
const currentUser = DEFAULT_USERS.find((u) => u.userId === currentUserId)
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<>
|
|
27
|
-
<ActionIcon
|
|
28
|
-
variant="subtle"
|
|
29
|
-
size="lg"
|
|
30
|
-
radius="md"
|
|
31
|
-
onClick={() => setOpened(true)}
|
|
32
|
-
aria-label="Switch user"
|
|
33
|
-
>
|
|
34
|
-
<Avatar size="sm" color="blue">
|
|
35
|
-
{currentUser?.name[0] ?? 'U'}
|
|
36
|
-
</Avatar>
|
|
37
|
-
</ActionIcon>
|
|
38
|
-
|
|
39
|
-
<UserSwitcherModal
|
|
40
|
-
opened={opened}
|
|
41
|
-
onClose={() => setOpened(false)}
|
|
42
|
-
currentUserId={currentUserId}
|
|
43
|
-
/>
|
|
44
|
-
</>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { Modal, Stack, Paper, Group, Avatar, Text, Badge } from '@mantine/core'
|
|
4
|
-
import { MdCheck } from 'react-icons/md'
|
|
5
|
-
import { DEFAULT_USERS } from './dev-plugin'
|
|
6
|
-
import { setDevUserCookie } from './cookie-utils'
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
opened: boolean
|
|
10
|
-
onClose: () => void
|
|
11
|
-
currentUserId: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* User switcher modal component that displays all available dev users
|
|
16
|
-
*/
|
|
17
|
-
export function UserSwitcherModal({ opened, onClose, currentUserId }: Props) {
|
|
18
|
-
const switchUser = (userId: string) => {
|
|
19
|
-
// Set cookie for 7 days
|
|
20
|
-
setDevUserCookie(userId)
|
|
21
|
-
// Reload to apply new user
|
|
22
|
-
window.location.reload()
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<Modal opened={opened} onClose={onClose} title="Switch Development User">
|
|
27
|
-
<Stack gap="sm">
|
|
28
|
-
{DEFAULT_USERS.map((user) => (
|
|
29
|
-
<Paper
|
|
30
|
-
key={user.userId}
|
|
31
|
-
p="md"
|
|
32
|
-
withBorder
|
|
33
|
-
style={{ cursor: 'pointer' }}
|
|
34
|
-
onClick={() => switchUser(user.userId)}
|
|
35
|
-
>
|
|
36
|
-
<Group justify="space-between" mb="xs">
|
|
37
|
-
<Group>
|
|
38
|
-
<Avatar color="blue">{user.name[0]}</Avatar>
|
|
39
|
-
<div>
|
|
40
|
-
<Text fw={500}>{user.name}</Text>
|
|
41
|
-
<Text size="sm" c="dimmed">
|
|
42
|
-
{user.email}
|
|
43
|
-
</Text>
|
|
44
|
-
</div>
|
|
45
|
-
</Group>
|
|
46
|
-
{user.userId === currentUserId && <MdCheck size={20} />}
|
|
47
|
-
</Group>
|
|
48
|
-
|
|
49
|
-
{user.externalGroups.length > 0 && (
|
|
50
|
-
<Group gap="xs">
|
|
51
|
-
{user.externalGroups.map((g) => (
|
|
52
|
-
<Badge key={g} variant="outline" size="sm">
|
|
53
|
-
{g}
|
|
54
|
-
</Badge>
|
|
55
|
-
))}
|
|
56
|
-
</Group>
|
|
57
|
-
)}
|
|
58
|
-
</Paper>
|
|
59
|
-
))}
|
|
60
|
-
</Stack>
|
|
61
|
-
</Modal>
|
|
62
|
-
)
|
|
63
|
-
}
|
package/src/cache-writer.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { writeAuthCacheSnapshot } from 'canopycms/auth/cache'
|
|
2
|
-
import { DEFAULT_USERS, DEFAULT_GROUPS } from './dev-plugin'
|
|
3
|
-
import type { DevUser, DevGroup } from './dev-plugin'
|
|
4
|
-
|
|
5
|
-
export interface RefreshDevCacheOptions {
|
|
6
|
-
/** Directory to write cache files to (e.g., .canopy-prod-sim/.cache) */
|
|
7
|
-
cachePath: string
|
|
8
|
-
/** Custom users (defaults to DEFAULT_USERS) */
|
|
9
|
-
users?: DevUser[]
|
|
10
|
-
/** Custom groups (defaults to DEFAULT_GROUPS) */
|
|
11
|
-
groups?: DevGroup[]
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Write dev users/groups to cache files for FileBasedAuthCache.
|
|
16
|
-
*
|
|
17
|
-
* This is the dev-auth equivalent of refreshClerkCache() — it populates
|
|
18
|
-
* the same JSON files that CachingAuthPlugin reads. Since dev users are
|
|
19
|
-
* hardcoded, no API calls are needed.
|
|
20
|
-
*
|
|
21
|
-
* Used by the worker's `run-once` command in prod-sim mode with dev auth.
|
|
22
|
-
*/
|
|
23
|
-
export async function refreshDevCache(
|
|
24
|
-
options: RefreshDevCacheOptions,
|
|
25
|
-
): Promise<{ userCount: number; groupCount: number }> {
|
|
26
|
-
const { cachePath } = options
|
|
27
|
-
const users = options.users ?? DEFAULT_USERS
|
|
28
|
-
const groups = options.groups ?? DEFAULT_GROUPS
|
|
29
|
-
|
|
30
|
-
const usersData = {
|
|
31
|
-
users: users.map((u) => ({
|
|
32
|
-
id: u.userId,
|
|
33
|
-
name: u.name,
|
|
34
|
-
email: u.email,
|
|
35
|
-
avatarUrl: u.avatarUrl,
|
|
36
|
-
})),
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const groupsData = {
|
|
40
|
-
groups: groups.map((g) => ({
|
|
41
|
-
id: g.id,
|
|
42
|
-
name: g.name,
|
|
43
|
-
description: g.description,
|
|
44
|
-
})),
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const membershipsData = {
|
|
48
|
-
memberships: Object.fromEntries(
|
|
49
|
-
users.filter((u) => u.externalGroups.length > 0).map((u) => [u.userId, u.externalGroups]),
|
|
50
|
-
),
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Write cache files atomically via snapshot directory + symlink swap
|
|
54
|
-
await writeAuthCacheSnapshot(cachePath, {
|
|
55
|
-
'users.json': usersData,
|
|
56
|
-
'orgs.json': groupsData,
|
|
57
|
-
'memberships.json': membershipsData,
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
return { userCount: users.length, groupCount: groups.length }
|
|
61
|
-
}
|
package/src/client.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import type { CanopyClientConfig } from 'canopycms/client'
|
|
4
|
-
import { UserSwitcherButton } from './UserSwitcherButton'
|
|
5
|
-
import { clearDevUserCookie } from './cookie-utils'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Hook that provides dev auth handlers and components for CanopyCMS editor.
|
|
9
|
-
* Model after: packages/canopycms-auth-clerk/src/client.ts
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```tsx
|
|
13
|
-
* import { useDevAuthConfig } from 'canopycms-auth-dev/client'
|
|
14
|
-
* import config from '../../canopycms.config'
|
|
15
|
-
*
|
|
16
|
-
* export default function EditPage() {
|
|
17
|
-
* const devAuth = useDevAuthConfig()
|
|
18
|
-
* const editorConfig = config.client(devAuth)
|
|
19
|
-
* return <CanopyEditorPage config={editorConfig} />
|
|
20
|
-
* }
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
export function useDevAuthConfig(): Pick<CanopyClientConfig, 'editor'> {
|
|
24
|
-
return {
|
|
25
|
-
editor: {
|
|
26
|
-
AccountComponent: UserSwitcherButton,
|
|
27
|
-
onLogoutClick: () => {
|
|
28
|
-
// Reset to default user
|
|
29
|
-
clearDevUserCookie()
|
|
30
|
-
window.location.reload()
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
}
|
|
34
|
-
}
|
package/src/cookie-utils.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { HeadersLike } from 'canopycms/auth'
|
|
2
|
-
|
|
3
|
-
export const DEV_USER_COOKIE_NAME = 'canopy-dev-user'
|
|
4
|
-
export const DEV_USER_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 // 7 days
|
|
5
|
-
export const DEFAULT_USER_ID = 'dev_user1_2nK8mP4xL9'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Server-side: Extract cookie value from HTTP headers
|
|
9
|
-
*/
|
|
10
|
-
export function getDevUserCookieFromHeaders(headers: HeadersLike): string | null {
|
|
11
|
-
const cookie = headers.get('Cookie')
|
|
12
|
-
if (!cookie) return null
|
|
13
|
-
|
|
14
|
-
const match = cookie.match(new RegExp(`${DEV_USER_COOKIE_NAME}=([^;]+)`))
|
|
15
|
-
return match?.[1] ?? null
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Client-side: Read cookie from document.cookie
|
|
20
|
-
*/
|
|
21
|
-
export function getDevUserCookie(): string | null {
|
|
22
|
-
if (typeof document === 'undefined') return null
|
|
23
|
-
|
|
24
|
-
const match = document.cookie.match(new RegExp(`${DEV_USER_COOKIE_NAME}=([^;]+)`))
|
|
25
|
-
return match?.[1] ?? null
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Client-side: Set dev user cookie
|
|
30
|
-
*/
|
|
31
|
-
export function setDevUserCookie(userId: string): void {
|
|
32
|
-
if (typeof document === 'undefined') return
|
|
33
|
-
|
|
34
|
-
document.cookie = `${DEV_USER_COOKIE_NAME}=${userId}; path=/; max-age=${DEV_USER_COOKIE_MAX_AGE}; SameSite=Lax`
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Client-side: Clear dev user cookie (logout)
|
|
39
|
-
*/
|
|
40
|
-
export function clearDevUserCookie(): void {
|
|
41
|
-
if (typeof document === 'undefined') return
|
|
42
|
-
|
|
43
|
-
document.cookie = `${DEV_USER_COOKIE_NAME}=; path=/; max-age=0`
|
|
44
|
-
}
|
package/src/dev-plugin.test.ts
DELETED
|
@@ -1,470 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
DevAuthPlugin,
|
|
4
|
-
createDevAuthPlugin,
|
|
5
|
-
DEFAULT_USERS,
|
|
6
|
-
DEFAULT_GROUPS,
|
|
7
|
-
DEV_ADMIN_USER_ID,
|
|
8
|
-
} from './dev-plugin'
|
|
9
|
-
import type { DevUser, DevGroup } from './dev-plugin'
|
|
10
|
-
import type { AuthenticationResult } from 'canopycms/auth'
|
|
11
|
-
|
|
12
|
-
// Type guard to assert successful authentication
|
|
13
|
-
function assertSuccess(result: AuthenticationResult): asserts result is AuthenticationResult & {
|
|
14
|
-
success: true
|
|
15
|
-
user: NonNullable<AuthenticationResult['user']>
|
|
16
|
-
} {
|
|
17
|
-
expect(result.success).toBe(true)
|
|
18
|
-
if (!result.success || !result.user) {
|
|
19
|
-
throw new Error('Authentication failed')
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('DevAuthPlugin', () => {
|
|
24
|
-
describe('authenticate', () => {
|
|
25
|
-
it('returns default user when no headers provided', async () => {
|
|
26
|
-
const plugin = new DevAuthPlugin({})
|
|
27
|
-
const result = await plugin.authenticate(new Headers())
|
|
28
|
-
|
|
29
|
-
assertSuccess(result)
|
|
30
|
-
expect(result.user.userId).toBe('dev_user1_2nK8mP4xL9') // user1
|
|
31
|
-
expect(result.user.name).toBe('User One')
|
|
32
|
-
expect(result.user.email).toBe('user1@localhost.dev')
|
|
33
|
-
expect(result.user.externalGroups).toEqual(['team-a', 'team-b'])
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('authenticates via X-Test-User header', async () => {
|
|
37
|
-
const plugin = new DevAuthPlugin({})
|
|
38
|
-
const headers = new Headers({ 'X-Test-User': 'admin' })
|
|
39
|
-
const result = await plugin.authenticate(headers)
|
|
40
|
-
|
|
41
|
-
assertSuccess(result)
|
|
42
|
-
expect(result.user.userId).toBe('dev_admin_3xY6zW1qR5') // admin1
|
|
43
|
-
expect(result.user.name).toBe('Admin One')
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('authenticates via x-dev-user-id header', async () => {
|
|
47
|
-
const plugin = new DevAuthPlugin({})
|
|
48
|
-
const headers = new Headers({ 'x-dev-user-id': 'dev_user2_7qR3tY6wN2' })
|
|
49
|
-
const result = await plugin.authenticate(headers)
|
|
50
|
-
|
|
51
|
-
assertSuccess(result)
|
|
52
|
-
expect(result.user.userId).toBe('dev_user2_7qR3tY6wN2') // user2
|
|
53
|
-
expect(result.user.name).toBe('User Two')
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('authenticates via canopy-dev-user cookie', async () => {
|
|
57
|
-
const plugin = new DevAuthPlugin({})
|
|
58
|
-
const headers = new Headers({
|
|
59
|
-
cookie: 'canopy-dev-user=dev_user3_5vS1pM8kJ4; other=value',
|
|
60
|
-
})
|
|
61
|
-
const result = await plugin.authenticate(headers)
|
|
62
|
-
|
|
63
|
-
assertSuccess(result)
|
|
64
|
-
expect(result.user.userId).toBe('dev_user3_5vS1pM8kJ4') // user3
|
|
65
|
-
expect(result.user.name).toBe('User Three')
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('prioritizes X-Test-User over cookie', async () => {
|
|
69
|
-
const plugin = new DevAuthPlugin({})
|
|
70
|
-
const headers = new Headers({
|
|
71
|
-
'X-Test-User': 'admin',
|
|
72
|
-
cookie: 'canopy-dev-user=dev_user1_2nK8mP4xL9',
|
|
73
|
-
})
|
|
74
|
-
const result = await plugin.authenticate(headers)
|
|
75
|
-
|
|
76
|
-
assertSuccess(result)
|
|
77
|
-
expect(result.user.userId).toBe('dev_admin_3xY6zW1qR5') // admin1 from header
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('maps test user keys to dev user IDs', async () => {
|
|
81
|
-
const plugin = new DevAuthPlugin({})
|
|
82
|
-
|
|
83
|
-
const testCases = [
|
|
84
|
-
{ key: 'admin', expectedId: 'dev_admin_3xY6zW1qR5' },
|
|
85
|
-
{ key: 'editor', expectedId: 'dev_user1_2nK8mP4xL9' },
|
|
86
|
-
{ key: 'viewer', expectedId: 'dev_user2_7qR3tY6wN2' },
|
|
87
|
-
{ key: 'reviewer', expectedId: 'dev_reviewer_9aB4cD2eF7' },
|
|
88
|
-
]
|
|
89
|
-
|
|
90
|
-
for (const { key, expectedId } of testCases) {
|
|
91
|
-
const headers = new Headers({ 'X-Test-User': key })
|
|
92
|
-
const result = await plugin.authenticate(headers)
|
|
93
|
-
|
|
94
|
-
assertSuccess(result)
|
|
95
|
-
expect(result.user.userId).toBe(expectedId)
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('returns failure for unknown user ID', async () => {
|
|
100
|
-
const plugin = new DevAuthPlugin({})
|
|
101
|
-
const headers = new Headers({ 'x-dev-user-id': 'unknown_user' })
|
|
102
|
-
const result = await plugin.authenticate(headers)
|
|
103
|
-
|
|
104
|
-
expect(result.success).toBe(false)
|
|
105
|
-
if (!result.success) {
|
|
106
|
-
expect(result.error).toContain('Dev user not found')
|
|
107
|
-
}
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('uses custom default user when specified', async () => {
|
|
111
|
-
const plugin = new DevAuthPlugin({
|
|
112
|
-
defaultUserId: 'dev_admin_3xY6zW1qR5', // admin1
|
|
113
|
-
})
|
|
114
|
-
const result = await plugin.authenticate(new Headers())
|
|
115
|
-
|
|
116
|
-
assertSuccess(result)
|
|
117
|
-
expect(result.user.userId).toBe('dev_admin_3xY6zW1qR5')
|
|
118
|
-
})
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
describe('searchUsers', () => {
|
|
122
|
-
it('returns all users when query is empty', async () => {
|
|
123
|
-
const plugin = new DevAuthPlugin({})
|
|
124
|
-
const results = await plugin.searchUsers('')
|
|
125
|
-
|
|
126
|
-
expect(results).toHaveLength(5)
|
|
127
|
-
expect(results[0].id).toBe('dev_user1_2nK8mP4xL9')
|
|
128
|
-
expect(results[0].name).toBe('User One')
|
|
129
|
-
expect(results[0].email).toBe('user1@localhost.dev')
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('filters users by name', async () => {
|
|
133
|
-
const plugin = new DevAuthPlugin({})
|
|
134
|
-
const results = await plugin.searchUsers('admin')
|
|
135
|
-
|
|
136
|
-
expect(results).toHaveLength(1)
|
|
137
|
-
expect(results[0].name).toBe('Admin One')
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('filters users by email', async () => {
|
|
141
|
-
const plugin = new DevAuthPlugin({})
|
|
142
|
-
const results = await plugin.searchUsers('reviewer1@')
|
|
143
|
-
|
|
144
|
-
expect(results).toHaveLength(1)
|
|
145
|
-
expect(results[0].id).toBe('dev_reviewer_9aB4cD2eF7')
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
it('is case insensitive', async () => {
|
|
149
|
-
const plugin = new DevAuthPlugin({})
|
|
150
|
-
const results = await plugin.searchUsers('USER')
|
|
151
|
-
|
|
152
|
-
expect(results.length).toBeGreaterThan(0)
|
|
153
|
-
expect(results.some((u) => u.name.toLowerCase().includes('user'))).toBe(true)
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('respects limit parameter', async () => {
|
|
157
|
-
const plugin = new DevAuthPlugin({})
|
|
158
|
-
const results = await plugin.searchUsers('', 2)
|
|
159
|
-
|
|
160
|
-
expect(results).toHaveLength(2)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('works with custom users', async () => {
|
|
164
|
-
const customUsers: DevUser[] = [
|
|
165
|
-
{
|
|
166
|
-
userId: 'custom_1',
|
|
167
|
-
name: 'Custom User',
|
|
168
|
-
email: 'custom@test.com',
|
|
169
|
-
externalGroups: [],
|
|
170
|
-
},
|
|
171
|
-
]
|
|
172
|
-
const plugin = new DevAuthPlugin({ users: customUsers })
|
|
173
|
-
const results = await plugin.searchUsers('custom')
|
|
174
|
-
|
|
175
|
-
expect(results).toHaveLength(1)
|
|
176
|
-
expect(results[0].id).toBe('custom_1')
|
|
177
|
-
})
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
describe('getUserMetadata', () => {
|
|
181
|
-
it('returns user metadata for valid user', async () => {
|
|
182
|
-
const plugin = new DevAuthPlugin({})
|
|
183
|
-
const metadata = await plugin.getUserMetadata('dev_user1_2nK8mP4xL9')
|
|
184
|
-
|
|
185
|
-
expect(metadata).toEqual({
|
|
186
|
-
id: 'dev_user1_2nK8mP4xL9',
|
|
187
|
-
name: 'User One',
|
|
188
|
-
email: 'user1@localhost.dev',
|
|
189
|
-
avatarUrl: undefined,
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('returns null for unknown user', async () => {
|
|
194
|
-
const plugin = new DevAuthPlugin({})
|
|
195
|
-
const metadata = await plugin.getUserMetadata('unknown')
|
|
196
|
-
|
|
197
|
-
expect(metadata).toBeNull()
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it('includes avatarUrl when present', async () => {
|
|
201
|
-
const customUsers: DevUser[] = [
|
|
202
|
-
{
|
|
203
|
-
userId: 'user_1',
|
|
204
|
-
name: 'User',
|
|
205
|
-
email: 'user@test.com',
|
|
206
|
-
avatarUrl: 'https://example.com/avatar.jpg',
|
|
207
|
-
externalGroups: [],
|
|
208
|
-
},
|
|
209
|
-
]
|
|
210
|
-
const plugin = new DevAuthPlugin({ users: customUsers })
|
|
211
|
-
const metadata = await plugin.getUserMetadata('user_1')
|
|
212
|
-
|
|
213
|
-
expect(metadata?.avatarUrl).toBe('https://example.com/avatar.jpg')
|
|
214
|
-
})
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
describe('getGroupMetadata', () => {
|
|
218
|
-
it('returns group metadata for valid group', async () => {
|
|
219
|
-
const plugin = new DevAuthPlugin({})
|
|
220
|
-
const metadata = await plugin.getGroupMetadata('team-a')
|
|
221
|
-
|
|
222
|
-
expect(metadata).toEqual({
|
|
223
|
-
id: 'team-a',
|
|
224
|
-
name: 'Team A',
|
|
225
|
-
description: 'Team A',
|
|
226
|
-
})
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
it('returns null for unknown group', async () => {
|
|
230
|
-
const plugin = new DevAuthPlugin({})
|
|
231
|
-
const metadata = await plugin.getGroupMetadata('unknown')
|
|
232
|
-
|
|
233
|
-
expect(metadata).toBeNull()
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
it('works with custom groups', async () => {
|
|
237
|
-
const customGroups: DevGroup[] = [
|
|
238
|
-
{ id: 'team-x', name: 'Team X', description: 'Custom team' },
|
|
239
|
-
]
|
|
240
|
-
const plugin = new DevAuthPlugin({ groups: customGroups })
|
|
241
|
-
const metadata = await plugin.getGroupMetadata('team-x')
|
|
242
|
-
|
|
243
|
-
expect(metadata).toEqual({
|
|
244
|
-
id: 'team-x',
|
|
245
|
-
name: 'Team X',
|
|
246
|
-
description: 'Custom team',
|
|
247
|
-
})
|
|
248
|
-
})
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
describe('listGroups', () => {
|
|
252
|
-
it('returns all groups by default', async () => {
|
|
253
|
-
const plugin = new DevAuthPlugin({})
|
|
254
|
-
const groups = await plugin.listGroups()
|
|
255
|
-
|
|
256
|
-
expect(groups).toHaveLength(3)
|
|
257
|
-
expect(groups.map((g) => g.id)).toEqual(['team-a', 'team-b', 'team-c'])
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
it('respects limit parameter', async () => {
|
|
261
|
-
const plugin = new DevAuthPlugin({})
|
|
262
|
-
const groups = await plugin.listGroups(2)
|
|
263
|
-
|
|
264
|
-
expect(groups).toHaveLength(2)
|
|
265
|
-
})
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
describe('searchExternalGroups', () => {
|
|
269
|
-
it('returns all groups when query is empty', async () => {
|
|
270
|
-
const plugin = new DevAuthPlugin({})
|
|
271
|
-
const results = await plugin.searchExternalGroups('')
|
|
272
|
-
|
|
273
|
-
expect(results).toHaveLength(3)
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
it('filters groups by name', async () => {
|
|
277
|
-
const plugin = new DevAuthPlugin({})
|
|
278
|
-
const results = await plugin.searchExternalGroups('team a')
|
|
279
|
-
|
|
280
|
-
expect(results).toHaveLength(1)
|
|
281
|
-
expect(results[0].id).toBe('team-a')
|
|
282
|
-
expect(results[0].name).toBe('Team A')
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
it('is case insensitive', async () => {
|
|
286
|
-
const plugin = new DevAuthPlugin({})
|
|
287
|
-
const results = await plugin.searchExternalGroups('TEAM')
|
|
288
|
-
|
|
289
|
-
expect(results.length).toBeGreaterThan(0)
|
|
290
|
-
})
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
describe('custom configuration', () => {
|
|
294
|
-
it('accepts custom users and groups', () => {
|
|
295
|
-
const customUsers: DevUser[] = [
|
|
296
|
-
{
|
|
297
|
-
userId: 'custom_1',
|
|
298
|
-
name: 'Custom',
|
|
299
|
-
email: 'custom@test.com',
|
|
300
|
-
externalGroups: ['custom-group'],
|
|
301
|
-
},
|
|
302
|
-
]
|
|
303
|
-
const customGroups: DevGroup[] = [{ id: 'custom-group', name: 'Custom Group' }]
|
|
304
|
-
|
|
305
|
-
const plugin = new DevAuthPlugin({
|
|
306
|
-
users: customUsers,
|
|
307
|
-
groups: customGroups,
|
|
308
|
-
defaultUserId: 'custom_1',
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
expect(plugin).toBeInstanceOf(DevAuthPlugin)
|
|
312
|
-
})
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
describe('createDevAuthPlugin factory', () => {
|
|
316
|
-
beforeEach(() => {
|
|
317
|
-
vi.spyOn(console, 'info').mockImplementation(() => {})
|
|
318
|
-
})
|
|
319
|
-
afterEach(() => {
|
|
320
|
-
vi.restoreAllMocks()
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
it('creates plugin with default config', () => {
|
|
324
|
-
const plugin = createDevAuthPlugin()
|
|
325
|
-
expect(plugin).toBeInstanceOf(DevAuthPlugin)
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
it('creates plugin with custom config', () => {
|
|
329
|
-
const plugin = createDevAuthPlugin({
|
|
330
|
-
defaultUserId: 'dev_admin_3xY6zW1qR5',
|
|
331
|
-
})
|
|
332
|
-
expect(plugin).toBeInstanceOf(DevAuthPlugin)
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
it('works without any arguments', () => {
|
|
336
|
-
const plugin = createDevAuthPlugin()
|
|
337
|
-
expect(plugin).toBeInstanceOf(DevAuthPlugin)
|
|
338
|
-
})
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
describe('DEFAULT_USERS', () => {
|
|
342
|
-
it('exports default users', () => {
|
|
343
|
-
expect(DEFAULT_USERS).toHaveLength(5)
|
|
344
|
-
expect(DEFAULT_USERS[0].userId).toBe('dev_user1_2nK8mP4xL9')
|
|
345
|
-
expect(DEFAULT_USERS[4].userId).toBe('dev_admin_3xY6zW1qR5')
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
it('has correct user structure', () => {
|
|
349
|
-
DEFAULT_USERS.forEach((user) => {
|
|
350
|
-
expect(user).toHaveProperty('userId')
|
|
351
|
-
expect(user).toHaveProperty('name')
|
|
352
|
-
expect(user).toHaveProperty('email')
|
|
353
|
-
expect(user).toHaveProperty('externalGroups')
|
|
354
|
-
expect(Array.isArray(user.externalGroups)).toBe(true)
|
|
355
|
-
})
|
|
356
|
-
})
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
describe('DEFAULT_GROUPS', () => {
|
|
360
|
-
it('exports default groups', () => {
|
|
361
|
-
expect(DEFAULT_GROUPS).toHaveLength(3)
|
|
362
|
-
expect(DEFAULT_GROUPS.map((g) => g.id)).toEqual(['team-a', 'team-b', 'team-c'])
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
it('has correct group structure', () => {
|
|
366
|
-
DEFAULT_GROUPS.forEach((group) => {
|
|
367
|
-
expect(group).toHaveProperty('id')
|
|
368
|
-
expect(group).toHaveProperty('name')
|
|
369
|
-
expect(group).toHaveProperty('description')
|
|
370
|
-
})
|
|
371
|
-
})
|
|
372
|
-
})
|
|
373
|
-
|
|
374
|
-
describe('cookie parsing', () => {
|
|
375
|
-
it('extracts cookie from single cookie string', async () => {
|
|
376
|
-
const plugin = new DevAuthPlugin({})
|
|
377
|
-
const headers = new Headers({
|
|
378
|
-
cookie: 'canopy-dev-user=dev_admin_3xY6zW1qR5',
|
|
379
|
-
})
|
|
380
|
-
const result = await plugin.authenticate(headers)
|
|
381
|
-
|
|
382
|
-
assertSuccess(result)
|
|
383
|
-
expect(result.user.userId).toBe('dev_admin_3xY6zW1qR5')
|
|
384
|
-
})
|
|
385
|
-
|
|
386
|
-
it('extracts cookie from multiple cookies', async () => {
|
|
387
|
-
const plugin = new DevAuthPlugin({})
|
|
388
|
-
const headers = new Headers({
|
|
389
|
-
cookie: 'session=abc123; canopy-dev-user=dev_user2_7qR3tY6wN2; other=value',
|
|
390
|
-
})
|
|
391
|
-
const result = await plugin.authenticate(headers)
|
|
392
|
-
|
|
393
|
-
assertSuccess(result)
|
|
394
|
-
expect(result.user.userId).toBe('dev_user2_7qR3tY6wN2')
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
it('extracts cookie without semicolon separator', async () => {
|
|
398
|
-
const plugin = new DevAuthPlugin({})
|
|
399
|
-
const headers = new Headers({
|
|
400
|
-
cookie: 'canopy-dev-user=dev_reviewer_9aB4cD2eF7',
|
|
401
|
-
})
|
|
402
|
-
const result = await plugin.authenticate(headers)
|
|
403
|
-
|
|
404
|
-
assertSuccess(result)
|
|
405
|
-
expect(result.user.userId).toBe('dev_reviewer_9aB4cD2eF7')
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
it('returns default user when cookie not found', async () => {
|
|
409
|
-
const plugin = new DevAuthPlugin({})
|
|
410
|
-
const headers = new Headers({
|
|
411
|
-
cookie: 'session=abc123; other=value',
|
|
412
|
-
})
|
|
413
|
-
const result = await plugin.authenticate(headers)
|
|
414
|
-
|
|
415
|
-
assertSuccess(result)
|
|
416
|
-
expect(result.user.userId).toBe('dev_user1_2nK8mP4xL9') // default
|
|
417
|
-
})
|
|
418
|
-
})
|
|
419
|
-
|
|
420
|
-
describe('auto-bootstrap admin', () => {
|
|
421
|
-
let originalEnv: string | undefined
|
|
422
|
-
|
|
423
|
-
beforeEach(() => {
|
|
424
|
-
originalEnv = process.env.CANOPY_BOOTSTRAP_ADMIN_IDS
|
|
425
|
-
vi.spyOn(console, 'info').mockImplementation(() => {})
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
afterEach(() => {
|
|
429
|
-
vi.restoreAllMocks()
|
|
430
|
-
if (originalEnv === undefined) {
|
|
431
|
-
delete process.env.CANOPY_BOOTSTRAP_ADMIN_IDS
|
|
432
|
-
} else {
|
|
433
|
-
process.env.CANOPY_BOOTSTRAP_ADMIN_IDS = originalEnv
|
|
434
|
-
}
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
it('auto-sets CANOPY_BOOTSTRAP_ADMIN_IDS when not already set', () => {
|
|
438
|
-
delete process.env.CANOPY_BOOTSTRAP_ADMIN_IDS
|
|
439
|
-
createDevAuthPlugin()
|
|
440
|
-
expect(process.env.CANOPY_BOOTSTRAP_ADMIN_IDS).toBe(DEV_ADMIN_USER_ID)
|
|
441
|
-
})
|
|
442
|
-
|
|
443
|
-
it('does not override existing CANOPY_BOOTSTRAP_ADMIN_IDS', () => {
|
|
444
|
-
process.env.CANOPY_BOOTSTRAP_ADMIN_IDS = 'custom_user_id'
|
|
445
|
-
createDevAuthPlugin()
|
|
446
|
-
expect(process.env.CANOPY_BOOTSTRAP_ADMIN_IDS).toBe('custom_user_id')
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
it('can be disabled via autoBootstrapAdmin: false', () => {
|
|
450
|
-
delete process.env.CANOPY_BOOTSTRAP_ADMIN_IDS
|
|
451
|
-
createDevAuthPlugin({ autoBootstrapAdmin: false })
|
|
452
|
-
expect(process.env.CANOPY_BOOTSTRAP_ADMIN_IDS).toBeUndefined()
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
it('skips auto-bootstrap when custom users do not include admin user', () => {
|
|
456
|
-
delete process.env.CANOPY_BOOTSTRAP_ADMIN_IDS
|
|
457
|
-
createDevAuthPlugin({
|
|
458
|
-
users: [
|
|
459
|
-
{
|
|
460
|
-
userId: 'custom_1',
|
|
461
|
-
name: 'Custom',
|
|
462
|
-
email: 'custom@test.com',
|
|
463
|
-
externalGroups: [],
|
|
464
|
-
},
|
|
465
|
-
],
|
|
466
|
-
})
|
|
467
|
-
expect(process.env.CANOPY_BOOTSTRAP_ADMIN_IDS).toBeUndefined()
|
|
468
|
-
})
|
|
469
|
-
})
|
|
470
|
-
})
|
package/src/dev-plugin.ts
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import type { AuthPlugin } from 'canopycms/auth'
|
|
2
|
-
import type { UserSearchResult, GroupMetadata, AuthenticationResult } from 'canopycms/auth'
|
|
3
|
-
import { extractHeaders } from 'canopycms/auth'
|
|
4
|
-
import type { CanopyUserId, CanopyGroupId } from 'canopycms'
|
|
5
|
-
import { getDevUserCookieFromHeaders } from './cookie-utils'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* WARNING: This plugin is for development and testing only!
|
|
9
|
-
* Do not use in production environments.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export interface DevUser {
|
|
13
|
-
userId: CanopyUserId
|
|
14
|
-
name: string
|
|
15
|
-
email: string
|
|
16
|
-
avatarUrl?: string
|
|
17
|
-
externalGroups: CanopyGroupId[]
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface DevGroup {
|
|
21
|
-
id: CanopyGroupId
|
|
22
|
-
name: string
|
|
23
|
-
description?: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const DEV_ADMIN_USER_ID: CanopyUserId = 'dev_admin_3xY6zW1qR5'
|
|
27
|
-
|
|
28
|
-
export interface DevAuthConfig {
|
|
29
|
-
/**
|
|
30
|
-
* Custom mock users. If not provided, uses default users.
|
|
31
|
-
*/
|
|
32
|
-
users?: DevUser[]
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Custom mock groups. If not provided, uses default groups.
|
|
36
|
-
*/
|
|
37
|
-
groups?: DevGroup[]
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Default user ID when no user is selected.
|
|
41
|
-
* @default 'dev_user1_2nK8mP4xL9' (user1)
|
|
42
|
-
*/
|
|
43
|
-
defaultUserId?: CanopyUserId
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Whether to auto-set CANOPY_BOOTSTRAP_ADMIN_IDS for the admin dev user
|
|
47
|
-
* when the env var is not already set. Defaults to true.
|
|
48
|
-
*/
|
|
49
|
-
autoBootstrapAdmin?: boolean
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const DEFAULT_USERS: DevUser[] = [
|
|
53
|
-
{
|
|
54
|
-
userId: 'dev_user1_2nK8mP4xL9',
|
|
55
|
-
name: 'User One',
|
|
56
|
-
email: 'user1@localhost.dev',
|
|
57
|
-
externalGroups: ['team-a', 'team-b'],
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
userId: 'dev_user2_7qR3tY6wN2',
|
|
61
|
-
name: 'User Two',
|
|
62
|
-
email: 'user2@localhost.dev',
|
|
63
|
-
externalGroups: ['team-b'],
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
userId: 'dev_user3_5vS1pM8kJ4',
|
|
67
|
-
name: 'User Three',
|
|
68
|
-
email: 'user3@localhost.dev',
|
|
69
|
-
externalGroups: ['team-c'],
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
userId: 'dev_reviewer_9aB4cD2eF7',
|
|
73
|
-
name: 'Reviewer One',
|
|
74
|
-
email: 'reviewer1@localhost.dev',
|
|
75
|
-
externalGroups: ['team-a'],
|
|
76
|
-
// Note: 'Reviewers' membership comes from internal groups file, not auth plugin
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
userId: DEV_ADMIN_USER_ID,
|
|
80
|
-
name: 'Admin One',
|
|
81
|
-
email: 'admin1@localhost.dev',
|
|
82
|
-
externalGroups: ['team-a', 'team-b', 'team-c'],
|
|
83
|
-
// Note: Does NOT include 'Admins' - that's applied by bootstrap admin config or auto-bootstrap
|
|
84
|
-
},
|
|
85
|
-
]
|
|
86
|
-
|
|
87
|
-
export const DEFAULT_GROUPS: DevGroup[] = [
|
|
88
|
-
{ id: 'team-a', name: 'Team A', description: 'Team A' },
|
|
89
|
-
{ id: 'team-b', name: 'Team B', description: 'Team B' },
|
|
90
|
-
{ id: 'team-c', name: 'Team C', description: 'Team C' },
|
|
91
|
-
]
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Dev authentication plugin implementation for CanopyCMS.
|
|
95
|
-
* Supports both cookie-based (UI) and header-based (tests) authentication.
|
|
96
|
-
*/
|
|
97
|
-
export class DevAuthPlugin implements AuthPlugin {
|
|
98
|
-
private users: DevUser[]
|
|
99
|
-
private groups: DevGroup[]
|
|
100
|
-
private defaultUserId: CanopyUserId
|
|
101
|
-
|
|
102
|
-
constructor(config: DevAuthConfig = {}) {
|
|
103
|
-
this.users = config.users ?? DEFAULT_USERS
|
|
104
|
-
this.groups = config.groups ?? DEFAULT_GROUPS
|
|
105
|
-
this.defaultUserId = config.defaultUserId ?? 'dev_user1_2nK8mP4xL9'
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async authenticate(context: unknown): Promise<AuthenticationResult> {
|
|
109
|
-
// 1. Extract headers using extractHeaders() helper
|
|
110
|
-
const headers = extractHeaders(context)
|
|
111
|
-
if (!headers) {
|
|
112
|
-
return { success: false, error: 'Invalid context' }
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 2. Check X-Test-User header (for test-app compatibility) FIRST
|
|
116
|
-
let userId = headers.get('X-Test-User')
|
|
117
|
-
|
|
118
|
-
// 3. If no test header, check x-dev-user-id header OR canopy-dev-user cookie
|
|
119
|
-
if (!userId) {
|
|
120
|
-
userId = headers.get('x-dev-user-id') ?? getDevUserCookieFromHeaders(headers)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 4. Fall back to default user
|
|
124
|
-
if (!userId) {
|
|
125
|
-
userId = this.defaultUserId
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 5. Map test user keys to dev user IDs for test compatibility
|
|
129
|
-
const userIdMapped = this.mapTestUserKey(userId)
|
|
130
|
-
|
|
131
|
-
// 6. Find user in config
|
|
132
|
-
const user = this.users.find((u) => u.userId === userIdMapped)
|
|
133
|
-
if (!user) {
|
|
134
|
-
return { success: false, error: `Dev user not found: ${userId}` }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// 7. Return AuthenticationResult with externalGroups
|
|
138
|
-
return {
|
|
139
|
-
success: true,
|
|
140
|
-
user: {
|
|
141
|
-
userId: user.userId,
|
|
142
|
-
email: user.email,
|
|
143
|
-
name: user.name,
|
|
144
|
-
avatarUrl: user.avatarUrl,
|
|
145
|
-
externalGroups: user.externalGroups,
|
|
146
|
-
},
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Map test-app user keys to dev user IDs for backward compatibility
|
|
152
|
-
*/
|
|
153
|
-
private mapTestUserKey(key: string): CanopyUserId {
|
|
154
|
-
const testUserMap: Record<string, CanopyUserId> = {
|
|
155
|
-
admin: DEV_ADMIN_USER_ID, // admin1
|
|
156
|
-
editor: 'dev_user1_2nK8mP4xL9', // user1
|
|
157
|
-
viewer: 'dev_user2_7qR3tY6wN2', // user2
|
|
158
|
-
reviewer: 'dev_reviewer_9aB4cD2eF7', // reviewer1
|
|
159
|
-
}
|
|
160
|
-
return testUserMap[key] ?? key
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async searchUsers(query: string, limit?: number): Promise<UserSearchResult[]> {
|
|
164
|
-
const lowerQuery = query.toLowerCase()
|
|
165
|
-
const filtered = this.users.filter(
|
|
166
|
-
(u) =>
|
|
167
|
-
u.name.toLowerCase().includes(lowerQuery) || u.email.toLowerCase().includes(lowerQuery),
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
const results = filtered.slice(0, limit)
|
|
171
|
-
return results.map((u) => ({
|
|
172
|
-
id: u.userId,
|
|
173
|
-
name: u.name,
|
|
174
|
-
email: u.email,
|
|
175
|
-
avatarUrl: u.avatarUrl,
|
|
176
|
-
}))
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async getUserMetadata(userId: CanopyUserId): Promise<UserSearchResult | null> {
|
|
180
|
-
const user = this.users.find((u) => u.userId === userId)
|
|
181
|
-
if (!user) return null
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
id: user.userId,
|
|
185
|
-
name: user.name,
|
|
186
|
-
email: user.email,
|
|
187
|
-
avatarUrl: user.avatarUrl,
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async getGroupMetadata(groupId: CanopyGroupId): Promise<GroupMetadata | null> {
|
|
192
|
-
const group = this.groups.find((g) => g.id === groupId)
|
|
193
|
-
if (!group) return null
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
id: group.id,
|
|
197
|
-
name: group.name,
|
|
198
|
-
description: group.description,
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async listGroups(limit?: number): Promise<GroupMetadata[]> {
|
|
203
|
-
const groups = limit ? this.groups.slice(0, limit) : this.groups
|
|
204
|
-
return groups.map((g) => ({
|
|
205
|
-
id: g.id,
|
|
206
|
-
name: g.name,
|
|
207
|
-
description: g.description,
|
|
208
|
-
}))
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async searchExternalGroups(query: string): Promise<Array<{ id: CanopyGroupId; name: string }>> {
|
|
212
|
-
const lowerQuery = query.toLowerCase()
|
|
213
|
-
return this.groups
|
|
214
|
-
.filter((g) => g.name.toLowerCase().includes(lowerQuery))
|
|
215
|
-
.map((g) => ({
|
|
216
|
-
id: g.id,
|
|
217
|
-
name: g.name,
|
|
218
|
-
}))
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Factory function for creating dev auth plugin.
|
|
224
|
-
* By default, auto-sets CANOPY_BOOTSTRAP_ADMIN_IDS to the admin dev user
|
|
225
|
-
* if the env var is not already set. Disable with { autoBootstrapAdmin: false }.
|
|
226
|
-
*/
|
|
227
|
-
export function createDevAuthPlugin(config?: DevAuthConfig): AuthPlugin {
|
|
228
|
-
const shouldAutoBootstrap = config?.autoBootstrapAdmin ?? true
|
|
229
|
-
if (shouldAutoBootstrap && !process.env.CANOPY_BOOTSTRAP_ADMIN_IDS) {
|
|
230
|
-
const users = config?.users ?? DEFAULT_USERS
|
|
231
|
-
const adminUser = users.find((u) => u.userId === DEV_ADMIN_USER_ID)
|
|
232
|
-
if (adminUser) {
|
|
233
|
-
process.env.CANOPY_BOOTSTRAP_ADMIN_IDS = adminUser.userId
|
|
234
|
-
console.info(
|
|
235
|
-
`CanopyCMS dev-auth: Auto-configured ${adminUser.name} (${adminUser.userId}) as bootstrap admin. ` +
|
|
236
|
-
`Set CANOPY_BOOTSTRAP_ADMIN_IDS env var or pass autoBootstrapAdmin: false to override.`,
|
|
237
|
-
)
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return new DevAuthPlugin(config ?? {})
|
|
241
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
createDevAuthPlugin,
|
|
3
|
-
DevAuthPlugin,
|
|
4
|
-
DEFAULT_USERS,
|
|
5
|
-
DEFAULT_GROUPS,
|
|
6
|
-
DEV_ADMIN_USER_ID,
|
|
7
|
-
} from './dev-plugin'
|
|
8
|
-
export type { DevAuthConfig, DevUser, DevGroup } from './dev-plugin'
|
|
9
|
-
export { createDevTokenVerifier } from './jwt-verifier'
|
|
10
|
-
export { refreshDevCache } from './cache-writer'
|
|
11
|
-
export type { RefreshDevCacheOptions } from './cache-writer'
|
package/src/jwt-verifier.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { extractHeaders } from 'canopycms/auth'
|
|
2
|
-
import type { TokenVerifier } from 'canopycms/auth'
|
|
3
|
-
import { getDevUserCookieFromHeaders, DEFAULT_USER_ID } from './cookie-utils'
|
|
4
|
-
import { DEV_ADMIN_USER_ID } from './dev-plugin'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Test user key → dev user ID mapping.
|
|
8
|
-
* Matches the mapping in DevAuthPlugin.mapTestUserKey().
|
|
9
|
-
*/
|
|
10
|
-
const TEST_USER_MAP: Record<string, string> = {
|
|
11
|
-
admin: DEV_ADMIN_USER_ID,
|
|
12
|
-
editor: 'dev_user1_2nK8mP4xL9',
|
|
13
|
-
viewer: 'dev_user2_7qR3tY6wN2',
|
|
14
|
-
reviewer: 'dev_reviewer_9aB4cD2eF7',
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Creates a token verifier for dev auth.
|
|
19
|
-
* Extracts userId from X-Test-User header, x-dev-user-id header,
|
|
20
|
-
* or canopy-dev-user cookie — same logic as DevAuthPlugin.authenticate().
|
|
21
|
-
*
|
|
22
|
-
* Used with CachingAuthPlugin in prod-sim mode to simulate the prod
|
|
23
|
-
* code path (token verification + cached metadata lookup) using dev users.
|
|
24
|
-
*/
|
|
25
|
-
export function createDevTokenVerifier(options?: { defaultUserId?: string }): TokenVerifier {
|
|
26
|
-
const defaultUserId = options?.defaultUserId ?? DEFAULT_USER_ID
|
|
27
|
-
|
|
28
|
-
return async (context: unknown) => {
|
|
29
|
-
const headers = extractHeaders(context)
|
|
30
|
-
if (!headers) return null
|
|
31
|
-
|
|
32
|
-
// Same extraction logic as DevAuthPlugin.authenticate()
|
|
33
|
-
let userId = headers.get('X-Test-User')
|
|
34
|
-
if (!userId) {
|
|
35
|
-
userId = headers.get('x-dev-user-id') ?? getDevUserCookieFromHeaders(headers)
|
|
36
|
-
}
|
|
37
|
-
if (!userId) {
|
|
38
|
-
userId = defaultUserId
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Map test user keys to dev user IDs
|
|
42
|
-
const mapped = TEST_USER_MAP[userId] ?? userId
|
|
43
|
-
|
|
44
|
-
return { userId: mapped }
|
|
45
|
-
}
|
|
46
|
-
}
|