update keycloak signup / login
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -108,6 +108,9 @@ export function SignInMethodsContainer(props: {
|
||||
callback: props.paths.callback,
|
||||
returnPath: props.paths.returnPath,
|
||||
}}
|
||||
queryParams={{
|
||||
prompt: 'login',
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
</>
|
||||
|
||||
@@ -79,6 +79,9 @@ export function SignUpMethodsContainer(props: {
|
||||
callback: props.paths.callback,
|
||||
returnPath: props.paths.appHome,
|
||||
}}
|
||||
queryParams={{
|
||||
prompt: 'login',
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
</>
|
||||
|
||||
@@ -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!`;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user