diff --git a/apps/rpc/src/modules/user/user-service.ts b/apps/rpc/src/modules/user/user-service.ts index 76535c79aa..d931157cd8 100644 --- a/apps/rpc/src/modules/user/user-service.ts +++ b/apps/rpc/src/modules/user/user-service.ts @@ -37,7 +37,12 @@ import { import type { UserRepository } from "./user-repository" import { mergeUsers } from "./user-merging" import type { GroupRepository } from "../group/group-repository" -import type { Auth0Connection } from "./user" +import { + Auth0UserProfileAppMetadataSchema, + Auth0UserProfileUserMetadataSchema, + type Auth0Connection, + type Auth0UserProfile, +} from "./user" export interface UserService { register(handle: DBHandle, subject: string): Promise @@ -170,6 +175,29 @@ export function getUserService( ): UserService { const logger = getLogger("user-service") + function parseAuth0ProfileMetadata(auth0User: Auth0UserProfile) { + const appMetadataResult = Auth0UserProfileAppMetadataSchema.safeParse(auth0User.app_metadata ?? {}) + if (!appMetadataResult.success) { + logger.error("Failed to parse Auth0 app metadata for User(ID=%s): %o", auth0User.user_id, appMetadataResult.error) + } + + const userMetadataResult = Auth0UserProfileUserMetadataSchema.safeParse(auth0User.user_metadata ?? {}) + if (!userMetadataResult.success) { + logger.error( + "Failed to parse Auth0 user metadata for User(ID=%s): %o", + auth0User.user_id, + userMetadataResult.error + ) + } + + return { + appMetadata: appMetadataResult.success ? appMetadataResult.data : {}, + userMetadata: userMetadataResult.success ? userMetadataResult.data : {}, + } + } + + type Auth0ProfileMetadata = ReturnType + async function findApplicableMembership( studyProgrammes: NTNUGroup[], studySpecializations: NTNUGroup[], @@ -368,24 +396,14 @@ export function getUserService( await userRepository.register(handle, userId) - const requestedSlug = UserWriteSchema.shape.username - .catch(crypto.randomUUID()) - .parse(response.data.app_metadata?.username) - - // Profile slugs are unique, so if somebody has already the requested slug as their profile slug, we change the - // requested slug to a new random UUID for now. The user can always update this later. - const match = await this.findByUsername(handle, requestedSlug) - const slug = match !== null ? crypto.randomUUID() : requestedSlug - const profile: UserWrite = { - username: slug, + username: crypto.randomUUID(), name: response.data.name, email: response.data.email, imageUrl: response.data.picture, - biography: response.data.app_metadata?.biography || null, - phone: response.data.app_metadata?.phone || null, - // This field was called `allergies` in OnlineWeb 4, but today it's called `dietaryRestrictions`. - dietaryRestrictions: response.data.app_metadata?.allergies || null, + biography: null, + phone: null, + dietaryRestrictions: null, gender: GenderSchema.enum.UNKNOWN, workspaceUserId: null, } diff --git a/apps/rpc/src/modules/user/user.ts b/apps/rpc/src/modules/user/user.ts index 356804600d..f5abd35814 100644 --- a/apps/rpc/src/modules/user/user.ts +++ b/apps/rpc/src/modules/user/user.ts @@ -1,4 +1,4 @@ -import type { GetUsers200ResponseOneOfInnerIdentitiesInner } from "auth0" +import type { GetUsers200ResponseOneOfInnerIdentitiesInner, ManagementClient } from "auth0" import { z } from "zod" export const Auth0ConnectionSchema = z.object({ @@ -8,3 +8,30 @@ export const Auth0ConnectionSchema = z.object({ }) export type Auth0Connection = z.infer + +export type Auth0UserProfile = Awaited>["data"] + +export const Auth0UserProfileUserMetadataSchema = z + .object({ + // The name a user entered when registering (since April 2026). Used for making sure we don't replace the user's + // name with the Feide name if an admin has manually updated it. + full_name: z.string(), + }) + .partial() + .passthrough() + +export type Auth0UserProfileUserMetadata = z.infer + +// Old users from OnlineWeb 4 (previous version of the website) may have some metadata used for the migration into +// Auth0. Only the fields defined here should still be in active use. +export const Auth0UserProfileAppMetadataSchema = z + .object({ + // The user's full name as provided by Feide. Used for replacing user-entered names. + feide_full_name: z.string(), + // Immutable copy of user metadata field `full_name`. Semantically, user metadata is editable by the user. + initial_full_name: z.string(), + }) + .partial() + .passthrough() + +export type Auth0UserProfileAppMetadata = z.infer