Skip to content

Commit c99c00d

Browse files
committed
feat: Convex shared auth integration
1 parent 6c19dc1 commit c99c00d

12 files changed

Lines changed: 152 additions & 81 deletions

File tree

apps/backend-convex/.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Use `npx convex env` command to set the following environment variables:
2+
3+
# CUSTOM_JWT_ISSUER=
4+
# CUSTOM_JWT_JWKS_URL=

apps/backend-convex/convex/_generated/api.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
FilterApi,
1414
FunctionReference,
1515
} from "convex/server";
16+
import type * as authInfo from "../authInfo.js";
1617
import type * as tasks from "../tasks.js";
1718

1819
/**
@@ -24,6 +25,7 @@ import type * as tasks from "../tasks.js";
2425
* ```
2526
*/
2627
declare const fullApi: ApiFromModules<{
28+
authInfo: typeof authInfo;
2729
tasks: typeof tasks;
2830
}>;
2931
export declare const api: FilterApi<
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* eslint-disable node/prefer-global/process */
2+
export default {
3+
providers: [
4+
{
5+
type: 'customJwt',
6+
issuer: process.env.CUSTOM_JWT_ISSUER!,
7+
jwks: process.env.CUSTOM_JWT_JWKS_URL!,
8+
algorithm: 'RS256',
9+
},
10+
],
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { query } from './_generated/server'
2+
3+
export const get = query({
4+
args: {},
5+
handler: async (ctx) => {
6+
return await ctx.auth.getUserIdentity()
7+
},
8+
})

apps/backend/src/api/$.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { appFactory } from '#src/helpers/factory.js'
22
import { apiRouteApp } from './$$'
33
import { authApp } from './auth/$'
4-
import { dummyConvexRouteApp } from './dummy/convex'
4+
import { dummyConvexTasksRouteApp } from './dummy/convexTasks'
55
import { dummyGreetRouteApp } from './dummy/greet'
66
import { dummyHelloRouteApp } from './dummy/hello'
77

@@ -13,7 +13,7 @@ export const apiApp = appFactory.createApp()
1313
.route('/auth', authApp)
1414

1515
// Some example routes
16-
.route('/dummy/convex', dummyConvexRouteApp)
16+
.route('/dummy/convexTasks', dummyConvexTasksRouteApp)
1717
.route('/dummy/hello', dummyHelloRouteApp)
1818
.route('/dummy/greet', dummyGreetRouteApp)
1919

Lines changed: 77 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { ClaimTokenType, FlagType } from '@kinde-oss/kinde-typescript-sdk'
1+
/**
2+
* This file contains routes and sample routes for possible APIs usecases with Kinde.
3+
*/
4+
5+
// import type { ClaimTokenType, FlagType } from '@kinde-oss/kinde-typescript-sdk'
26
import { appFactory } from '#src/helpers/factory.js'
37
import { getSessionManager } from '#src/helpers/kinde.js'
48
import { getKindeClient } from '#src/providers/auth/kinde-main.js'
@@ -9,6 +13,18 @@ export const authRoutesApp = appFactory.createApp()
913
return c.text('Good', 200)
1014
})
1115

16+
.get('/authState', async (c) => {
17+
const kindeClient = await getKindeClient()
18+
const sessionManager = getSessionManager(c)
19+
20+
const [profile, token] = await Promise.all([
21+
kindeClient.getUserProfile(sessionManager).catch(() => null),
22+
kindeClient.getToken(sessionManager).catch(() => null),
23+
])
24+
25+
return c.json({ profile, token })
26+
})
27+
1228
.get('/login', async (c) => {
1329
const kindeClient = await getKindeClient()
1430
const org_code = c.req.query('org_code')
@@ -49,92 +65,92 @@ export const authRoutesApp = appFactory.createApp()
4965
return c.redirect(logoutUrl.toString())
5066
})
5167

