update keycloak signup / login

This commit is contained in:
2025-09-08 01:02:10 +03:00
parent 7815a1c011
commit f01829de96
25 changed files with 501 additions and 239 deletions

View File

@@ -79,15 +79,9 @@ export function PersonalAccountDropdown({
}) {
const { data: personalAccountData } = usePersonalAccountData(user.id);
const signedInAsLabel = useMemo(() => {
const email = user?.email ?? undefined;
const phone = user?.phone ?? undefined;
return email ?? phone;
}, [user]);
const displayName =
personalAccountData?.name ?? account?.name ?? user?.email ?? '';
const { name, last_name } = personalAccountData ?? {};
const firstNameLabel = toTitleCase(name) ?? '-';
const fullNameLabel = name && last_name ? toTitleCase(`${name} ${last_name}`) : '-';
const hasTotpFactor = useMemo(() => {
const factors = user?.factors ?? [];
@@ -128,7 +122,7 @@ export function PersonalAccountDropdown({
<ProfileAvatar
className={'rounded-md'}
fallbackClassName={'rounded-md border'}
displayName={displayName ?? user?.email ?? ''}
displayName={firstNameLabel}
pictureUrl={personalAccountData?.picture_url}
/>
@@ -142,7 +136,7 @@ export function PersonalAccountDropdown({
data-test={'account-dropdown-display-name'}
className={'truncate text-sm'}
>
{toTitleCase(displayName)}
{firstNameLabel}
</span>
</div>
@@ -164,7 +158,7 @@ export function PersonalAccountDropdown({
</div>
<div>
<span className={'block truncate'}>{signedInAsLabel}</span>
<span className={'block truncate'}>{fullNameLabel}</span>
</div>
</div>
</DropdownMenuItem>

View File

@@ -48,6 +48,41 @@ class AccountsApi {
return data;
}
/**
* @name getPersonalAccountByUserId
* @description Get the personal account data for the given user ID.
* @param userId
*/
async getPersonalAccountByUserId(userId: string): Promise<AccountWithParams> {
const { data, error } = await this.client
.schema('medreport')
.from('accounts')
.select(
'*, accountParams: account_params (weight, height, isSmoker:is_smoker)',
)
.eq('primary_owner_user_id', userId)
.eq('is_personal_account', true)
.single();
if (error) {
throw error;
}
const { personal_code, ...rest } = data;
return {
...rest,
personal_code: (() => {
if (!personal_code) {
return null;
}
if (personal_code.toLowerCase().startsWith('ee')) {
return personal_code.substring(2);
}
return personal_code;
})(),
};
}
/**
* @name getAccountWorkspace
* @description Get the account workspace data.

View File

@@ -25,8 +25,8 @@ import { AuthProviderButton } from './auth-provider-button';
* @see https://supabase.com/docs/guides/auth/social-login
*/
const OAUTH_SCOPES: Partial<Record<Provider, string>> = {
azure: 'email',
keycloak: 'openid',
// azure: 'email',
// keycloak: 'openid',
// add your OAuth providers here
};
@@ -88,10 +88,12 @@ export const OauthProviders: React.FC<{
queryParams.set('invite_token', props.inviteToken);
}
const redirectPath = [
props.paths.callback,
queryParams.toString(),
].join('?');
// signicat/keycloak will not allow redirect-uri with changing query params
const INCLUDE_QUERY_PARAMS = false as boolean;
const redirectPath = INCLUDE_QUERY_PARAMS
? [props.paths.callback, queryParams.toString()].join('?')
: props.paths.callback;
const redirectTo = [origin, redirectPath].join('');
const scopes = OAUTH_SCOPES[provider] ?? undefined;
@@ -102,6 +104,7 @@ export const OauthProviders: React.FC<{
redirectTo,
queryParams: props.queryParams,
scopes,
// skipBrowserRedirect: false,
},
} satisfies SignInWithOAuthCredentials;

View File

@@ -108,6 +108,9 @@ export function SignInMethodsContainer(props: {
callback: props.paths.callback,
returnPath: props.paths.returnPath,
}}
queryParams={{
prompt: 'login',
}}
/>
</If>
</>

View File

@@ -79,6 +79,9 @@ export function SignUpMethodsContainer(props: {
callback: props.paths.callback,
returnPath: props.paths.appHome,
}}
queryParams={{
prompt: 'login',
}}
/>
</If>
</>

View File

@@ -4,7 +4,6 @@ import { sdk } from "@lib/config"
import medusaError from "@lib/util/medusa-error"
import { HttpTypes } from "@medusajs/types"
import { revalidateTag } from "next/cache"
import { redirect } from "next/navigation"
import {
getAuthHeaders,
getCacheOptions,
@@ -127,7 +126,7 @@ export async function login(_currentState: unknown, formData: FormData) {
}
}
export async function signout(countryCode?: string, shouldRedirect = true) {
export async function medusaLogout(countryCode = 'ee') {
await sdk.auth.logout()
await removeAuthToken()
@@ -139,10 +138,6 @@ export async function signout(countryCode?: string, shouldRedirect = true) {
const cartCacheTag = await getCacheTag("carts")
revalidateTag(cartCacheTag)
if (shouldRedirect) {
redirect(`/${countryCode!}/account`)
}
}
export async function transferCart() {
@@ -262,72 +257,110 @@ export const updateCustomerAddress = async (
})
}
export async function medusaLoginOrRegister(credentials: {
email: string
password?: string
}) {
const { email, password } = credentials;
async function medusaLogin(email: string, password: string) {
const token = await sdk.auth.login("customer", "emailpass", { email, password });
await setAuthToken(token as string);
try {
const token = await sdk.auth.login("customer", "emailpass", {
email,
password,
await transferCart();
} catch (e) {
console.error("Failed to transfer cart", e);
}
const customer = await retrieveCustomer();
if (!customer) {
throw new Error("Customer not found for active session");
}
return customer.id;
}
async function medusaRegister({
email,
password,
name,
lastName,
}: {
email: string;
password: string;
name: string | undefined;
lastName: string | undefined;
}) {
console.info(`Creating new Medusa account for Keycloak user with email=${email}`);
const registerToken = await sdk.auth.register("customer", "emailpass", { email, password });
await setAuthToken(registerToken);
console.info(`Creating new Medusa customer profile for Keycloak user with email=${email} and name=${name} and lastName=${lastName}`);
await sdk.store.customer.create(
{ email, first_name: name, last_name: lastName },
{},
{
...(await getAuthHeaders()),
});
await setAuthToken(token as string);
}
try {
await transferCart();
} catch (e) {
console.error("Failed to transfer cart", e);
export async function medusaLoginOrRegister(credentials: {
email: string
supabaseUserId?: string
name?: string,
lastName?: string,
} & ({ isDevPasswordLogin: true; password: string } | { isDevPasswordLogin?: false; password?: undefined })) {
const { email, supabaseUserId, name, lastName } = credentials;
const password = await (async () => {
if (credentials.isDevPasswordLogin) {
return credentials.password;
}
const customerCacheTag = await getCacheTag("customers");
revalidateTag(customerCacheTag);
return generateDeterministicPassword(email, supabaseUserId);
})();
try {
return await medusaLogin(email, password);
} catch (loginError) {
console.error("Failed to login customer, attempting to register", loginError);
const customer = await retrieveCustomer();
if (!customer) {
throw new Error("Customer not found");
}
return customer.id;
} catch (error) {
console.error("Failed to login customer, attempting to register", error);
try {
const registerToken = await sdk.auth.register("customer", "emailpass", {
email: email,
password: password,
})
await setAuthToken(registerToken as string);
const headers = {
...(await getAuthHeaders()),
};
await sdk.store.customer.create({ email }, {}, headers);
const loginToken = await sdk.auth.login("customer", "emailpass", {
email,
password,
});
await setAuthToken(loginToken as string);
const customerCacheTag = await getCacheTag("customers");
revalidateTag(customerCacheTag);
try {
await transferCart();
} catch (e) {
console.error("Failed to transfer cart", e);
}
const customer = await retrieveCustomer();
if (!customer) {
throw new Error("Customer not found");
}
return customer.id;
await medusaRegister({ email, password, name, lastName });
return await medusaLogin(email, password);
} catch (registerError) {
console.error("Failed to create Medusa account for user with email=${email}", registerError);
throw medusaError(registerError);
}
}
}
/**
* Generate a deterministic password based on user identifier
* This ensures the same user always gets the same password for Medusa
*/
async function generateDeterministicPassword(email: string, userId?: string): Promise<string> {
// Use the user ID or email as the base for deterministic generation
const baseString = userId || email;
const secret = process.env.MEDUSA_PASSWORD_SECRET!;
// Create a deterministic password using HMAC
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
const messageData = encoder.encode(baseString);
// Import key for HMAC
const key = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
// Generate HMAC
const signature = await crypto.subtle.sign('HMAC', key, messageData);
// Convert to base64 and make it a valid password
const hashArray = Array.from(new Uint8Array(signature));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
// Take first 24 characters and add some complexity
const basePassword = hashHex.substring(0, 24);
// Add some required complexity for Medusa (uppercase, lowercase, numbers, symbols)
return `Mk${basePassword}9!`;
}

View File

@@ -10,7 +10,7 @@ import MapPin from "@modules/common/icons/map-pin"
import Package from "@modules/common/icons/package"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { HttpTypes } from "@medusajs/types"
import { signout } from "@lib/data/customer"
import { medusaLogout } from "@lib/data/customer"
const AccountNav = ({
customer,
@@ -21,7 +21,7 @@ const AccountNav = ({
const { countryCode } = useParams() as { countryCode: string }
const handleLogout = async () => {
await signout(countryCode)
await medusaLogout(countryCode)
}
return (