Fix business tenant binding and update password on cross-site signup.
Resolve the active business from the host domain so multi-business staff no longer load another tenant’s CMS data, and replace the Meshkee password when linking an existing account to a new site. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
193c63bfd2
commit
89269dff04
@@ -50,26 +50,26 @@ function canAccessBusinessDashboard(user: AuthUser) {
|
|||||||
|
|
||||||
async function prepareBusinessContext(user: AuthUser) {
|
async function prepareBusinessContext(user: AuthUser) {
|
||||||
const domain = getBusinessDomain()
|
const domain = getBusinessDomain()
|
||||||
|
// Always resolve the host → business id. Never fall back to businesses[0]:
|
||||||
if (isSuperAdminUser(user)) {
|
// slug ≠ first DNS label (e.g. arjmand-pharmacy vs arjmandpharmacy.com) and
|
||||||
|
// multi-business staff would otherwise load the wrong tenant's CMS data.
|
||||||
const tenant = await resolveTenantByDomain(domain)
|
const tenant = await resolveTenantByDomain(domain)
|
||||||
setActiveBusinessById(String(tenant.id), tenant.domain)
|
setActiveBusinessById(String(tenant.id), tenant.domain)
|
||||||
|
|
||||||
|
if (isSuperAdminUser(user)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const membership =
|
const membership = user.businesses.find(
|
||||||
user.businesses.find((business) => business.slug === tenantSlugFromDomain(domain)) ??
|
(business) => String(business.id) === String(tenant.id),
|
||||||
user.businesses[0]
|
)
|
||||||
|
|
||||||
if (!membership) {
|
if (!membership) {
|
||||||
throw new ApiError('No business membership found for this account.', 403)
|
throw new ApiError(
|
||||||
|
'This account is not a staff member of this business.',
|
||||||
|
403,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveBusinessById(String(membership.id), domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
function tenantSlugFromDomain(domain: string) {
|
|
||||||
return domain.split('.')[0]?.toLowerCase() ?? domain
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assertBusinessAccess(user: AuthUser) {
|
async function assertBusinessAccess(user: AuthUser) {
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import { getBusinessDomain } from './config'
|
|||||||
const ACTIVE_BUSINESS_ID_KEY = 'meshkee_active_business_id'
|
const ACTIVE_BUSINESS_ID_KEY = 'meshkee_active_business_id'
|
||||||
const ACTIVE_BUSINESS_DOMAIN_KEY = 'meshkee_active_business_domain'
|
const ACTIVE_BUSINESS_DOMAIN_KEY = 'meshkee_active_business_domain'
|
||||||
|
|
||||||
export function setActiveBusiness(user: AuthUser) {
|
/** Prefer resolving the host tenant, then {@link setActiveBusinessById}. */
|
||||||
|
export function setActiveBusiness(user: AuthUser, businessId?: string) {
|
||||||
const domain = getBusinessDomain()
|
const domain = getBusinessDomain()
|
||||||
localStorage.setItem(ACTIVE_BUSINESS_DOMAIN_KEY, domain)
|
localStorage.setItem(ACTIVE_BUSINESS_DOMAIN_KEY, domain)
|
||||||
|
|
||||||
if (user.businesses.length > 0) {
|
const id = businessId ?? (user.businesses[0] ? String(user.businesses[0].id) : null)
|
||||||
localStorage.setItem(ACTIVE_BUSINESS_ID_KEY, String(user.businesses[0].id))
|
if (id) {
|
||||||
|
localStorage.setItem(ACTIVE_BUSINESS_ID_KEY, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -336,12 +336,16 @@ const en = {
|
|||||||
'signup.error.length': 'Password must be at least 8 characters.',
|
'signup.error.length': 'Password must be at least 8 characters.',
|
||||||
'signup.error.create': 'Unable to create account.',
|
'signup.error.create': 'Unable to create account.',
|
||||||
'signup.error.cellExists':
|
'signup.error.cellExists':
|
||||||
'This mobile number already has an account in Meshkee Unified Management System (__MESHKEE_APP__) on {sites}. Your password and data will stay the same. Continue to verify by SMS and join this website too.',
|
'This mobile number already has an account in Meshkee Unified Management System (__MESHKEE_APP__) on {sites}. Your profile stays the same; the password you enter here will replace your Meshkee password. Continue to verify by SMS and join this website too.',
|
||||||
'signup.error.cellExistsGeneric':
|
'signup.error.cellExistsGeneric':
|
||||||
'This mobile number already has an account in Meshkee Unified Management System (__MESHKEE_APP__) on another website. Your password and data will stay the same. Continue to verify by SMS and join this website too.',
|
'This mobile number already has an account in Meshkee Unified Management System (__MESHKEE_APP__) on another website. Your profile stays the same; the password you enter here will replace your Meshkee password. Continue to verify by SMS and join this website too.',
|
||||||
'signup.info.verify': 'Account created. Enter the SMS code to verify your number and sign in.',
|
'signup.info.verify': 'Account created. Enter the SMS code to verify your number and sign in.',
|
||||||
'signup.info.verifyExisting':
|
'signup.info.verifyExisting':
|
||||||
'Enter the SMS code to join this website. Your Meshkee password and profile stay the same.',
|
'Enter the SMS code to join this website. Your Meshkee password and profile stay the same.',
|
||||||
|
'signup.info.verifyExistingPasswordUpdated':
|
||||||
|
'Enter the SMS code to finish joining. Your Meshkee password was updated to the one you just entered.',
|
||||||
|
'signup.toast.passwordUpdated':
|
||||||
|
'Your Meshkee password was updated to the one you entered on this website.',
|
||||||
'signup.cellExists.title': 'Existing Meshkee account',
|
'signup.cellExists.title': 'Existing Meshkee account',
|
||||||
'signup.cellExists.admit': 'I understand, continue',
|
'signup.cellExists.admit': 'I understand, continue',
|
||||||
|
|
||||||
@@ -729,12 +733,16 @@ const fa: Record<MessageKey, string> = {
|
|||||||
'signup.error.length': 'رمز عبور باید حداقل ۸ کاراکتر باشد.',
|
'signup.error.length': 'رمز عبور باید حداقل ۸ کاراکتر باشد.',
|
||||||
'signup.error.create': 'ایجاد حساب ممکن نشد.',
|
'signup.error.create': 'ایجاد حساب ممکن نشد.',
|
||||||
'signup.error.cellExists':
|
'signup.error.cellExists':
|
||||||
'این شماره موبایل قبلاً در سیستم مدیریت یکپارچه ی مشکی (__MESHKEE_APP__) حساب دارد ({sites}). رمز عبور و اطلاعات شما همان خواهد بود. با ادامه، تأیید پیامکی را انجام دهید تا وارد این وبسایت هم شوید.',
|
'این شماره موبایل قبلاً در سیستم مدیریت یکپارچه ی مشکی (__MESHKEE_APP__) حساب دارد ({sites}). اطلاعات شما همان میماند؛ رمزی که اینجا وارد کردید جایگزین رمز مشکی میشود. با ادامه، تأیید پیامکی را انجام دهید تا وارد این وبسایت هم شوید.',
|
||||||
'signup.error.cellExistsGeneric':
|
'signup.error.cellExistsGeneric':
|
||||||
'این شماره موبایل قبلاً در سیستم مدیریت یکپارچه ی مشکی (__MESHKEE_APP__) (وبسایت دیگری) حساب دارد. رمز عبور و اطلاعات شما همان خواهد بود. با ادامه، تأیید پیامکی را انجام دهید تا وارد این وبسایت هم شوید.',
|
'این شماره موبایل قبلاً در سیستم مدیریت یکپارچه ی مشکی (__MESHKEE_APP__) (وبسایت دیگری) حساب دارد. اطلاعات شما همان میماند؛ رمزی که اینجا وارد کردید جایگزین رمز مشکی میشود. با ادامه، تأیید پیامکی را انجام دهید تا وارد این وبسایت هم شوید.',
|
||||||
'signup.info.verify': 'حساب ایجاد شد. کد پیامکی را وارد کنید تا شماره تأیید شود و وارد شوید.',
|
'signup.info.verify': 'حساب ایجاد شد. کد پیامکی را وارد کنید تا شماره تأیید شود و وارد شوید.',
|
||||||
'signup.info.verifyExisting':
|
'signup.info.verifyExisting':
|
||||||
'کد پیامکی را وارد کنید تا به این وبسایت بپیوندید. رمز عبور و اطلاعات مشکی شما همان میماند.',
|
'کد پیامکی را وارد کنید تا به این وبسایت بپیوندید. رمز عبور و اطلاعات مشکی شما همان میماند.',
|
||||||
|
'signup.info.verifyExistingPasswordUpdated':
|
||||||
|
'کد پیامکی را وارد کنید تا عضویت تمام شود. رمز مشکی شما به رمزی که همین الان وارد کردید بهروز شد.',
|
||||||
|
'signup.toast.passwordUpdated':
|
||||||
|
'رمز مشکی شما به رمزی که در این وبسایت وارد کردید بهروز شد.',
|
||||||
'signup.cellExists.title': 'حساب موجود در مشکی',
|
'signup.cellExists.title': 'حساب موجود در مشکی',
|
||||||
'signup.cellExists.admit': 'متوجه شدم، ادامه',
|
'signup.cellExists.admit': 'متوجه شدم، ادامه',
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
resetPassword,
|
resetPassword,
|
||||||
sendOtp,
|
sendOtp,
|
||||||
} from '../services/authService'
|
} from '../services/authService'
|
||||||
import { LanguageSelect, PasswordStrengthMeter, isPasswordStrong, useLocale } from '@meshkee/dashboard-ui'
|
import { LanguageSelect, PasswordStrengthMeter, isPasswordStrong, useLocale, useToast } from '@meshkee/dashboard-ui'
|
||||||
import { useT } from '../i18n/useT'
|
import { useT } from '../i18n/useT'
|
||||||
import { safeRedirectPath } from '../lib/redirect'
|
import { safeRedirectPath } from '../lib/redirect'
|
||||||
import { BackgroundOrbs } from '../components/BackgroundOrbs'
|
import { BackgroundOrbs } from '../components/BackgroundOrbs'
|
||||||
@@ -66,6 +66,7 @@ export function LoginPage() {
|
|||||||
const { businessName, businessNameEn, logoUrl } = useTenantBranding()
|
const { businessName, businessNameEn, logoUrl } = useTenantBranding()
|
||||||
const tenantDomain = getTenantDomain()
|
const tenantDomain = getTenantDomain()
|
||||||
const t = useT()
|
const t = useT()
|
||||||
|
const { showToast } = useToast()
|
||||||
const { locale, dir } = useLocale()
|
const { locale, dir } = useLocale()
|
||||||
|
|
||||||
const [view, setView] = useState<AuthView>('login')
|
const [view, setView] = useState<AuthView>('login')
|
||||||
@@ -207,7 +208,7 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runSignup() {
|
async function runSignup(acknowledgeExistingAccount = false) {
|
||||||
const cellNumber = resolveCellNumber()
|
const cellNumber = resolveCellNumber()
|
||||||
if (!cellNumber) return
|
if (!cellNumber) return
|
||||||
|
|
||||||
@@ -220,10 +221,15 @@ export function LoginPage() {
|
|||||||
firstName: firstName.trim(),
|
firstName: firstName.trim(),
|
||||||
lastName: lastName.trim(),
|
lastName: lastName.trim(),
|
||||||
domain: tenantDomain,
|
domain: tenantDomain,
|
||||||
|
...(acknowledgeExistingAccount ? { acknowledgeExistingAccount: true } : {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
setCellExistsSites(null)
|
setCellExistsSites(null)
|
||||||
|
|
||||||
|
if (data.passwordUpdated) {
|
||||||
|
showToast(t('signup.toast.passwordUpdated'), 'info')
|
||||||
|
}
|
||||||
|
|
||||||
if (data.user.customerBusinesses.length === 0) {
|
if (data.user.customerBusinesses.length === 0) {
|
||||||
logoutRequest()
|
logoutRequest()
|
||||||
setError(CUSTOMER_ACCESS_MESSAGE)
|
setError(CUSTOMER_ACCESS_MESSAGE)
|
||||||
@@ -232,14 +238,19 @@ export function LoginPage() {
|
|||||||
|
|
||||||
if (!data.user.cellVerifiedAt) {
|
if (!data.user.cellVerifiedAt) {
|
||||||
logoutRequest()
|
logoutRequest()
|
||||||
await continueToSmsVerify(cellNumber)
|
await continueToSmsVerify(
|
||||||
|
cellNumber,
|
||||||
|
data.passwordUpdated
|
||||||
|
? t('signup.info.verifyExistingPasswordUpdated')
|
||||||
|
: t('signup.info.verify'),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await login(cellNumber, password)
|
await login(cellNumber, password)
|
||||||
navigate(redirectTo)
|
navigate(redirectTo)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ApiError) {
|
if (!acknowledgeExistingAccount && err instanceof ApiError) {
|
||||||
const sites = parseCellExistsSites(err.message)
|
const sites = parseCellExistsSites(err.message)
|
||||||
if (sites !== null) {
|
if (sites !== null) {
|
||||||
setCellExistsSites(sites)
|
setCellExistsSites(sites)
|
||||||
@@ -253,13 +264,13 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function continueToSmsVerify(cellNumber: string) {
|
async function continueToSmsVerify(cellNumber: string, infoMessage?: string) {
|
||||||
setView('otp')
|
setView('otp')
|
||||||
setSmsStep('phone')
|
setSmsStep('phone')
|
||||||
setPassword('')
|
setPassword('')
|
||||||
setConfirmPassword('')
|
setConfirmPassword('')
|
||||||
setSmsCode('')
|
setSmsCode('')
|
||||||
setInfo(t('signup.info.verify'))
|
setInfo(infoMessage ?? t('signup.info.verify'))
|
||||||
try {
|
try {
|
||||||
const otpResult = await sendOtp(cellNumber, tenantDomain)
|
const otpResult = await sendOtp(cellNumber, tenantDomain)
|
||||||
if (!otpResult.enabled) {
|
if (!otpResult.enabled) {
|
||||||
@@ -292,9 +303,8 @@ export function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Existing Meshkee account on another site: user admits the notice, then proves
|
* Existing Meshkee account on another site: user admits the notice, then we link
|
||||||
* phone ownership via SMS. login-otp links them to this website (password/data unchanged),
|
* them with the new password and prove phone ownership via SMS.
|
||||||
* so they appear in the business Customers list after verification.
|
|
||||||
*/
|
*/
|
||||||
async function handleAdmitCellExists() {
|
async function handleAdmitCellExists() {
|
||||||
clearMessages()
|
clearMessages()
|
||||||
@@ -304,27 +314,7 @@ export function LoginPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitting(true)
|
await runSignup(true)
|
||||||
setCellExistsSites(null)
|
|
||||||
try {
|
|
||||||
setInfo(t('signup.info.verifyExisting'))
|
|
||||||
setView('otp')
|
|
||||||
setSmsStep('phone')
|
|
||||||
setPassword('')
|
|
||||||
setConfirmPassword('')
|
|
||||||
setSmsCode('')
|
|
||||||
const otpResult = await sendOtp(cellNumber, tenantDomain)
|
|
||||||
if (!otpResult.enabled) {
|
|
||||||
setInfo(otpResult.message)
|
|
||||||
}
|
|
||||||
setCodeSent(true)
|
|
||||||
setSmsStep('code')
|
|
||||||
startCountdown()
|
|
||||||
} catch (otpErr) {
|
|
||||||
handleApiError(otpErr, t('login.error.sendCode'))
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleResetPassword(e: React.FormEvent) {
|
async function handleResetPassword(e: React.FormEvent) {
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ Business and customer login pages share the same SMS-backed flows (super-admin s
|
|||||||
| Verify only | `POST /auth/verify-otp` | Marks verified; no tokens |
|
| Verify only | `POST /auth/verify-otp` | Marks verified; no tokens |
|
||||||
| Dashboard SSO | `POST /auth/handoff` + `POST /auth/handoff/consume` | Admin icon on customer dashboard; 60s one-time ticket |
|
| Dashboard SSO | `POST /auth/handoff` + `POST /auth/handoff/consume` | Admin icon on customer dashboard; 60s one-time ticket |
|
||||||
|
|
||||||
Auth helpers: `apps/*/src/services/authService.ts` (`login`, `loginWithOtp`, `resetPassword`, `sendOtp`, `register`, `createHandoff`). `AuthContext` exposes `login` + `loginWithOtp`. Customer header “open business dashboard” uses a one-time SSO ticket so staff do not log in again.
|
Auth helpers: `apps/*/src/services/authService.ts` (`login`, `loginWithOtp`, `resetPassword`, `sendOtp`, `register`, `createHandoff`). `AuthContext` exposes `login` + `loginWithOtp`. Business dashboard active tenant is resolved from the host domain (never `businesses[0]` alone). Customer header “open business dashboard” uses a one-time SSO ticket so staff do not log in again.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export interface MeResponse {
|
|||||||
|
|
||||||
export interface RegisterResponse extends LoginResponse {
|
export interface RegisterResponse extends LoginResponse {
|
||||||
smsEnabled?: boolean
|
smsEnabled?: boolean
|
||||||
|
/** True when an existing Meshkee account was linked and its password was replaced. */
|
||||||
|
passwordUpdated?: boolean
|
||||||
registeredBusiness?: { id: string; name: string; slug: string }
|
registeredBusiness?: { id: string; name: string; slug: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user