-
Notifications
You must be signed in to change notification settings - Fork 26
Introduce user abstraction (WIP) #173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
028da08
58fda3f
97c7543
6432a47
9997151
ada8df8
b11435e
0cd1a59
b52b437
c8ffdaa
03fae6a
4b74c2b
f6703b8
d6af947
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,100 @@ | ||||||||||||||||||||||||||||||||
| import type { CreateUser } from "oidc-spa/core"; | ||||||||||||||||||||||||||||||||
| import { z } from "zod"; | ||||||||||||||||||||||||||||||||
| import avatarFallbackSvgUrl from "./assets/avatarFallback.svg"; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // App-level user shape exposed by `useOidc()`. | ||||||||||||||||||||||||||||||||
| // You decide what an user should looks like! | ||||||||||||||||||||||||||||||||
| export type User = { | ||||||||||||||||||||||||||||||||
| id: string; | ||||||||||||||||||||||||||||||||
| username: string; | ||||||||||||||||||||||||||||||||
| displayName: string; | ||||||||||||||||||||||||||||||||
| email: string | undefined; | ||||||||||||||||||||||||||||||||
| avatarImgUrl: string; | ||||||||||||||||||||||||||||||||
| isRealmAdmin: boolean; | ||||||||||||||||||||||||||||||||
| userInfo: { | ||||||||||||||||||||||||||||||||
| sub: string; | ||||||||||||||||||||||||||||||||
| [claim: string]: unknown; | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| keycloakUserProfile?: import("oidc-spa/keycloak").KeycloakProfile; | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // The function that oidc-spa will call to create the user object, | ||||||||||||||||||||||||||||||||
| // gathering information from different sources depending of what you need. | ||||||||||||||||||||||||||||||||
| export const createUser: CreateUser<User> = async ({ | ||||||||||||||||||||||||||||||||
| decodedIdToken: decodedIdToken_generic, | ||||||||||||||||||||||||||||||||
| accessToken, | ||||||||||||||||||||||||||||||||
| fetchUserInfo, | ||||||||||||||||||||||||||||||||
| issuerUri | ||||||||||||||||||||||||||||||||
| }) => { | ||||||||||||||||||||||||||||||||
| /* ================= Possible source: ID token claims. ====================== */ | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const DecodedIdToken = z.object({ | ||||||||||||||||||||||||||||||||
| sub: z.string(), | ||||||||||||||||||||||||||||||||
| name: z.string(), | ||||||||||||||||||||||||||||||||
| picture: z.string().optional(), | ||||||||||||||||||||||||||||||||
| email: z.string().email().optional(), | ||||||||||||||||||||||||||||||||
| preferred_username: z.string().optional() | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const decodedIdToken = DecodedIdToken.parse(decodedIdToken_generic); | ||||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ls -la
git ls-files examples/tanstack-router-file-router/src/oidc.user.ts
sed -n '1,140p' examples/tanstack-router-file-router/src/oidc.user.tsRepository: keycloakify/oidc-spa Length of output: 4938 🏁 Script executed: #!/bin/bash
set -euo pipefail
nl -ba examples/tanstack-router-file-router/src/oidc.user.ts | sed -n '1,130p'Repository: keycloakify/oidc-spa Length of output: 107 🏁 Script executed: #!/bin/bash
set -euo pipefail
cat -n examples/tanstack-router-file-router/src/oidc.user.ts | sed -n '1,140p'Repository: keycloakify/oidc-spa Length of output: 4334 Don’t hard-require
Suggested fix const DecodedIdToken = z.object({
sub: z.string(),
- name: z.string(),
+ name: z.string().optional(),
picture: z.string().optional(),
email: z.string().email().optional(),
preferred_username: z.string().optional()
});
@@
- displayName: decodedIdToken.name,
+ displayName:
+ decodedIdToken.name ??
+ decodedIdToken.preferred_username ??
+ decodedIdToken.sub,🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /* ================== Possible source: access token claims. ================== */ | ||||||||||||||||||||||||||||||||
| // This is pragmatic, but not textbook OIDC: clients should usually | ||||||||||||||||||||||||||||||||
| // treat access tokens as opaque, and some providers do not issue JWTs. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const DecodedAccessToken = z.object({ | ||||||||||||||||||||||||||||||||
| realm_access: z.object({ roles: z.array(z.string()) }).optional() | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const { decodeJwt } = await import("oidc-spa/decode-jwt"); | ||||||||||||||||||||||||||||||||
| const { isKeycloak } = await import("oidc-spa/keycloak"); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const decodedAccessToken = !isKeycloak({ issuerUri }) | ||||||||||||||||||||||||||||||||
| ? undefined | ||||||||||||||||||||||||||||||||
| : DecodedAccessToken.parse(decodeJwt(accessToken)); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /* ================= Possible source: your own API. ========================= */ | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // const { fetchWithAuth } = await import("./oidc"); | ||||||||||||||||||||||||||||||||
| // const userFromApi = await fetchWithAuth("/api/user").then(r => r.json()); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /* ================= Possible source: the standard OIDC UserInfo endpoint. == */ | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const userInfo = await fetchUserInfo(); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /* ================= Possible source: provider-specific endpoints. ========== */ | ||||||||||||||||||||||||||||||||
| const { createKeycloakUtils } = await import("oidc-spa/keycloak"); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const keycloakUtils = isKeycloak({ issuerUri }) ? createKeycloakUtils({ issuerUri }) : undefined; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const keycloakUserProfile = await keycloakUtils?.fetchUserProfile({ accessToken }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
Comment on lines
+68
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate the target file
ls -la examples/tanstack-router-file-router/src/oidc.user.ts
# Show the relevant section around the reported lines
sed -n '1,140p' examples/tanstack-router-file-router/src/oidc.user.ts | nl -ba | sed -n '50,110p'
# Find where createUser() is defined/used in this example
rg -n "createUser\\b" examples/tanstack-router-file-router/src -S || true
# Find the Keycloak utils factory and fetchUserProfile definition
rg -n "isKeycloak\\b|createKeycloakUtils\\b|fetchUserProfile\\b" examples/tanstack-router-file-router/src -S || true
# Search entire repo for createKeycloakUtils/fetchUserProfile to understand behavior
rg -n "function\\s+createKeycloakUtils\\b|createKeycloakUtils\\b" -S . || true
rg -n "fetchUserProfile\\b" -S . || trueRepository: keycloakify/oidc-spa Length of output: 228 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="examples/tanstack-router-file-router/src/oidc.user.ts"
echo "== Target file =="
sed -n '1,140p' "$FILE" | awk '{printf "%d\t%s\n", NR,$0}' | sed -n '45,95p'
echo "== Search for createUser() usages in this example =="
rg -n "createUser\\b" examples/tanstack-router-file-router/src -S || true
echo "== Search for keycloak utils / fetchUserProfile in this example =="
rg -n "isKeycloak\\b|createKeycloakUtils\\b|fetchUserProfile\\b" examples/tanstack-router-file-router/src -S || true
echo "== Search repo for createKeycloakUtils / fetchUserProfile implementation =="
rg -n "createKeycloakUtils\\b" -S . || true
rg -n "fetchUserProfile\\b" -S . || trueRepository: keycloakify/oidc-spa Length of output: 12766 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect createUser implementation in the example
FILE_EX="examples/tanstack-router-file-router/src/oidc.user.ts"
echo "== createUser() around line ~1-110 =="
sed -n '1,120p' "$FILE_EX" | awk '{printf "%5d\t%s\n", NR,$0}'
# Inspect keycloakUtils.ts fetchUserProfile implementation
FILE_KC="src/keycloak/keycloakUtils.ts"
echo "== createKeycloakUtils() / fetchUserProfile around line ~160-270 =="
sed -n '160,270p' "$FILE_KC" | awk '{printf "%5d\t%s\n", NR+160,$0}'Repository: keycloakify/oidc-spa Length of output: 7895 Guard Keycloak
File: Suggested fix- const keycloakUserProfile = await keycloakUtils?.fetchUserProfile({ accessToken });
+ const keycloakUserProfile = await (async () => {
+ if (!keycloakUtils) {
+ return undefined;
+ }
+ try {
+ return await keycloakUtils.fetchUserProfile({ accessToken });
+ } catch {
+ return undefined;
+ }
+ })();📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| /* ================== Merging =============================================== */ | ||||||||||||||||||||||||||||||||
| // Merge whichever sources you decided to use into the single | ||||||||||||||||||||||||||||||||
| // `User` shape consumed by the rest of the app. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const user: User = { | ||||||||||||||||||||||||||||||||
| id: decodedIdToken.sub, | ||||||||||||||||||||||||||||||||
| username: decodedIdToken.preferred_username ?? decodedIdToken.sub, | ||||||||||||||||||||||||||||||||
| displayName: decodedIdToken.name, | ||||||||||||||||||||||||||||||||
| avatarImgUrl: decodedIdToken.picture || avatarFallbackSvgUrl, | ||||||||||||||||||||||||||||||||
| email: decodedIdToken.email, | ||||||||||||||||||||||||||||||||
| isRealmAdmin: decodedAccessToken?.realm_access?.roles.includes("realm-admin") ?? false, | ||||||||||||||||||||||||||||||||
| userInfo, | ||||||||||||||||||||||||||||||||
| keycloakUserProfile | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return user; | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // App-level user returned when the mock implementation is enabled. | ||||||||||||||||||||||||||||||||
| export const user_mock: User = { | ||||||||||||||||||||||||||||||||
| id: "mock-user", | ||||||||||||||||||||||||||||||||
| username: "john.doe", | ||||||||||||||||||||||||||||||||
| displayName: "John Doe", | ||||||||||||||||||||||||||||||||
| email: undefined, | ||||||||||||||||||||||||||||||||
| avatarImgUrl: avatarFallbackSvgUrl, | ||||||||||||||||||||||||||||||||
| isRealmAdmin: true, | ||||||||||||||||||||||||||||||||
| userInfo: { sub: "1234" }, | ||||||||||||||||||||||||||||||||
| keycloakUserProfile: undefined | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.