52-
.get('/isAuth', async (c) => {
53-
const kindeClient = await getKindeClient()
68+
// .get('/isAuth', async (c) => {
69+
// const kindeClient = await getKindeClient()
5470

55-
const isAuthenticated = await kindeClient.isAuthenticated(getSessionManager(c)) // Boolean: true or false
71+
// const isAuthenticated = await kindeClient.isAuthenticated(getSessionManager(c)) // Boolean: true or false
5672

57-
return c.json(isAuthenticated)
58-
})
73+
// return c.json(isAuthenticated)
74+
// })
5975

60-
.get('/profile', async (c) => {
61-
const kindeClient = await getKindeClient()
76+
// .get('/profile', async (c) => {
77+
// const kindeClient = await getKindeClient()
6278

63-
const profile = await kindeClient.getUserProfile(getSessionManager(c))
79+
// const profile = await kindeClient.getUserProfile(getSessionManager(c))
6480

65-
return c.json(profile)
66-
})
81+
// return c.json(profile)
82+
// })
6783

68-
.get('/createOrg', async (c) => {
69-
const kindeClient = await getKindeClient()
70-
const org_name = c.req.query('org_name')?.toString()
84+
// .get('/createOrg', async (c) => {
85+
// const kindeClient = await getKindeClient()
86+
// const org_name = c.req.query('org_name')?.toString()
7187

72-
const createUrl = await kindeClient.createOrg(getSessionManager(c), { org_name })
88+
// const createUrl = await kindeClient.createOrg(getSessionManager(c), { org_name })
7389

74-
return c.redirect(createUrl.toString())
75-
})
90+
// return c.redirect(createUrl.toString())
91+
// })
7692

77-
.get('/getOrg', async (c) => {
78-
const kindeClient = await getKindeClient()
93+
// .get('/getOrg', async (c) => {
94+
// const kindeClient = await getKindeClient()
7995

80-
const org = await kindeClient.getOrganization(getSessionManager(c))
96+
// const org = await kindeClient.getOrganization(getSessionManager(c))
8197

82-
return c.json(org)
83-
})
98+
// return c.json(org)
99+
// })
84100

85-
.get('/getOrgs', async (c) => {
86-
const kindeClient = await getKindeClient()
101+
// .get('/getOrgs', async (c) => {
102+
// const kindeClient = await getKindeClient()
87103

88-
const orgs = await kindeClient.getUserOrganizations(getSessionManager(c))
104+
// const orgs = await kindeClient.getUserOrganizations(getSessionManager(c))
89105

90-
return c.json(orgs)
91-
})
106+
// return c.json(orgs)
107+
// })
92108

93-
.get('/getPerm/:perm', async (c) => {
94-
const kindeClient = await getKindeClient()
109+
// .get('/getPerm/:perm', async (c) => {
110+
// const kindeClient = await getKindeClient()
95111

96-
const perm = await kindeClient.getPermission(getSessionManager(c), c.req.param('perm'))
112+
// const perm = await kindeClient.getPermission(getSessionManager(c), c.req.param('perm'))
97113

98-
return c.json(perm)
99-
})
114+
// return c.json(perm)
115+
// })
100116

101-
.get('/getPerms', async (c) => {
102-
const kindeClient = await getKindeClient()
117+
// .get('/getPerms', async (c) => {
118+
// const kindeClient = await getKindeClient()
103119

104-
const perms = await kindeClient.getPermissions(getSessionManager(c))
120+
// const perms = await kindeClient.getPermissions(getSessionManager(c))
105121

106-
return c.json(perms)
107-
})
122+
// return c.json(perms)
123+
// })
108124

109-
// Try: /api/auth/getClaim/aud, /api/auth/getClaim/email/id_token
110-
.get('/getClaim/:claim', async (c) => {
111-
const kindeClient = await getKindeClient()
112-
const type = (c.req.query('type') ?? 'access_token') as ClaimTokenType
125+
// // Try: /api/auth/getClaim/aud, /api/auth/getClaim/email/id_token
126+
// .get('/getClaim/:claim', async (c) => {
127+
// const kindeClient = await getKindeClient()
128+
// const type = (c.req.query('type') ?? 'access_token') as ClaimTokenType
113129

114-
if (!/^(?:access_token|id_token)$/.test(type))
115-
return c.text('Bad request: type', 400)
130+
// if (!/^(?:access_token|id_token)$/.test(type))
131+
// return c.text('Bad request: type', 400)
116132

117-
const claim = await kindeClient.getClaim(getSessionManager(c), c.req.param('claim'), type)
118-
return c.json(claim)
119-
})
133+
// const claim = await kindeClient.getClaim(getSessionManager(c), c.req.param('claim'), type)
134+
// return c.json(claim)
135+
// })
120136

