mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-11 22:30:59 +04:30
Improve cross-site customer signup and login membership.
Same-password register joins silently; mismatched password explains the existing Meshkee account; login/OTP with domain attaches business membership. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
953b87b616
commit
acc8f35682
@@ -43,7 +43,7 @@ export class AuthController {
|
|||||||
|
|
||||||
@Post('login-otp')
|
@Post('login-otp')
|
||||||
loginWithOtp(@Body() dto: LoginOtpDto) {
|
loginWithOtp(@Body() dto: LoginOtpDto) {
|
||||||
return this.authService.loginWithOtp(dto.cellNumber, dto.code);
|
return this.authService.loginWithOtp(dto.cellNumber, dto.code, dto.domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('reset-password')
|
@Post('reset-password')
|
||||||
|
|||||||
+83
-15
@@ -57,16 +57,41 @@ export class AuthService {
|
|||||||
const existingUser = await this.prisma.user.findUnique({
|
const existingUser = await this.prisma.user.findUnique({
|
||||||
where: { cellNumber: dto.cellNumber },
|
where: { cellNumber: dto.cellNumber },
|
||||||
include: {
|
include: {
|
||||||
businessCustomers: { where: { businessId: business.id } },
|
businessCustomers: {
|
||||||
|
include: { business: { select: { id: true, name: true, nameFa: true } } },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingUser?.businessCustomers.length) {
|
const alreadyOnThisSite = existingUser?.businessCustomers.some(
|
||||||
|
(item) => item.businessId === business.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (alreadyOnThisSite) {
|
||||||
throw new ConflictException(
|
throw new ConflictException(
|
||||||
'This cell number is already registered on this website',
|
'This cell number is already registered on this website',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
const passwordValid = await bcrypt.compare(
|
||||||
|
dto.password,
|
||||||
|
existingUser.passwordHash,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!passwordValid) {
|
||||||
|
const otherNames = existingUser.businessCustomers
|
||||||
|
.map((item) => item.business.nameFa?.trim() || item.business.name)
|
||||||
|
.filter(Boolean);
|
||||||
|
const siteList =
|
||||||
|
otherNames.length > 0 ? otherNames.join(', ') : 'another Meshkee website';
|
||||||
|
|
||||||
|
throw new ConflictException(
|
||||||
|
`CELL_EXISTS_OTHER_SITE:${siteList}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const user = await this.prisma.$transaction(async (tx) => {
|
const user = await this.prisma.$transaction(async (tx) => {
|
||||||
const account =
|
const account =
|
||||||
existingUser ??
|
existingUser ??
|
||||||
@@ -81,18 +106,6 @@ export class AuthService {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (existingUser) {
|
|
||||||
const passwordValid = await bcrypt.compare(
|
|
||||||
dto.password,
|
|
||||||
existingUser.passwordHash,
|
|
||||||
);
|
|
||||||
if (!passwordValid) {
|
|
||||||
throw new ConflictException(
|
|
||||||
'Cell number exists on another account. Use login or reset password.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx.businessCustomer.create({
|
await tx.businessCustomer.create({
|
||||||
data: {
|
data: {
|
||||||
businessId: business.id,
|
businessId: business.id,
|
||||||
@@ -159,6 +172,10 @@ export class AuthService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dto.domain?.trim()) {
|
||||||
|
await this.ensureCustomerMembershipForDomain(user.id, dto.domain.trim());
|
||||||
|
}
|
||||||
|
|
||||||
await this.prisma.user.update({
|
await this.prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: { lastLoginAt: new Date() },
|
data: { lastLoginAt: new Date() },
|
||||||
@@ -340,9 +357,13 @@ export class AuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async loginWithOtp(cellNumber: string, code: string) {
|
async loginWithOtp(cellNumber: string, code: string, domain?: string) {
|
||||||
const user = await this.consumeOtp(cellNumber, code);
|
const user = await this.consumeOtp(cellNumber, code);
|
||||||
|
|
||||||
|
if (domain?.trim()) {
|
||||||
|
await this.ensureCustomerMembershipForDomain(user.id, domain.trim());
|
||||||
|
}
|
||||||
|
|
||||||
await this.prisma.user.update({
|
await this.prisma.user.update({
|
||||||
where: { id: user.id },
|
where: { id: user.id },
|
||||||
data: {
|
data: {
|
||||||
@@ -401,6 +422,53 @@ export class AuthService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ensureCustomerMembershipForDomain(
|
||||||
|
userId: bigint,
|
||||||
|
domain: string,
|
||||||
|
) {
|
||||||
|
const business = await this.tenant.resolveBusinessByDomain(domain);
|
||||||
|
|
||||||
|
const existing = await this.prisma.businessCustomer.findUnique({
|
||||||
|
where: {
|
||||||
|
businessId_userId: { businessId: business.id, userId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
await this.prisma.businessCustomer.create({
|
||||||
|
data: {
|
||||||
|
businessId: business.id,
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerRole = await this.prisma.role.findUnique({
|
||||||
|
where: { slug: 'customer' },
|
||||||
|
});
|
||||||
|
if (!customerRole) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasCustomerRole = await this.prisma.userRole.findUnique({
|
||||||
|
where: {
|
||||||
|
userId_roleId: {
|
||||||
|
userId,
|
||||||
|
roleId: customerRole.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasCustomerRole) {
|
||||||
|
await this.prisma.userRole.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
roleId: customerRole.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async getAuthUser(userId: bigint): Promise<AuthUser> {
|
private async getAuthUser(userId: bigint): Promise<AuthUser> {
|
||||||
const user = await this.prisma.user.findUnique({
|
const user = await this.prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsString, Length, Matches } from 'class-validator';
|
import { IsOptional, IsString, Length, Matches } from 'class-validator';
|
||||||
|
|
||||||
export class LoginOtpDto {
|
export class LoginOtpDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -11,4 +11,9 @@ export class LoginOtpDto {
|
|||||||
@Length(6, 6)
|
@Length(6, 6)
|
||||||
@Matches(/^\d{6}$/, { message: 'code must be a 6-digit number' })
|
@Matches(/^\d{6}$/, { message: 'code must be a 6-digit number' })
|
||||||
code!: string;
|
code!: string;
|
||||||
|
|
||||||
|
/** Website apex — when set, attaches customer membership for that tenant on successful OTP login. */
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
domain?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsString, Matches, MinLength } from 'class-validator';
|
import { IsOptional, IsString, Matches, MinLength } from 'class-validator';
|
||||||
|
|
||||||
export class LoginDto {
|
export class LoginDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -10,4 +10,9 @@ export class LoginDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(8)
|
@MinLength(8)
|
||||||
password!: string;
|
password!: string;
|
||||||
|
|
||||||
|
/** Website apex — when set, attaches customer membership for that tenant on successful login. */
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
domain?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user