feat(auth): allow owner to force OTP-only login per user

Team menu «ورود با کد» blocks password login with a clear message and switches the login screen to OTP.
This commit is contained in:
2026-10-22 15:43:24 +03:30
parent 80fb5bf1bb
commit 65cf4e9a66
9 changed files with 181 additions and 11 deletions
+28
View File
@@ -49,6 +49,7 @@ type MembershipRow = {
priorArchiveEnabled?: boolean;
logoutTime?: string | null;
blockLoginAfterLogout?: boolean;
loginOtpOnly?: boolean;
createdAt: Date;
orgRole: { id: string; name: string; color: string; dataAccessPolicy?: unknown } | null;
user: {
@@ -454,6 +455,32 @@ export class UsersService {
return this.dtoOf(organizationId, targetId);
}
async updateLoginOtpOnly(
organizationId: string,
actor: { userId: string; role: UserRole },
targetId: string,
loginOtpOnly: boolean,
): Promise<OrgUserDto> {
if (actor.role !== 'owner' && actor.role !== 'admin') {
throw new ForbiddenException('فقط مالک یا مدیر می‌تواند شیوهٔ ورود را تغییر دهد');
}
const target = await this.memberships.findUserInOrgById(organizationId, targetId);
if (target.role === 'owner') {
throw new BadRequestException('برای مالک نمی‌توان ورود فقط با کد را فعال کرد');
}
if (loginOtpOnly) {
const mobile = target.user.mobile?.trim();
if (!mobile) {
throw new BadRequestException('برای ورود با کد، کاربر باید موبایل داشته باشد');
}
}
await this.prisma.membership.update({
where: { id: target.id },
data: { loginOtpOnly },
});
return this.dtoOf(organizationId, targetId);
}
async updateStatus(
organizationId: string,
actor: { userId: string; role: UserRole },
@@ -589,6 +616,7 @@ export class UsersService {
logoutTime: row.role === 'owner' ? null : row.logoutTime ?? null,
blockLoginAfterLogout:
row.role !== 'owner' && Boolean(row.logoutTime) && Boolean(row.blockLoginAfterLogout),
loginOtpOnly: row.role === 'owner' ? false : Boolean(row.loginOtpOnly),
createdAt: row.createdAt.toISOString(),
};
}