121-
.get('/getFlag/:code', async (c) => {
122-
const kindeClient = await getKindeClient()
137+
// .get('/getFlag/:code', async (c) => {
138+
// const kindeClient = await getKindeClient()
123139

124-
const claim = await kindeClient.getFlag(
125-
getSessionManager(c),
126-
c.req.param('code'),
127-
c.req.query('default'),
128-
c.req.query('flagType') as keyof FlagType | undefined,
129-
)
140+
// const claim = await kindeClient.getFlag(
141+
// getSessionManager(c),
142+
// c.req.param('code'),
143+
// c.req.query('default'),
144+
// c.req.query('flagType') as keyof FlagType | undefined,
145+
// )
130146

131-
return c.json(claim)
132-
})
147+
// return c.json(claim)
148+
// })
133149

134-
.get('/getToken', async (c) => {
135-
const kindeClient = await getKindeClient()
150+
// .get('/getToken', async (c) => {
151+
// const kindeClient = await getKindeClient()
136152

137-
const accessToken = await kindeClient.getToken(getSessionManager(c))
153+
// const accessToken = await kindeClient.getToken(getSessionManager(c))
138154

139-
return c.text(accessToken)
140-
})
155+
// return c.text(accessToken)
156+
// })
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import { api } from 'backend-convex/convex/_generated/api'
55
import { describeRoute } from 'hono-openapi'
66
import { resolver } from 'hono-openapi/arktype'
77

