Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions packages/nuxt/src/runtime/components/ThunderIDRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {useState, useRuntimeConfig} from '#imports';
* - {@link I18nProvider} ← `preferences.i18n`
* - {@link ThemeProvider} ← `mode`
* - {@link FlowProvider}
* - {@link UserProvider} ← `profile`, `flattenedProfile`, `schemas`,
* - {@link UserProvider} ← `profile`, `flattenedProfile`, `userSchema`,
* `updateProfile`, `revalidateProfile`, `onUpdateProfile`
*
* The `THUNDERID_KEY` (config + auth state + actions) is still provided at the
Expand Down Expand Up @@ -130,12 +130,18 @@ const ThunderIDRoot: Component = defineComponent({
};

/**
* Re-fetch the full user profile from `/api/auth/user/profile`.
* Re-fetch the full user profile (and its attribute schema) from `/api/auth/user/profile`.
*/
const revalidateProfile = async (): Promise<void> => {
try {
const res: UserProfile = await $fetch<UserProfile>('/api/auth/user/profile');
if (res) userProfileState.value = res;
const res: (UserProfile & {userSchema?: Record<string, AttributeSchema> | null}) | null = await $fetch<
UserProfile & {userSchema?: Record<string, AttributeSchema> | null}
>('/api/auth/user/profile');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to move these paths to constants. @coderabbitai can you create an issue for this on https://github.com/thunder-id/thunderid

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh issue list \
  --repo thunder-id/thunderid \
  --state open \
  --search 'Nuxt API route paths constants in:title,body' \
  --limit 20 \
  --json number,title,url,body

Length of output: 343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

body=$(cat <<'EOF'
## Summary

Extract the Nuxt SDK API route paths into shared constants.

## Rationale

`ThunderIDRoot` uses route string literals for the profile endpoint. Shared constants reduce repeated route strings and prevent route updates from becoming inconsistent across Nuxt runtime modules.

## Affected areas

- `packages/nuxt/src/runtime/components/ThunderIDRoot.ts`
- Other Nuxt runtime components, plugins, middleware, and Nitro handlers that use the same `/api/auth/*` route paths

## Required changes

1. Define and export shared constants for Nuxt SDK API route paths.
2. Replace repeated route string literals with the shared constants.
3. Keep the constants in the appropriate Nuxt package layer. Do not duplicate them across runtime modules.
4. Preserve the current request methods and route behavior.

## Acceptance criteria

- Nuxt API route paths used by more than one runtime module resolve through shared constants.
- `ThunderIDRoot` uses the shared constants for profile fetch and profile update requests.
- No duplicate or inconsistent route path literals remain in the affected Nuxt runtime modules.
- The changes meet the repository ESLint and Prettier requirements.

## Backlinks

- Source pull request: https://github.com/thunder-id/javascript-sdks/pull/84
- Source review comment: https://github.com/thunder-id/javascript-sdks/pull/84#discussion_r3802082276
- Requested by: `@brionmario`
EOF
)

gh issue create \
  --repo thunder-id/thunderid \
  --title 'Extract Nuxt SDK API route paths into shared constants' \
  --body "$body" \
  --assignee brionmario

Length of output: 1811


@brionmario, created and assigned thunder-id/thunderid#5060: “Extract Nuxt SDK API route paths into shared constants.”

You are interacting with an AI system.

if (res) {
const {userSchema: fetchedSchema, ...profile} = res;
userProfileState.value = profile as UserProfile;
userSchemaState.value = fetchedSchema ?? null;
}
} catch {
// Non-fatal — profile stays stale until the next navigation.
}
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxt/src/runtime/components/user/UserDropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const UserDropdown: Component = defineComponent({
},
setup(props: {className: string}, {slots, emit}: {emit: any; slots: any}): () => VNode | VNode[] | null {
const {user, signOut} = useThunderID();
useUser();
const {revalidateProfile} = useUser();
const isProfileModalOpen: Ref<boolean> = ref(false);

return (): VNode | VNode[] | null =>
Expand All @@ -44,6 +44,7 @@ const UserDropdown: Component = defineComponent({
isProfileModalOpen: isProfileModalOpen.value,
onProfileClick: (): void => {
isProfileModalOpen.value = true;
void revalidateProfile();
emit('profileClick');
},
onProfileModalClose: (): void => {
Expand Down
5 changes: 3 additions & 2 deletions packages/nuxt/src/runtime/components/user/UserProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const UserProfile: Component = defineComponent({
}>,
{slots}: SetupContext,
): () => VNode {
const {flattenedProfile, schemas, updateProfile} = useUser();
const {flattenedProfile, profile, updateProfile, userSchema} = useUser();

return (): VNode =>
h(
Expand All @@ -55,9 +55,10 @@ const UserProfile: Component = defineComponent({
flattenedProfile: flattenedProfile?.value,
hideFields: props.hideFields,
onUpdate: updateProfile,
schemas: schemas?.value,
profile: profile?.value,
showFields: props.showFields,
title: props.title,
userSchema: userSchema?.value,
},
slots,
);
Expand Down
61 changes: 39 additions & 22 deletions packages/nuxt/src/runtime/server/routes/auth/user/profile.get.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2025 The ThunderID Authors
// SPDX-License-Identifier: Apache-2.0

import type {UserProfile} from '@thunderid/node';
import type {AttributeSchema, UserProfile} from '@thunderid/node';
import {defineEventHandler, createError} from 'h3';
import type {H3Event} from 'h3';
import ThunderIDNuxtClient from '../../../ThunderIDNuxtClient';
Expand All @@ -11,30 +11,47 @@ import {useRuntimeConfig} from '#imports';
/**
* GET /api/auth/user/profile
*
* Returns the full {@link UserProfile} (with `flattenedProfile`) for the authenticated user. Used by `ThunderIDRoot.revalidateProfile`
* to refresh client-side state after a profile update.
* Returns the full {@link UserProfile} (with `flattenedProfile`) plus the `users/me/meta`
* attribute schema for the authenticated user. Used by `ThunderIDRoot.revalidateProfile` to
* refresh client-side state — both after a profile update, and to self-heal cases where the
* SSR-seeded state was empty.
*
* Mirrors `getUserProfileAction` in the Next.js SDK.
*/
export default defineEventHandler(async (event: H3Event): Promise<UserProfile> => {
const config: ReturnType<typeof useRuntimeConfig> = useRuntimeConfig();
const sessionSecret: string | undefined = config.thunderid?.sessionSecret;
export default defineEventHandler(
async (event: H3Event): Promise<UserProfile & {userSchema: Record<string, AttributeSchema> | null}> => {
const config: ReturnType<typeof useRuntimeConfig> = useRuntimeConfig();
const sessionSecret: string | undefined = config.thunderid?.sessionSecret;

const session: Awaited<ReturnType<typeof verifyAndRehydrateSession>> = await verifyAndRehydrateSession(
event,
sessionSecret,
);
if (!session) {
throw createError({statusCode: 401, statusMessage: 'Unauthorized: Invalid or expired session.'});
}
const session: Awaited<ReturnType<typeof verifyAndRehydrateSession>> = await verifyAndRehydrateSession(
event,
sessionSecret,
);
if (!session) {
throw createError({statusCode: 401, statusMessage: 'Unauthorized: Invalid or expired session.'});
}

try {
const client: ThunderIDNuxtClient = ThunderIDNuxtClient.getInstance();
return await client.getUserProfile(session.sessionId);
} catch (err) {
throw createError({
statusCode: 500,
statusMessage: `Failed to retrieve user profile: ${err instanceof Error ? err.message : String(err)}`,
});
}
});

let userProfile: UserProfile;
try {
userProfile = await client.getUserProfile(session.sessionId);
} catch (err) {
throw createError({
statusCode: 500,
statusMessage: `Failed to retrieve user profile: ${err instanceof Error ? err.message : String(err)}`,
});
}

// Schema fetch failures shouldn't fail the whole response — the profile itself is still
// useful without it, same resilience the SSR plugin applies to this same pair of calls.
let userSchema: Record<string, AttributeSchema> | null = null;
try {
userSchema = await client.getUserSchema(session.sessionId);
} catch {
userSchema = null;
}

return {...userProfile, userSchema};
},
);
Loading