Redesign login/register with Mento branding and email/mobile OTP.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-05 00:47:33 +03:30
co-authored by Cursor
parent aa3fe49ce6
commit c620d1d6d4
12 changed files with 1792 additions and 184 deletions
@@ -0,0 +1,33 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "mobile" TEXT;
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "emailVerifiedAt" TIMESTAMP(3);
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "mobileVerifiedAt" TIMESTAMP(3);
-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "User_mobile_key" ON "User"("mobile");
CREATE INDEX IF NOT EXISTS "User_email_idx" ON "User"("email");
-- CreateTable
CREATE TABLE IF NOT EXISTS "AuthChallenge" (
"id" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"channel" TEXT,
"email" TEXT,
"mobile" TEXT,
"passwordHash" TEXT,
"payload" JSONB NOT NULL DEFAULT '{}',
"emailCode" TEXT,
"mobileCode" TEXT,
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
"mobileVerified" BOOLEAN NOT NULL DEFAULT false,
"expiresAt" TIMESTAMP(3) NOT NULL,
"consumedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AuthChallenge_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "AuthChallenge_kind_createdAt_idx" ON "AuthChallenge"("kind", "createdAt");
CREATE INDEX IF NOT EXISTS "AuthChallenge_expiresAt_idx" ON "AuthChallenge"("expiresAt");
CREATE INDEX IF NOT EXISTS "AuthChallenge_email_idx" ON "AuthChallenge"("email");
CREATE INDEX IF NOT EXISTS "AuthChallenge_mobile_idx" ON "AuthChallenge"("mobile");
+32
View File
@@ -172,9 +172,13 @@ model Organization {
model User {
id String @id @default(cuid())
email String
/// موبایل نرمال‌شده (مثلاً 9121234567) — برای ورود با موبایل
mobile String?
passwordHash String
fullName String
role UserRole @default(sales)
emailVerifiedAt DateTime?
mobileVerifiedAt DateTime?
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
ownedDeals Deal[] @relation("DealOwner")
@@ -197,7 +201,9 @@ model User {
updatedAt DateTime @updatedAt
@@unique([organizationId, email])
@@unique([mobile])
@@index([organizationId])
@@index([email])
}
model Company {
@@ -934,6 +940,32 @@ model AutomationRule {
@@index([organizationId, isActive, trigger])
}
/// چالش OTP برای ثبت‌نام / ورود پنل
model AuthChallenge {
id String @id @default(cuid())
/// register | login
kind String
/// برای login: email | mobile
channel String?
email String?
mobile String?
passwordHash String?
/// دادهٔ موقت ثبت‌نام (سازمان، نام، زمینه، کد معرف، …)
payload Json @default("{}")
emailCode String?
mobileCode String?
emailVerified Boolean @default(false)
mobileVerified Boolean @default(false)
expiresAt DateTime
consumedAt DateTime?
createdAt DateTime @default(now())
@@index([kind, createdAt])
@@index([expiresAt])
@@index([email])
@@index([mobile])
}
/// اتصال به نرم‌افزار حسابداری خارجی (ته‌حساب) — چند اتصال/DB به‌ازای هر سازمان
model AccountingConnection {
id String @id @default(cuid())
+20
View File
@@ -38,9 +38,12 @@ async function main() {
users: {
create: {
email,
mobile: '09120000001',
fullName: 'مدیر نمونه',
passwordHash,
role: 'owner',
emailVerifiedAt: new Date(),
mobileVerifiedAt: new Date(),
},
},
},
@@ -54,6 +57,17 @@ async function main() {
const owner = await prisma.user.findFirst({
where: { organizationId: org.id, email },
});
if (owner && !owner.mobile) {
await prisma.user.update({
where: { id: owner.id },
data: {
mobile: '09120000001',
emailVerifiedAt: owner.emailVerifiedAt ?? new Date(),
mobileVerifiedAt: owner.mobileVerifiedAt ?? new Date(),
},
});
console.log('Updated demo owner mobile: 09120000001');
}
// چندارزی نمونه
await prisma.organization.update({
@@ -93,9 +107,12 @@ async function main() {
data: {
organizationId: org.id,
email: salesEmail,
mobile: '09120000002',
fullName: 'کارشناس فروش نمونه',
passwordHash: await bcrypt.hash('demo1234', 10),
role: 'sales',
emailVerifiedAt: new Date(),
mobileVerifiedAt: new Date(),
},
});
console.log('Seeded sales user:', salesEmail, '/ demo1234');
@@ -110,9 +127,12 @@ async function main() {
data: {
organizationId: org.id,
email: adminEmail,
mobile: '09120000003',
fullName: 'مدیر سازمانی نمونه',
passwordHash: await bcrypt.hash('demo1234', 10),
role: 'admin',
emailVerifiedAt: new Date(),
mobileVerifiedAt: new Date(),
},
});
console.log('Seeded admin user:', adminEmail, '/ demo1234');