canopycms-auth-dev 0.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/README.md +324 -0
- package/dist/UserSwitcherButton.d.ts +4 -0
- package/dist/UserSwitcherButton.js +23 -0
- package/dist/UserSwitcherButton.js.map +1 -0
- package/dist/UserSwitcherModal.d.ts +10 -0
- package/dist/UserSwitcherModal.js +19 -0
- package/dist/UserSwitcherModal.js.map +1 -0
- package/dist/cache-writer.d.ts +22 -0
- package/dist/cache-writer.js +42 -0
- package/dist/cache-writer.js.map +1 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.js +32 -0
- package/dist/client.js.map +1 -0
- package/dist/cookie-utils.d.ts +20 -0
- package/dist/cookie-utils.js +39 -0
- package/dist/cookie-utils.js.map +1 -0
- package/dist/dev-plugin.d.ts +71 -0
- package/dist/dev-plugin.js +168 -0
- package/dist/dev-plugin.js.map +1 -0
- package/dist/dev-plugin.test.d.ts +1 -0
- package/dist/dev-plugin.test.js +382 -0
- package/dist/dev-plugin.test.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt-verifier.d.ts +12 -0
- package/dist/jwt-verifier.js +41 -0
- package/dist/jwt-verifier.js.map +1 -0
- package/package.json +62 -0
- package/src/UserSwitcherButton.tsx +46 -0
- package/src/UserSwitcherModal.tsx +63 -0
- package/src/cache-writer.ts +61 -0
- package/src/client.ts +34 -0
- package/src/cookie-utils.ts +44 -0
- package/src/dev-plugin.test.ts +470 -0
- package/src/dev-plugin.ts +241 -0
- package/src/index.ts +11 -0
- package/src/jwt-verifier.ts +46 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { extractHeaders } from 'canopycms/auth';
|
|
2
|
+
import { getDevUserCookieFromHeaders, DEFAULT_USER_ID } from './cookie-utils';
|
|
3
|
+
import { DEV_ADMIN_USER_ID } from './dev-plugin';
|
|
4
|
+
/**
|
|
5
|
+
* Test user key → dev user ID mapping.
|
|
6
|
+
* Matches the mapping in DevAuthPlugin.mapTestUserKey().
|
|
7
|
+
*/
|
|
8
|
+
const TEST_USER_MAP = {
|
|
9
|
+
admin: DEV_ADMIN_USER_ID,
|
|
10
|
+
editor: 'dev_user1_2nK8mP4xL9',
|
|
11
|
+
viewer: 'dev_user2_7qR3tY6wN2',
|
|
12
|
+
reviewer: 'dev_reviewer_9aB4cD2eF7',
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Creates a token verifier for dev auth.
|
|
16
|
+
* Extracts userId from X-Test-User header, x-dev-user-id header,
|
|
17
|
+
* or canopy-dev-user cookie — same logic as DevAuthPlugin.authenticate().
|
|
18
|
+
*
|
|
19
|
+
* Used with CachingAuthPlugin in prod-sim mode to simulate the prod
|
|
20
|
+
* code path (token verification + cached metadata lookup) using dev users.
|
|
21
|
+
*/
|
|
22
|
+
export function createDevTokenVerifier(options) {
|
|
23
|
+
const defaultUserId = options?.defaultUserId ?? DEFAULT_USER_ID;
|
|
24
|
+
return async (context) => {
|
|
25
|
+
const headers = extractHeaders(context);
|
|
26
|
+
if (!headers)
|
|
27
|
+
return null;
|
|
28
|
+
// Same extraction logic as DevAuthPlugin.authenticate()
|
|
29
|
+
let userId = headers.get('X-Test-User');
|
|
30
|
+
if (!userId) {
|
|
31
|
+
userId = headers.get('x-dev-user-id') ?? getDevUserCookieFromHeaders(headers);
|
|
32
|
+
}
|
|
33
|
+
if (!userId) {
|
|
34
|
+
userId = defaultUserId;
|
|
35
|
+
}
|
|
36
|
+
// Map test user keys to dev user IDs
|
|
37
|
+
const mapped = TEST_USER_MAP[userId] ?? userId;
|
|
38
|
+
return { userId: mapped };
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=jwt-verifier.js.map
|
|
@@ -0,0 +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;;;;;;;GAOG;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
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "canopycms-auth-dev",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Development authentication provider for CanopyCMS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/safeinsights/canopycms.git",
|
|
9
|
+
"directory": "packages/canopycms-auth-dev"
|
|
10
|
+
},
|
|
11
|
+
"private": false,
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.ts",
|
|
17
|
+
"./client": "./src/client.ts",
|
|
18
|
+
"./cache-writer": "./src/cache-writer.ts"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts"
|
|
29
|
+
},
|
|
30
|
+
"./client": {
|
|
31
|
+
"import": "./dist/client.js",
|
|
32
|
+
"types": "./dist/client.d.ts"
|
|
33
|
+
},
|
|
34
|
+
"./cache-writer": {
|
|
35
|
+
"import": "./dist/cache-writer.js",
|
|
36
|
+
"types": "./dist/cache-writer.d.ts"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.build.json",
|
|
42
|
+
"test": "vitest run --reporter=dot",
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"canopycms": "*",
|
|
50
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
51
|
+
"@mantine/core": "^7.0.0",
|
|
52
|
+
"@mantine/hooks": "^7.0.0",
|
|
53
|
+
"@mantine/modals": "^7.0.0",
|
|
54
|
+
"react-icons": "^5.0.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^22.9.0",
|
|
58
|
+
"@types/react": "^18.0.0",
|
|
59
|
+
"typescript": "^5.6.3",
|
|
60
|
+
"vitest": "^1.6.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
}
|