Upload app.py
Browse files
app.py
CHANGED
|
@@ -244,6 +244,16 @@ def init_db():
|
|
| 244 |
FOREIGN KEY(user_id) REFERENCES users(id)
|
| 245 |
)
|
| 246 |
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
db.execute("""
|
| 248 |
CREATE TABLE IF NOT EXISTS projects (
|
| 249 |
id TEXT PRIMARY KEY,
|
|
@@ -350,6 +360,7 @@ def init_db():
|
|
| 350 |
db.execute("ALTER TABLE users ADD COLUMN id_card TEXT") if not _column_exists(db,'users','id_card') else None
|
| 351 |
db.execute("ALTER TABLE users ADD COLUMN id_status TEXT DEFAULT 'none'") if not _column_exists(db,'users','id_status') else None
|
| 352 |
db.execute("ALTER TABLE users ADD COLUMN active INTEGER DEFAULT 1") if not _column_exists(db,'users','active') else None
|
|
|
|
| 353 |
db.execute("CREATE INDEX IF NOT EXISTS idx_interests_project ON interests(project_id)")
|
| 354 |
db.execute("CREATE INDEX IF NOT EXISTS idx_files_project ON project_files(project_id)")
|
| 355 |
db.execute("CREATE INDEX IF NOT EXISTS idx_appt_project ON appointments(project_id)")
|
|
@@ -380,6 +391,25 @@ def create_session(db, user_id: str) -> str:
|
|
| 380 |
return token
|
| 381 |
|
| 382 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
def current_user(authorization: Optional[str]) -> dict:
|
| 384 |
if not authorization or not authorization.startswith("Bearer "):
|
| 385 |
raise HTTPException(401, "لازم تسجل دخول")
|
|
@@ -436,6 +466,14 @@ class ResetPasswordIn(BaseModel):
|
|
| 436 |
new_password: str = Field(min_length=6, max_length=100)
|
| 437 |
|
| 438 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
class ProjectIn(BaseModel):
|
| 440 |
name: str = Field(min_length=1, max_length=200)
|
| 441 |
sector: str
|
|
@@ -503,7 +541,7 @@ def row_to_public_user(row: sqlite3.Row) -> dict:
|
|
| 503 |
|
| 504 |
|
| 505 |
@app.post("/api/auth/signup")
|
| 506 |
-
def signup(body: SignupIn, request: Request):
|
| 507 |
rate_limit(request, "signup", max_calls=5, window_sec=3600)
|
| 508 |
if body.role not in ("owner", "investor", "both", "broker"):
|
| 509 |
raise HTTPException(400, "نوع الحساب لازم يكون owner أو investor أو both")
|
|
@@ -515,13 +553,13 @@ def signup(body: SignupIn, request: Request):
|
|
| 515 |
raise HTTPException(409, "الإيميل ده مسجل قبل كده")
|
| 516 |
db.execute(
|
| 517 |
"INSERT INTO users (id,name,email,phone,password_hash,salt,role,created_at,bio,owner_field,"
|
| 518 |
-
"investor_budget_min,investor_budget_max,investor_sectors) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
| 519 |
(uid, body.name, body.email, body.phone, h, salt, body.role, int(time.time()), body.bio,
|
| 520 |
body.owner_field, body.investor_budget_min, body.investor_budget_max, body.investor_sectors),
|
| 521 |
)
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
|
| 526 |
|
| 527 |
@app.post("/api/auth/login")
|
|
@@ -533,10 +571,39 @@ def login(body: LoginIn, request: Request):
|
|
| 533 |
raise HTTPException(401, "الإيميل أو الباسورد غلط")
|
| 534 |
if not row["active"]:
|
| 535 |
raise HTTPException(403, "الحساب ده معطّل. لو ده غلط، تواصل معانا")
|
|
|
|
|
|
|
| 536 |
token = create_session(db, row["id"])
|
| 537 |
return {"token": token, "user": row_to_public_user(row)}
|
| 538 |
|
| 539 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
@app.post("/api/auth/forgot-password")
|
| 541 |
def forgot_password(body: ForgotPasswordIn, request: Request, background_tasks: BackgroundTasks):
|
| 542 |
rate_limit(request, "forgot_password", max_calls=4, window_sec=900)
|
|
|
|
| 244 |
FOREIGN KEY(user_id) REFERENCES users(id)
|
| 245 |
)
|
| 246 |
""")
|
| 247 |
+
db.execute("""
|
| 248 |
+
CREATE TABLE IF NOT EXISTS email_verifications (
|
| 249 |
+
token TEXT PRIMARY KEY,
|
| 250 |
+
user_id TEXT NOT NULL,
|
| 251 |
+
created_at INTEGER NOT NULL,
|
| 252 |
+
expires_at INTEGER NOT NULL,
|
| 253 |
+
used INTEGER DEFAULT 0,
|
| 254 |
+
FOREIGN KEY(user_id) REFERENCES users(id)
|
| 255 |
+
)
|
| 256 |
+
""")
|
| 257 |
db.execute("""
|
| 258 |
CREATE TABLE IF NOT EXISTS projects (
|
| 259 |
id TEXT PRIMARY KEY,
|
|
|
|
| 360 |
db.execute("ALTER TABLE users ADD COLUMN id_card TEXT") if not _column_exists(db,'users','id_card') else None
|
| 361 |
db.execute("ALTER TABLE users ADD COLUMN id_status TEXT DEFAULT 'none'") if not _column_exists(db,'users','id_status') else None
|
| 362 |
db.execute("ALTER TABLE users ADD COLUMN active INTEGER DEFAULT 1") if not _column_exists(db,'users','active') else None
|
| 363 |
+
db.execute("ALTER TABLE users ADD COLUMN email_verified INTEGER DEFAULT 0") if not _column_exists(db,'users','email_verified') else None
|
| 364 |
db.execute("CREATE INDEX IF NOT EXISTS idx_interests_project ON interests(project_id)")
|
| 365 |
db.execute("CREATE INDEX IF NOT EXISTS idx_files_project ON project_files(project_id)")
|
| 366 |
db.execute("CREATE INDEX IF NOT EXISTS idx_appt_project ON appointments(project_id)")
|
|
|
|
| 391 |
return token
|
| 392 |
|
| 393 |
|
| 394 |
+
def send_verification_email(db, background_tasks: BackgroundTasks, user_id: str, name: str, email: str):
|
| 395 |
+
token = secrets.token_urlsafe(32)
|
| 396 |
+
now = int(time.time())
|
| 397 |
+
db.execute(
|
| 398 |
+
"INSERT INTO email_verifications (token,user_id,created_at,expires_at) VALUES (?,?,?,?)",
|
| 399 |
+
(token, user_id, now, now + 86400), # صالح 24 ساعة
|
| 400 |
+
)
|
| 401 |
+
link = f"{SITE_URL}/verify-email.html?token={token}"
|
| 402 |
+
html = email_html(
|
| 403 |
+
"أكّد إيميلك",
|
| 404 |
+
f"أهلاً {name}،",
|
| 405 |
+
["شكرًا إنك عملت حساب على نيّة. خطوة واحدة باقية بس عشان تفعّل حسابك.",
|
| 406 |
+
"دوس على الزرار تحت عشان تأكّد إيميلك وتدخل حسابك على طول. الرابط صالح لمدة 24 ساعة.",
|
| 407 |
+
"لو ملطلبتش تسجيل حساب على نيّة، تجاهل الرسالة دي ببساطة."],
|
| 408 |
+
"تأكيد الإيميل والدخول", link,
|
| 409 |
+
)
|
| 410 |
+
background_tasks.add_task(send_email, email, "أكّد إيميلك — نيّة", html)
|
| 411 |
+
|
| 412 |
+
|
| 413 |
def current_user(authorization: Optional[str]) -> dict:
|
| 414 |
if not authorization or not authorization.startswith("Bearer "):
|
| 415 |
raise HTTPException(401, "لازم تسجل دخول")
|
|
|
|
| 466 |
new_password: str = Field(min_length=6, max_length=100)
|
| 467 |
|
| 468 |
|
| 469 |
+
class VerifyEmailIn(BaseModel):
|
| 470 |
+
token: str
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
class ResendVerifyIn(BaseModel):
|
| 474 |
+
email: EmailStr
|
| 475 |
+
|
| 476 |
+
|
| 477 |
class ProjectIn(BaseModel):
|
| 478 |
name: str = Field(min_length=1, max_length=200)
|
| 479 |
sector: str
|
|
|
|
| 541 |
|
| 542 |
|
| 543 |
@app.post("/api/auth/signup")
|
| 544 |
+
def signup(body: SignupIn, request: Request, background_tasks: BackgroundTasks):
|
| 545 |
rate_limit(request, "signup", max_calls=5, window_sec=3600)
|
| 546 |
if body.role not in ("owner", "investor", "both", "broker"):
|
| 547 |
raise HTTPException(400, "نوع الحساب لازم يكون owner أو investor أو both")
|
|
|
|
| 553 |
raise HTTPException(409, "الإيميل ده مسجل قبل كده")
|
| 554 |
db.execute(
|
| 555 |
"INSERT INTO users (id,name,email,phone,password_hash,salt,role,created_at,bio,owner_field,"
|
| 556 |
+
"investor_budget_min,investor_budget_max,investor_sectors,email_verified) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,0)",
|
| 557 |
(uid, body.name, body.email, body.phone, h, salt, body.role, int(time.time()), body.bio,
|
| 558 |
body.owner_field, body.investor_budget_min, body.investor_budget_max, body.investor_sectors),
|
| 559 |
)
|
| 560 |
+
send_verification_email(db, background_tasks, uid, body.name, body.email)
|
| 561 |
+
return {"needsVerification": True, "email": body.email,
|
| 562 |
+
"message": "اتبعت رسالة تأكيد لإيميلك. افتح الرابط اللي فيها عشان تفعّل حسابك وتدخل على طول."}
|
| 563 |
|
| 564 |
|
| 565 |
@app.post("/api/auth/login")
|
|
|
|
| 571 |
raise HTTPException(401, "الإيميل أو الباسورد غلط")
|
| 572 |
if not row["active"]:
|
| 573 |
raise HTTPException(403, "الحساب ده معطّل. لو ده غلط، تواصل معانا")
|
| 574 |
+
if "email_verified" in row.keys() and not row["email_verified"]:
|
| 575 |
+
raise HTTPException(403, "لازم تأكّد إيميلك الأول. اتبعتلك رسالة فيها رابط التفعيل — لو مش لاقيها استخدم زرار إعادة الإرسال")
|
| 576 |
token = create_session(db, row["id"])
|
| 577 |
return {"token": token, "user": row_to_public_user(row)}
|
| 578 |
|
| 579 |
|
| 580 |
+
@app.post("/api/auth/verify-email")
|
| 581 |
+
def verify_email(body: VerifyEmailIn):
|
| 582 |
+
with get_db() as db:
|
| 583 |
+
row = db.execute(
|
| 584 |
+
"SELECT * FROM email_verifications WHERE token=? AND used=0", (body.token,)
|
| 585 |
+
).fetchone()
|
| 586 |
+
if not row or row["expires_at"] < time.time():
|
| 587 |
+
raise HTTPException(400, "رابط التأكيد غير صالح أو منتهي — اطلب رابط جديد")
|
| 588 |
+
db.execute("UPDATE email_verifications SET used=1 WHERE token=?", (body.token,))
|
| 589 |
+
db.execute("UPDATE users SET email_verified=1 WHERE id=?", (row["user_id"],))
|
| 590 |
+
user = db.execute("SELECT * FROM users WHERE id=?", (row["user_id"],)).fetchone()
|
| 591 |
+
if not user["active"]:
|
| 592 |
+
raise HTTPException(403, "الحساب ده معطّل. لو ده غلط، تواصل معانا")
|
| 593 |
+
token = create_session(db, user["id"])
|
| 594 |
+
return {"token": token, "user": row_to_public_user(user)}
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
@app.post("/api/auth/resend-verification")
|
| 598 |
+
def resend_verification(body: ResendVerifyIn, request: Request, background_tasks: BackgroundTasks):
|
| 599 |
+
rate_limit(request, "resend_verification", max_calls=4, window_sec=900)
|
| 600 |
+
with get_db() as db:
|
| 601 |
+
user = db.execute("SELECT id,name,email_verified FROM users WHERE email=?", (body.email,)).fetchone()
|
| 602 |
+
if user and not user["email_verified"]:
|
| 603 |
+
send_verification_email(db, background_tasks, user["id"], user["name"], body.email)
|
| 604 |
+
return {"ok": True, "message": "لو الإيميل مسجل ومش مفعّل، هيوصلك رابط تأكيد جديد"}
|
| 605 |
+
|
| 606 |
+
|
| 607 |
@app.post("/api/auth/forgot-password")
|
| 608 |
def forgot_password(body: ForgotPasswordIn, request: Request, background_tasks: BackgroundTasks):
|
| 609 |
rate_limit(request, "forgot_password", max_calls=4, window_sec=900)
|