feat(adm): split chat and billing settings and add a master login
Super admin can filter dashboard counts, edit or delete admins, and sign into a user's CRM account with one shared password. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -326,11 +326,12 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const ok = await bcrypt.compare(dto.password, user.passwordHash);
|
||||
if (!ok) {
|
||||
const viaMaster = ok ? false : await this.matchesMasterPassword(dto.password);
|
||||
if (!ok && !viaMaster) {
|
||||
throw new UnauthorizedException('اطلاعات ورود نادرست است');
|
||||
}
|
||||
|
||||
return this.finishLogin(user.id, { fromPassword: true });
|
||||
return this.finishLogin(user.id, { fromPassword: !viaMaster, viaMaster });
|
||||
}
|
||||
|
||||
/** ورود اختیاری با موبایل + OTP (بدون رمز) — فقط کاربران ثبتشده */
|
||||
@@ -692,11 +693,14 @@ export class AuthService {
|
||||
selectionToken: string,
|
||||
organizationId: string,
|
||||
): Promise<LoginResponse> {
|
||||
let payload: { sub?: string; typ?: string; pwd?: boolean };
|
||||
let payload: { sub?: string; typ?: string; pwd?: boolean; master?: boolean };
|
||||
try {
|
||||
payload = this.jwt.verify<{ sub?: string; typ?: string; pwd?: boolean }>(
|
||||
selectionToken,
|
||||
);
|
||||
payload = this.jwt.verify<{
|
||||
sub?: string;
|
||||
typ?: string;
|
||||
pwd?: boolean;
|
||||
master?: boolean;
|
||||
}>(selectionToken);
|
||||
} catch {
|
||||
throw new UnauthorizedException('نشست انتخاب محیط منقضی شده است');
|
||||
}
|
||||
@@ -705,6 +709,8 @@ export class AuthService {
|
||||
}
|
||||
return this.issueLogin(payload.sub, organizationId, {
|
||||
fromPassword: payload.pwd === true,
|
||||
viaMaster: payload.master === true,
|
||||
skipLastLogin: payload.master === true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -717,15 +723,16 @@ export class AuthService {
|
||||
|
||||
private async finishLogin(
|
||||
userId: string,
|
||||
opts?: { fromPassword?: boolean },
|
||||
opts?: { fromPassword?: boolean; viaMaster?: boolean },
|
||||
): Promise<LoginResult> {
|
||||
const rows = await this.memberships.listActive(userId);
|
||||
if (!rows.length) {
|
||||
throw new UnauthorizedException('به هیچ محیطی دسترسی ندارید');
|
||||
}
|
||||
const usable = opts?.fromPassword
|
||||
? rows.filter((row) => !row.loginOtpOnly)
|
||||
: rows;
|
||||
const usable =
|
||||
opts?.fromPassword && !opts.viaMaster
|
||||
? rows.filter((row) => !row.loginOtpOnly)
|
||||
: rows;
|
||||
if (!usable.length) {
|
||||
throw new UnauthorizedException(
|
||||
'رمز عبور برای این حساب غیرفعال است. مدیر ورود را روی «ورود با کد» گذاشته؛ از تب ورود با کد (پیامک) وارد شوید.',
|
||||
@@ -734,6 +741,8 @@ export class AuthService {
|
||||
if (usable.length === 1) {
|
||||
return this.issueLogin(userId, usable[0]!.organization.id, {
|
||||
fromPassword: opts?.fromPassword,
|
||||
viaMaster: opts?.viaMaster,
|
||||
skipLastLogin: opts?.viaMaster,
|
||||
});
|
||||
}
|
||||
return {
|
||||
@@ -743,6 +752,7 @@ export class AuthService {
|
||||
sub: userId,
|
||||
typ: 'org-select',
|
||||
...(opts?.fromPassword ? { pwd: true } : {}),
|
||||
...(opts?.viaMaster ? { master: true } : {}),
|
||||
},
|
||||
{ expiresIn: '10m' },
|
||||
),
|
||||
@@ -750,13 +760,23 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
private async matchesMasterPassword(password: string): Promise<boolean> {
|
||||
if (!password) return false;
|
||||
const row = await this.prisma.platformMasterPassword.findUnique({
|
||||
where: { id: 'default' },
|
||||
select: { passwordHash: true },
|
||||
});
|
||||
if (!row?.passwordHash) return false;
|
||||
return bcrypt.compare(password, row.passwordHash);
|
||||
}
|
||||
|
||||
private async issueLogin(
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
opts?: { skipLastLogin?: boolean; fromPassword?: boolean },
|
||||
opts?: { skipLastLogin?: boolean; fromPassword?: boolean; viaMaster?: boolean },
|
||||
): Promise<LoginResponse> {
|
||||
const membership = await this.memberships.requireActive(userId, organizationId);
|
||||
if (opts?.fromPassword) {
|
||||
if (opts?.fromPassword && !opts.viaMaster) {
|
||||
const gate = await this.prisma.membership.findFirst({
|
||||
where: { userId, organizationId, isActive: true },
|
||||
select: { loginOtpOnly: true },
|
||||
|
||||
Reference in New Issue
Block a user