Skip to content

Commit 1d1eafb

Browse files
committed
integrate users/me/meta endpoint into the Vue and Nuxt sdks
1 parent 2ef39d8 commit 1d1eafb

21 files changed

Lines changed: 695 additions & 367 deletions

packages/nuxt/src/module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export default defineNuxtModule<ThunderIDNuxtConfig>({
8888
applicationId: publicConfig.applicationId,
8989
baseUrl: publicConfig.baseUrl,
9090
clientId: publicConfig.clientId,
91+
endpoints: publicConfig.endpoints,
9192
platform: publicConfig.platform,
9293
preferences: publicConfig.preferences,
9394
scopes: publicConfig.scopes,
@@ -102,6 +103,7 @@ export default defineNuxtModule<ThunderIDNuxtConfig>({
102103
applicationId?: string;
103104
baseUrl: string;
104105
clientId: string;
106+
endpoints?: ThunderIDNuxtConfig['endpoints'];
105107
platform?: ThunderIDNuxtConfig['platform'];
106108
preferences: ThunderIDNuxtConfig['preferences'];
107109
scopes: string | string[];
@@ -286,6 +288,7 @@ declare module '@nuxt/schema' {
286288
applicationId?: string;
287289
baseUrl: string;
288290
clientId: string;
291+
endpoints?: ThunderIDNuxtConfig['endpoints'];
289292
platform?: ThunderIDNuxtConfig['platform'];
290293
preferences?: ThunderIDNuxtConfig['preferences'];
291294
scopes: string | string[];

packages/nuxt/src/runtime/components/ThunderIDRoot.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import {generateFlattenedUserProfile} from '@thunderid/browser';
5-
import type {UpdateMeProfileConfig, User, UserProfile} from '@thunderid/node';
5+
import type {AttributeSchema, UpdateMeProfileConfig, User, UserProfile} from '@thunderid/node';
66
import {FlowMetaProvider, FlowProvider, I18nProvider, ThemeProvider, UserProvider} from '@thunderid/vue';
77
import {defineComponent, h, type Component, type Ref, type SetupContext, type VNode} from 'vue';
88
import type {ThunderIDAuthState, ThunderIDNuxtConfig} from '../types';
9-
import {getAuthStateKey, getUserProfileStateKey} from '../utils/stateKeys';
9+
import {getAuthStateKey, getUserProfileStateKey, getUserSchemaStateKey} from '../utils/stateKeys';
1010
import {useState, useRuntimeConfig} from '#imports';
1111

1212
/**
@@ -55,6 +55,10 @@ const ThunderIDRoot: Component = defineComponent({
5555

5656
// ── Read SSR-hydrated state keys (seeded by the Nuxt plugin) ────────────
5757
const userProfileState: Ref<UserProfile | null> = useState<UserProfile | null>(getUserProfileStateKey(vendor));
58+
const userSchemaState: Ref<Record<string, AttributeSchema> | null> = useState<Record<
59+
string,
60+
AttributeSchema
61+
> | null>(getUserSchemaStateKey(vendor));
5862
// Used by onUpdateProfile to keep the top-level auth user claim in sync.
5963
const authState: Ref<ThunderIDAuthState> = useState<ThunderIDAuthState>(getAuthStateKey(vendor));
6064

@@ -176,6 +180,7 @@ const ThunderIDRoot: Component = defineComponent({
176180
profile: shouldFetchProfile ? userProfileState.value : null,
177181
revalidateProfile: shouldFetchProfile ? revalidateProfile : undefined,
178182
updateProfile: shouldFetchProfile ? updateProfile : undefined,
183+
userSchema: shouldFetchProfile ? userSchemaState.value : null,
179184
},
180185
{
181186
default: (): VNode | VNode[] | undefined => slots.default?.(),

packages/nuxt/src/runtime/plugins/thunderid.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33

44
import {getRedirectBasedSignUpUrl} from '@thunderid/browser';
55
import {VendorConstants} from '@thunderid/node';
6-
import type {UserProfile} from '@thunderid/node';
6+
import type {AttributeSchema, UserProfile} from '@thunderid/node';
77
import {ThunderIDPlugin, THUNDERID_KEY} from '@thunderid/vue';
88
import type {H3Event} from 'h3';
99
import {computed} from 'vue';
1010
import type {ComputedRef, Ref} from 'vue';
1111
import ThunderIDRoot from '../components/ThunderIDRoot';
1212
import type {ThunderIDAuthState, ThunderIDSSRData} from '../types';
13-
import {getAuthStateKey, getUserProfileStateKey} from '../utils/stateKeys';
13+
import {getAuthStateKey, getUserProfileStateKey, getUserSchemaStateKey} from '../utils/stateKeys';
1414
import type {NuxtApp} from '#app';
1515
import {defineNuxtPlugin, useState, useRequestEvent, useRuntimeConfig, navigateTo} from '#app';
1616

@@ -88,6 +88,10 @@ export default defineNuxtPlugin((nuxtApp: NuxtApp) => {
8888
getUserProfileStateKey(vendor),
8989
() => null,
9090
);
91+
const userSchemaState: Ref<Record<string, AttributeSchema> | null> = useState<Record<string, AttributeSchema> | null>(
92+
getUserSchemaStateKey(vendor),
93+
() => null,
94+
);
9195

9296
if (import.meta.server) {
9397
const event: H3Event | undefined = useRequestEvent();
@@ -101,6 +105,7 @@ export default defineNuxtPlugin((nuxtApp: NuxtApp) => {
101105
user: ssr.user,
102106
};
103107
userProfileState.value = ssr.userProfile;
108+
userSchemaState.value = ssr.userSchema ?? null;
104109
} else {
105110
// Backwards-compat: fall back to the legacy context shape (pre-Step-2 plugin).
106111
const ssrContext: {isSignedIn?: boolean; session?: {sub?: string}} | undefined = (

packages/nuxt/src/runtime/server/ThunderIDNuxtClient.ts

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33

44
import {
55
ThunderIDNodeClient,
6+
ThunderIDRuntimeError,
7+
extractUserClaimsFromIdToken,
8+
generateFlattenedUserProfile,
9+
getUsersMe,
10+
getUsersMeMeta,
11+
resolveResourceEndpoint,
12+
updateMeProfile,
13+
type AttributeSchema,
614
type AuthClientConfig,
715
type IdToken,
816
type Storage,
@@ -44,6 +52,7 @@ class ThunderIDNuxtClient extends ThunderIDNodeClient<ThunderIDNuxtConfig> {
4452
clientId: config.clientId!,
4553
clientSecret: config.clientSecret || undefined,
4654
enablePKCE: true,
55+
endpoints: config.endpoints,
4756
scopes: config.scopes || ['openid', 'profile'],
4857
tokenRequest: config.tokenRequest,
4958
} as AuthClientConfig<ThunderIDNuxtConfig>;
@@ -124,8 +133,23 @@ class ThunderIDNuxtClient extends ThunderIDNodeClient<ThunderIDNuxtConfig> {
124133
return (configData?.afterSignOutUrl as string) || (configData?.afterSignInUrl as string) || '/';
125134
}
126135

127-
override getUser(sessionId?: string): Promise<User> {
128-
return super.getUser(sessionId);
136+
override async getUser(sessionId?: string): Promise<User> {
137+
try {
138+
const configData: AuthClientConfig<ThunderIDNuxtConfig> = await this.getStorageManager().getConfigData();
139+
const baseUrl: string | undefined = configData?.baseUrl;
140+
141+
const profile: User = await getUsersMe({
142+
baseUrl,
143+
url: resolveResourceEndpoint('usersMe', configData),
144+
headers: {
145+
Authorization: `Bearer ${await this.getAccessToken(sessionId)}`,
146+
},
147+
});
148+
149+
return profile;
150+
} catch (error) {
151+
return await super.getUser(sessionId);
152+
}
129153
}
130154

131155
override getAccessToken(sessionId?: string): Promise<string> {
@@ -144,13 +168,67 @@ class ThunderIDNuxtClient extends ThunderIDNodeClient<ThunderIDNuxtConfig> {
144168
return super.exchangeToken(config, sessionId) as unknown as Promise<TokenResponse | Response>;
145169
}
146170

147-
override async getUserProfile(sessionId: string): Promise<UserProfile> {
148-
const user: User = await this.getUser(sessionId);
149-
return {flattenedProfile: user, profile: user};
171+
override async getUserProfile(sessionId?: string): Promise<UserProfile> {
172+
try {
173+
const configData: AuthClientConfig<ThunderIDNuxtConfig> = await this.getStorageManager().getConfigData();
174+
const baseUrl: string | undefined = configData?.baseUrl;
175+
176+
const profile: User = await getUsersMe({
177+
baseUrl,
178+
url: resolveResourceEndpoint('usersMe', configData),
179+
headers: {
180+
Authorization: `Bearer ${await this.getAccessToken(sessionId)}`,
181+
},
182+
});
183+
184+
return {
185+
flattenedProfile: generateFlattenedUserProfile(profile),
186+
profile,
187+
};
188+
} catch (error) {
189+
return {
190+
flattenedProfile: extractUserClaimsFromIdToken(await super.getDecodedIdToken(sessionId)),
191+
profile: extractUserClaimsFromIdToken(await super.getDecodedIdToken(sessionId)),
192+
};
193+
}
194+
}
195+
196+
override async updateUserProfile(payload: any, sessionId?: string): Promise<User> {
197+
try {
198+
const configData: AuthClientConfig<ThunderIDNuxtConfig> = await this.getStorageManager().getConfigData();
199+
const baseUrl: string | undefined = configData?.baseUrl;
200+
201+
return updateMeProfile({
202+
baseUrl,
203+
url: resolveResourceEndpoint('usersMe', configData),
204+
headers: {
205+
Authorization: `Bearer ${await this.getAccessToken(sessionId)}`,
206+
},
207+
payload: payload?.payload ?? payload,
208+
});
209+
} catch (error) {
210+
throw new ThunderIDRuntimeError(
211+
`Failed to update user profile: ${error instanceof Error ? error.message : String(error)}`,
212+
'ThunderIDNuxtClient-UpdateProfileError-001',
213+
'nuxt',
214+
'An error occurred while updating the user profile. Please check your configuration and network connection.',
215+
);
216+
}
150217
}
151218

152-
override async updateUserProfile(config: UpdateMeProfileConfig, sessionId: string): Promise<User> {
153-
throw new Error('Profile updates are not supported for the ThunderID platform.');
219+
async getUserSchema(sessionId?: string): Promise<Record<string, AttributeSchema> | null> {
220+
const configData: AuthClientConfig<ThunderIDNuxtConfig> = await this.getStorageManager().getConfigData();
221+
const baseUrl: string | undefined = configData?.baseUrl;
222+
223+
const metaRes = await getUsersMeMeta({
224+
baseUrl,
225+
url: resolveResourceEndpoint('usersMeMeta', configData),
226+
headers: {
227+
Authorization: `Bearer ${await this.getAccessToken(sessionId)}`,
228+
},
229+
});
230+
231+
return metaRes?.schema ?? null;
154232
}
155233

156234
public override getStorageManager(): any {

packages/nuxt/src/runtime/server/plugins/thunderid-ssr.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export default defineNitroPlugin((nitro: {hooks: {hook: Function}}) => {
8585
baseUrl: publicConfig.baseUrl,
8686
clientId: publicConfig.clientId,
8787
clientSecret: privateConfig?.clientSecret || undefined,
88+
endpoints: publicConfig.endpoints,
8889
platform: publicConfig.platform,
8990
scopes: publicConfig.scopes || ['openid', 'profile'],
9091
tokenRequest: publicConfig.tokenRequest,
@@ -144,12 +145,15 @@ export default defineNitroPlugin((nitro: {hooks: {hook: Function}}) => {
144145
// ── 4. Parallel SSR data fetches (gated by preferences) ───────────────
145146
const shouldFetchProfile: boolean = prefs?.user?.fetchUserProfile !== false;
146147

147-
const [userResult, userProfileResult] = await Promise.allSettled([
148+
const [userResult, userProfileResult, userSchemaResult] = await Promise.allSettled([
148149
// Always fetch the basic user object (needed for ThunderIDAuthState.user)
149150
client.getUser(session.sessionId),
150151

151152
// User profile (flattened)
152153
shouldFetchProfile ? client.getUserProfile(session.sessionId) : Promise.resolve(null),
154+
155+
// User schema metadata from /users/me/meta
156+
shouldFetchProfile ? client.getUserSchema(session.sessionId) : Promise.resolve(null),
153157
]);
154158

155159
if (userResult.status === 'rejected') {
@@ -158,6 +162,9 @@ export default defineNitroPlugin((nitro: {hooks: {hook: Function}}) => {
158162
if (userProfileResult.status === 'rejected') {
159163
log.warn('Failed to fetch user profile:', userProfileResult.reason);
160164
}
165+
if (userSchemaResult.status === 'rejected') {
166+
log.warn('Failed to fetch user schema:', userSchemaResult.reason);
167+
}
161168

162169
// ── 5. Write to event context ──────────────────────────────────────────
163170
const ssrData: ThunderIDSSRData = {
@@ -166,6 +173,7 @@ export default defineNitroPlugin((nitro: {hooks: {hook: Function}}) => {
166173
session,
167174
user: userResult.status === 'fulfilled' ? userResult.value : null,
168175
userProfile: userProfileResult.status === 'fulfilled' ? userProfileResult.value : null,
176+
userSchema: userSchemaResult.status === 'fulfilled' ? userSchemaResult.value : null,
169177
};
170178

171179
const eventContext: Record<string, unknown> = event.context;

packages/nuxt/src/runtime/server/utils/session.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,15 @@ export async function verifyTempSessionToken(
126126
/**
127127
* Session cookie name.
128128
*/
129-
export function getSessionCookieName(): string {
130-
return CookieConfig.SESSION_COOKIE_NAME;
129+
export function getSessionCookieName(vendor?: string): string {
130+
return CookieConfig.getSessionCookieName(vendor);
131131
}
132132

133133
/**
134134
* Temp session cookie name.
135135
*/
136-
export function getTempSessionCookieName(): string {
137-
return CookieConfig.TEMP_SESSION_COOKIE_NAME;
136+
export function getTempSessionCookieName(vendor?: string): string {
137+
return CookieConfig.getTempSessionCookieName(vendor);
138138
}
139139

140140
/**

packages/nuxt/src/runtime/types.ts

Lines changed: 14 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,25 @@
11
// Copyright 2025 The ThunderID Authors
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import type {I18nPreferences, TokenEndpointAuthMethod, User, UserProfile} from '@thunderid/node';
4+
import type {
5+
AttributeSchema,
6+
AuthClientConfig,
7+
I18nPreferences,
8+
TokenEndpointAuthMethod,
9+
User,
10+
UserProfile,
11+
} from '@thunderid/node';
512
import type {JWTPayload} from 'jose';
613

714
/**
815
* Configuration for the ThunderID Nuxt module.
16+
* Extends `AuthClientConfig` from `@thunderid/node` for 1:1 SDK parity.
917
*/
10-
export interface ThunderIDNuxtConfig {
11-
/** URL to redirect to after sign-in (default: '/') */
12-
afterSignInUrl?: string;
13-
/** URL to redirect to after sign-out (default: '/') */
14-
afterSignOutUrl?: string;
15-
/**
16-
* ThunderID application id (`spId`) — appended to the redirect-based sign-up
17-
* URL when present. Mirrors `applicationId` in the React/Next.js SDKs.
18-
*/
19-
applicationId?: string;
20-
/** Base URL of the ThunderID org tenant (e.g. https://localhost:8090) */
21-
baseUrl?: string;
22-
/** OAuth2 Client ID */
23-
clientId?: string;
24-
/** OAuth2 Client Secret (server-only, use THUNDERID_CLIENT_SECRET env var) */
25-
clientSecret?: string;
26-
/**
27-
* Feature-gating preferences that control which server-side data fetches
28-
* the Nitro plugin performs on every SSR request.
29-
*/
30-
preferences?: {
31-
/** i18n configuration forwarded to `I18nProvider`. */
32-
i18n?: I18nPreferences;
33-
theme?: {
34-
/**
35-
* Theme mode forwarded to the Vue SDK's `ThemeProvider`.
36-
* - `'light'` (default) | `'dark'`: Fixed color scheme. Toggle at runtime with `useTheme().toggleTheme()`.
37-
* - `'system'`: Follows the OS `prefers-color-scheme`.
38-
* - `'class'`: Reads a CSS class on `<html>` (works well with Tailwind dark-mode).
39-
* - `'branding'`: Follows the active theme from the tenant's branding preference.
40-
*/
41-
mode?: 'light' | 'dark' | 'system' | 'class' | 'branding';
42-
};
43-
user?: {
44-
/** Whether to fetch the user profile during SSR (default: true). */
45-
fetchUserProfile?: boolean;
46-
};
47-
};
48-
/** OAuth2 scopes to request */
49-
scopes?: string | string[];
18+
export interface ThunderIDNuxtConfig extends AuthClientConfig {
5019
/** Secret for signing session JWTs (use THUNDERID_SESSION_SECRET env var) */
5120
sessionSecret?: string;
52-
/**
53-
* Optional override for the redirect-based sign-in URL. Reserved for
54-
* parity with the React/Next.js SDKs; not currently used by the redirect
55-
* flow (which goes through `/api/auth/signin`).
56-
*/
57-
signInUrl?: string;
58-
/**
59-
* Optional override for the redirect-based sign-up URL. When set,
60-
* `<ThunderIDSignUpButton>` and `useThunderID().signUp()` (no-arg) navigate
61-
* here instead of deriving the URL from `baseUrl`/`clientId`.
62-
*/
63-
signUpUrl?: string;
64-
/**
65-
* Configuration for the token endpoint request.
66-
*/
67-
tokenRequest?: {
68-
/**
69-
* OAuth 2.0 client authentication method used at the token endpoint.
70-
* Defaults to `client_secret_basic` for ThunderIDV2 and `client_secret_post`
71-
* for all other platforms when not specified.
72-
*/
73-
authMethod?: TokenEndpointAuthMethod;
74-
};
21+
/** Platform identifier */
22+
platform?: any;
7523
/**
7624
* Vendor/brand namespace used to prefix Nuxt `useState` keys, the
7725
* `event.context` namespace, and other server-side identifiers.
@@ -133,6 +81,8 @@ export interface ThunderIDSSRData {
13381
user: User | null;
13482
/** Flattened user profile + raw profile (null when `preferences.user.fetchUserProfile` is false). */
13583
userProfile: UserProfile | null;
84+
/** User schema metadata from /users/me/meta (null when `preferences.user.fetchUserProfile` is false). */
85+
userSchema?: Record<string, AttributeSchema> | null;
13686
}
13787

13888
/**

0 commit comments

Comments
 (0)