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:
co-authored by
Cursor
parent
e942348b1d
commit
6ce6f959ac
+73
-24
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user