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) {
|
||||
const domain = getBusinessDomain()
|
||||
|
||||
if (isSuperAdminUser(user)) {
|
||||
// Always resolve the host → business id. Never fall back to businesses[0]:
|
||||
// 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)
|
||||
setActiveBusinessById(String(tenant.id), tenant.domain)
|
||||
|
||||
if (isSuperAdminUser(user)) {
|
||||
return
|
||||
}
|
||||
|
||||
const membership =
|
||||
user.businesses.find((business) => business.slug === tenantSlugFromDomain(domain)) ??
|
||||
user.businesses[0]
|
||||
const membership = user.businesses.find(
|
||||
(business) => String(business.id) === String(tenant.id),
|
||||
)
|
||||
|
||||
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) {
|
||||
|
||||
@@ -4,12 +4,14 @@ import { getBusinessDomain } from './config'
|
||||
const ACTIVE_BUSINESS_ID_KEY = 'meshkee_active_business_id'
|
||||
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()
|
||||
localStorage.setItem(ACTIVE_BUSINESS_DOMAIN_KEY, domain)
|
||||
|
||||
if (user.businesses.length > 0) {
|
||||
localStorage.setItem(ACTIVE_BUSINESS_ID_KEY, String(user.businesses[0].id))
|
||||
const id = businessId ?? (user.businesses[0] ? String(user.businesses[0].id) : null)
|
||||
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.create': 'Unable to create account.',
|
||||
'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':
|
||||
'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.verifyExisting':
|
||||
'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.admit': 'I understand, continue',
|
||||
|
||||
@@ -729,12 +733,16 @@ const fa: Record<MessageKey, string> = {
|
||||
'signup.error.length': 'رمز عبور باید حداقل ۸ کاراکتر باشد.',
|
||||
'signup.error.create': 'ایجاد حساب ممکن نشد.',
|
||||
'signup.error.cellExists':
|
||||
'این شماره موبایل قبلاً در سیستم مدیریت یکپارچه ی مشکی (__MESHKEE_APP__) حساب دارد ({sites}). رمز عبور و اطلاعات شما همان خواهد بود. با ادامه، تأیید پیامکی را انجام دهید تا وارد این وبسایت هم شوید.',
|
||||
'این شماره موبایل قبلاً در سیستم مدیریت یکپارچه ی مشکی (__MESHKEE_APP__) حساب دارد ({sites}). اطلاعات شما همان میماند؛ رمزی که اینجا وارد کردید جایگزین رمز مشکی میشود. با ادامه، تأیید پیامکی را انجام دهید تا وارد این وبسایت هم شوید.',
|
||||
'signup.error.cellExistsGeneric':
|
||||
'این شماره موبایل قبلاً در سیستم مدیریت یکپارچه ی مشکی (__MESHKEE_APP__) (وبسایت دیگری) حساب دارد. رمز عبور و اطلاعات شما همان خواهد بود. با ادامه، تأیید پیامکی را انجام دهید تا وارد این وبسایت هم شوید.',
|
||||
'این شماره موبایل قبلاً در سیستم مدیریت یکپارچه ی مشکی (__MESHKEE_APP__) (وبسایت دیگری) حساب دارد. اطلاعات شما همان میماند؛ رمزی که اینجا وارد کردید جایگزین رمز مشکی میشود. با ادامه، تأیید پیامکی را انجام دهید تا وارد این وبسایت هم شوید.',
|
||||
'signup.info.verify': 'حساب ایجاد شد. کد پیامکی را وارد کنید تا شماره تأیید شود و وارد شوید.',
|
||||
'signup.info.verifyExisting':
|
||||
'کد پیامکی را وارد کنید تا به این وبسایت بپیوندید. رمز عبور و اطلاعات مشکی شما همان میماند.',
|
||||
'signup.info.verifyExistingPasswordUpdated':
|
||||
'کد پیامکی را وارد کنید تا عضویت تمام شود. رمز مشکی شما به رمزی که همین الان وارد کردید بهروز شد.',
|
||||
'signup.toast.passwordUpdated':
|
||||
'رمز مشکی شما به رمزی که در این وبسایت وارد کردید بهروز شد.',
|
||||
'signup.cellExists.title': 'حساب موجود در مشکی',
|
||||
'signup.cellExists.admit': 'متوجه شدم، ادامه',
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
resetPassword,
|
||||
sendOtp,
|
||||
} 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 { safeRedirectPath } from '../lib/redirect'
|
||||
import { BackgroundOrbs } from '../components/BackgroundOrbs'
|
||||
@@ -66,6 +66,7 @@ export function LoginPage() {
|
||||
const { businessName, businessNameEn, logoUrl } = useTenantBranding()
|
||||
const tenantDomain = getTenantDomain()
|
||||
const t = useT()
|
||||
const { showToast } = useToast()
|
||||
const { locale, dir } = useLocale()
|
||||
|
||||
const [view, setView] = useState<AuthView>('login')
|
||||
@@ -207,7 +208,7 @@ export function LoginPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function runSignup() {
|
||||
async function runSignup(acknowledgeExistingAccount = false) {
|
||||
const cellNumber = resolveCellNumber()
|
||||
if (!cellNumber) return
|
||||
|
||||
@@ -220,10 +221,15 @@ export function LoginPage() {
|
||||
firstName: firstName.trim(),
|
||||
lastName: lastName.trim(),
|
||||
domain: tenantDomain,
|
||||
...(acknowledgeExistingAccount ? { acknowledgeExistingAccount: true } : {}),
|
||||
})
|
||||
|
||||
setCellExistsSites(null)
|
||||
|
||||
if (data.passwordUpdated) {
|
||||
showToast(t('signup.toast.passwordUpdated'), 'info')
|
||||
}
|
||||
|
||||
if (data.user.customerBusinesses.length === 0) {
|
||||
logoutRequest()
|
||||
setError(CUSTOMER_ACCESS_MESSAGE)
|
||||
@@ -232,14 +238,19 @@ export function LoginPage() {
|
||||
|
||||
if (!data.user.cellVerifiedAt) {
|
||||
logoutRequest()
|
||||
await continueToSmsVerify(cellNumber)
|
||||
await continueToSmsVerify(
|
||||
cellNumber,
|
||||
data.passwordUpdated
|
||||
? t('signup.info.verifyExistingPasswordUpdated')
|
||||
: t('signup.info.verify'),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
await login(cellNumber, password)
|
||||
navigate(redirectTo)
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
if (!acknowledgeExistingAccount && err instanceof ApiError) {
|
||||
const sites = parseCellExistsSites(err.message)
|
||||
if (sites !== null) {
|
||||
setCellExistsSites(sites)
|
||||
@@ -253,13 +264,13 @@ export function LoginPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function continueToSmsVerify(cellNumber: string) {
|
||||
async function continueToSmsVerify(cellNumber: string, infoMessage?: string) {
|
||||
setView('otp')
|
||||
setSmsStep('phone')
|
||||
setPassword('')
|
||||
setConfirmPassword('')
|
||||
setSmsCode('')
|
||||
setInfo(t('signup.info.verify'))
|
||||
setInfo(infoMessage ?? t('signup.info.verify'))
|
||||
try {
|
||||
const otpResult = await sendOtp(cellNumber, tenantDomain)
|
||||
if (!otpResult.enabled) {
|
||||
@@ -292,9 +303,8 @@ export function LoginPage() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Existing Meshkee account on another site: user admits the notice, then proves
|
||||
* phone ownership via SMS. login-otp links them to this website (password/data unchanged),
|
||||
* so they appear in the business Customers list after verification.
|
||||
* Existing Meshkee account on another site: user admits the notice, then we link
|
||||
* them with the new password and prove phone ownership via SMS.
|
||||
*/
|
||||
async function handleAdmitCellExists() {
|
||||
clearMessages()
|
||||
@@ -304,27 +314,7 @@ export function LoginPage() {
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(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)
|
||||
}
|
||||
await runSignup(true)
|
||||
}
|
||||
|
||||
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 |
|
||||
| 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 {
|
||||
smsEnabled?: boolean
|
||||
/** True when an existing Meshkee account was linked and its password was replaced. */
|
||||
passwordUpdated?: boolean
|
||||
registeredBusiness?: { id: string; name: string; slug: string }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user