8-
export const dummyConvexRouteApp = appFactory.createApp()
8+
export const dummyConvexTasksRouteApp = appFactory.createApp()
99
.get(
1010
'',
1111
describeRoute({
12-
description: 'Items from `tasks` table',
12+
description: 'Get items from `tasks` table',
1313
responses: {
1414
200: {
1515
description: 'The tasks list',

apps/frontend/app/app.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ defineOgImageComponent('NuxtSeo', {
88
colorMode: 'dark',
99
})
1010
11-
const { $init } = useNuxtApp()
11+
const { $init, $auth } = useNuxtApp()
1212
onMounted(async () => {
1313
await nextTick()
1414
$init.mounted = true
@@ -17,8 +17,13 @@ onMounted(async () => {
1717
// Init convex client if url configured
1818
const convexVueContext = inject<ConvexVueContext>('convex-vue')
1919
// Don't init if on server, see https://github.com/chris-visser/convex-vue/issues/6
20-
if (import.meta.client && convexVueContext?.options?.url)
20+
if (import.meta.client && convexVueContext?.options?.url) {
2121
convexVueContext.initClient(convexVueContext.options)
22+
// Also set auth hook for the client
23+
convexVueContext.clientRef.value?.setAuth(async () => {
24+
return $auth.token
25+
})
26+
}
2227
</script>
2328

2429
<template>

apps/frontend/app/components/ConvexIntegrationTest.vue

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const { $apiClient } = useNuxtApp()
66
const convexClient = useConvexClient()
77
88
const { data: tasks } = useConvexQuery(api.tasks.get)
9+
const { data: authInfo } = useConvexQuery(api.authInfo.get)
910
const { mutate: mutateAddTask } = useConvexMutation(api.tasks.add)
1011
1112
const taskInputRef = ref('')
@@ -15,21 +16,24 @@ async function addTask() {
1516
.then(() => { taskInputRef.value = '' })
1617
}
1718
18-
const isFetching = ref(false)
19-
async function testConvexViaBackendCTA() {
20-
isFetching.value = true
19+
const isFetchingTasks = ref(false)
20+
async function testConvexViaBackendTasksCTA() {
21+
isFetchingTasks.value = true
2122
22-
await hcParse($apiClient.api.dummy.convex.$get())
23+
await hcParse($apiClient.api.dummy.convexTasks.$get())
2324
.then(r => toast.add({ detail: r.map(t => t.text).join('\n') }))
2425
.catch(e => toast.add({ severity: 'error', detail: e.message }))
2526
26-
isFetching.value = false
27+
isFetchingTasks.value = false
2728
}
2829
</script>
2930

3031
<template>
31-
<div class="mb-4 text-xl">
32-
{{ $t('components.convexIntegrationTest.configuredUrl') }}: <code class="rounded bg-gray-100 px-1 py-0.5 text-base dark:bg-gray-800">{{ convexClient.client.url }}</code>
32+
<div class="mb-4 w-full text-xl">
33+
<div>
34+
{{ $t('components.convexIntegrationTest.configuredUrl') }}: <code class="rounded bg-gray-100 px-1 py-0.5 text-base dark:bg-gray-800">{{ convexClient.client.url }}</code>
35+
</div>
36+
<div>{{ $t('components.convexIntegrationTest.authInfo') }}: <pre class="max-w-full w-full overflow-x-auto rounded bg-black p-2 px-4 text-left text-xs text-white">{{ authInfo }}</pre></div>
3337
</div>
3438
<div class="max-w-md w-full flex flex-col gap-5">
3539
<IftaLabel class="w-full">
@@ -38,7 +42,7 @@ async function testConvexViaBackendCTA() {
3842
</IftaLabel>
3943
</div>
4044

41-
<div class="mt-6 max-w-md w-full">
45+
<div class="max-w-md w-full">
4246
<h3 class="mb-2 text-lg font-semibold">
4347
{{ $t('components.convexIntegrationTest.tasksList.title') }}
4448
</h3>
@@ -52,5 +56,7 @@ async function testConvexViaBackendCTA() {
5256
</p>
5357
</div>
5458

55-
<Button :loading="isFetching" class="mt-8" :label="$t('components.convexIntegrationTest.testBackendButton')" @pointerdown="testConvexViaBackendCTA()" />
59+
<div class="my-8 flex flex-col gap-4">
60+
<Button :loading="isFetchingTasks" :label="$t('components.convexIntegrationTest.testBackendButton.fetchTasks')" @pointerdown="testConvexViaBackendTasksCTA()" />
61+
</div>
5662
</template>

apps/frontend/app/plugins/auth.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import type { UserType } from '@kinde-oss/kinde-typescript-sdk'
22
import type { Reactive } from 'vue'
33

4-
export type AuthState = { loggedIn: true, user: UserType } | { loggedIn: false, user: null }
4+
export type AuthState = (
5+
{ loggedIn: true, user: UserType, token: string }
6+
| { loggedIn: false, user: null, token: null }
7+
)
58

69
// The current plugin targets SSG and CSR, if you use SSR, you need to converts it to useState and useAsyncData for optimized performance
710

11+
// Note: token is passed down for use 3rd party integrations like Convex, if you only use `frontend` and `backend`, you can remove it to be more secure.
12+
813
export default defineNuxtPlugin({
914
name: 'local-auth',
1015
parallel: true,
@@ -18,18 +23,21 @@ export default defineNuxtPlugin({
1823
const auth = reactive({
1924
loggedIn: false,
2025
user: null,
26+
token: null,
2127
}) as Reactive<AuthState>
2228

2329
async function refreshAuth() {
24-
const profile = await hcParse(authApi.profile.$get()).catch(() => null)
30+
const authState = await hcParse(authApi.authState.$get())
2531

26-
if (profile) {
32+
if (authState?.profile) {
2733
auth.loggedIn = true
28-
auth.user = profile
34+
auth.user = authState.profile
35+
auth.token = authState.token
2936
}
3037
else {
3138
auth.loggedIn = false
3239
auth.user = null
40+
auth.token = null
3341
}
3442

3543
// Refresh every 15 minutes

0 commit comments

Comments
 (0)