Add dark-mode business logo, payment order steps, and Arvan DNS diagnostics.

Store logo_dark_media_id on businesses, expose logoDarkUrl on tenant/website APIs, prepend awaiting-payment steps to order workflows, and surface clearer Arvan API network errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-26 00:21:46 +03:30
co-authored by Cursor
parent e942348b1d
commit 6ce6f959ac
17 changed files with 237 additions and 29 deletions
+73 -24
View File
@@ -30,6 +30,8 @@ import { parseUserProfile } from './profile.util';
import { SmsService } from './sms.service';
const OTP_TTL_SECONDS = 300;
/** Pending cross-site password change (longer than OTP so resend still works). */
const PENDING_PASSWORD_TTL_SECONDS = 600;
const HANDOFF_TTL_SECONDS = 60;
const HANDOFF_RATE_LIMIT = 10;
const HANDOFF_RATE_WINDOW_SECONDS = 60;
@@ -78,13 +80,14 @@ export class AuthService {
);
}
let passwordMatchesExisting = false;
if (existingUser) {
const passwordValid = await bcrypt.compare(
passwordMatchesExisting = await bcrypt.compare(
dto.password,
existingUser.passwordHash,
);
if (!passwordValid && !dto.acknowledgeExistingAccount) {
if (!passwordMatchesExisting && !dto.acknowledgeExistingAccount) {
const otherNames = existingUser.businessCustomers
.map((item) => item.business.nameFa?.trim() || item.business.name)
.filter(Boolean);
@@ -98,18 +101,49 @@ export class AuthService {
}
const linkedExistingAccount = Boolean(existingUser);
// New password claimed on another site — apply only after OTP proves ownership.
const wantsPasswordUpdate =
linkedExistingAccount && !passwordMatchesExisting;
// Hold new password in Redis until OTP; do not touch DB password or membership yet.
if (existingUser && wantsPasswordUpdate && smsEnabled) {
await this.redis.setPendingPasswordUpdate(
dto.cellNumber,
{
passwordHash,
businessId: business.id.toString(),
},
PENDING_PASSWORD_TTL_SECONDS,
);
const authUser = await this.getAuthUser(existingUser.id);
return {
message:
'Please verify your cell number with OTP. Your Meshkee password will be updated after verification.',
smsEnabled: true,
requiresOtp: true,
passwordPending: true,
passwordUpdated: false,
user: this.serializeUser(authUser),
registeredBusiness: {
id: business.id,
name: business.name,
slug: business.slug,
},
};
}
const userId = await this.prisma.$transaction(async (tx) => {
let userId: bigint;
if (existingUser) {
// Cross-site join: replace Meshkee password with the one entered on this signup.
const updated = await tx.user.update({
where: { id: existingUser.id },
data: { passwordHash },
select: { id: true },
});
userId = updated.id;
if (wantsPasswordUpdate) {
await tx.user.update({
where: { id: existingUser.id },
data: { passwordHash },
});
}
userId = existingUser.id;
} else {
const created = await tx.user.create({
data: {
@@ -157,15 +191,15 @@ export class AuthService {
const tokens = await this.issueTokens(authUser);
return {
message: linkedExistingAccount
? smsEnabled
? 'Joined this website. Your Meshkee password was updated to the one you just entered. Please verify your cell number with OTP.'
: 'Joined this website. Your Meshkee password was updated to the one you just entered.'
: smsEnabled
? 'Registration successful. Please verify your cell number with OTP.'
: 'Registration successful. SMS verification is disabled — account auto-verified.',
message: wantsPasswordUpdate
? 'Joined this website. Your Meshkee password was updated to the one you just entered.'
: linkedExistingAccount
? 'Joined this website with your existing Meshkee account.'
: smsEnabled
? 'Registration successful. Please verify your cell number with OTP.'
: 'Registration successful. SMS verification is disabled — account auto-verified.',
smsEnabled,
passwordUpdated: linkedExistingAccount,
passwordUpdated: wantsPasswordUpdate,
user: this.serializeUser(authUser),
registeredBusiness: {
id: business.id,
@@ -439,10 +473,8 @@ export class AuthService {
async loginWithOtp(cellNumber: string, code: string, domain?: string) {
const user = await this.consumeOtp(cellNumber, code);
if (domain?.trim()) {
await this.ensureCustomerMembershipForDomain(user.id, domain.trim());
}
const pending = await this.redis.consumePendingPasswordUpdate(cellNumber);
const passwordUpdated = Boolean(pending?.passwordHash);
await this.prisma.user.update({
where: { id: user.id },
@@ -451,14 +483,28 @@ export class AuthService {
isActive: true,
cellVerifiedAt: user.cellVerifiedAt ?? new Date(),
lastLoginAt: new Date(),
...(pending?.passwordHash
? { passwordHash: pending.passwordHash }
: {}),
},
});
if (pending?.businessId) {
await this.ensureCustomerMembership(user.id, BigInt(pending.businessId));
}
if (domain?.trim()) {
await this.ensureCustomerMembershipForDomain(user.id, domain.trim());
}
const authUser = await this.getAuthUser(user.id);
const tokens = await this.issueTokens(authUser);
return {
message: 'Login successful',
message: passwordUpdated
? 'Login successful. Your Meshkee password was updated.'
: 'Login successful',
passwordUpdated,
user: this.serializeUser(authUser),
...tokens,
};
@@ -510,17 +556,20 @@ export class AuthService {
domain: string,
) {
const business = await this.tenant.resolveBusinessByDomain(domain);
await this.ensureCustomerMembership(userId, business.id);
}
private async ensureCustomerMembership(userId: bigint, businessId: bigint) {
const existing = await this.prisma.businessCustomer.findUnique({
where: {
businessId_userId: { businessId: business.id, userId },
businessId_userId: { businessId, userId },
},
});
if (!existing) {
await this.prisma.businessCustomer.create({
data: {
businessId: business.id,
businessId,
userId,
},
});