Skip to content

Commit b75493b

Browse files
author
Uttam Singh
committed
Added OTP login flow, reset password flow, email sending, user create updates
1 parent c9adf61 commit b75493b

File tree

5 files changed

+143
-147
lines changed

5 files changed

+143
-147
lines changed

backend/app/models/user.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
from sqlalchemy import Column, Integer, String
2-
from app.database import Base
1+
# app/models/user.py (SQLAlchemy)
2+
from sqlalchemy import Column, Integer, String, Boolean, DateTime
3+
from datetime import datetime
34

45
class User(Base):
56
__tablename__ = "users"
6-
77
id = Column(Integer, primary_key=True, index=True)
8-
name = Column(String(100), nullable=False)
9-
email = Column(String(100), unique=True, nullable=False)
10-
hashed_password = Column(String(255), nullable=False)
11-
department = Column(String(100), nullable=True)
12-
role = Column(String(50), nullable=False, default="auditee")
13-
manager_email = Column(String(100), nullable=True)
8+
name = Column(String(255))
9+
email = Column(String(255), unique=True, index=True, nullable=False)
10+
password = Column(String(255), nullable=False) # hashed
11+
department = Column(String(100))
12+
manager_email = Column(String(255))
13+
role = Column(String(50), default="auditee")
14+
15+
# OTP + reset fields
16+
otp_code = Column(String(10), nullable=True)
17+
otp_expires_at = Column(DateTime, nullable=True)
18+
first_login = Column(Boolean, default=True)

backend/app/routers/auth.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# app/routers/auth.py
2+
from fastapi import APIRouter, HTTPException, Depends
3+
from sqlalchemy.orm import Session
4+
from datetime import datetime
5+
from app.database import get_db
6+
from app.models.user import User
7+
8+
router = APIRouter()
9+
10+
@router.post("/login-otp")
11+
def login_with_otp(email: str, otp: str, db: Session = Depends(get_db)):
12+
user = db.query(User).filter(User.email == email).first()
13+
if not user:
14+
raise HTTPException(status_code=400, detail="Invalid credentials")
15+
if not user.otp_code or user.otp_code != otp:
16+
raise HTTPException(status_code=400, detail="Invalid OTP")
17+
if not user.otp_expires_at or datetime.utcnow() > user.otp_expires_at:
18+
raise HTTPException(status_code=400, detail="OTP expired")
19+
20+
# OTP valid -> clear otp_code (optional)
21+
user.otp_code = None
22+
user.otp_expires_at = None
23+
db.commit()
24+
25+
# Return first_login so frontend knows to redirect
26+
return {"ok": True, "first_login": user.first_login, "user_id": user.id}
27+
from app.utils.auth import hash_password
28+
29+
@router.post("/reset-password")
30+
def reset_password(user_id: int, new_password: str, db: Session = Depends(get_db)):
31+
user = db.get(User, user_id)
32+
if not user:
33+
raise HTTPException(status_code=404, detail="User not found")
34+
35+
user.password = hash_password(new_password)
36+
user.first_login = False
37+
db.commit()
38+
return {"ok": True, "message": "Password updated"}

backend/app/routers/users.py

Lines changed: 38 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,51 @@
1-
from fastapi import APIRouter, Depends, HTTPException, Form
2-
from passlib.hash import bcrypt
1+
# app/routers/users.py (or your existing users router)
2+
from fastapi import APIRouter, Depends, HTTPException
33
from sqlalchemy.orm import Session
44
from app.database import get_db
55
from app.models.user import User
6+
from app.utils.auth import hash_password, generate_otp, otp_expiry, send_otp_email
7+
from datetime import datetime
68

7-
# ✅ Define router (this line is crucial)
89
router = APIRouter()
910

10-
# ✅ Create a new user
1111
@router.post("/")
1212
def create_user(
13-
name: str = Form(...),
14-
email: str = Form(...),
15-
password: str = Form(...),
16-
department: str = Form(None),
17-
role: str = Form("auditee"),
18-
manager_email: str = Form(None),
13+
name: str,
14+
email: str,
15+
password: str,
16+
department: str = None,
17+
manager_email: str = None,
18+
role: str = "auditee",
1919
db: Session = Depends(get_db),
2020
):
21+
# check exist
22+
if db.query(User).filter(User.email == email).first():
23+
raise HTTPException(status_code=400, detail="Email already registered")
24+
25+
hashed = hash_password(password)
26+
otp = generate_otp()
27+
expiry = otp_expiry(10)
28+
29+
user = User(
30+
name=name,
31+
email=email,
32+
password=hashed,
33+
department=department,
34+
manager_email=manager_email,
35+
role=role,
36+
otp_code=otp,
37+
otp_expires_at=expiry,
38+
first_login=True,
39+
)
40+
db.add(user)
41+
db.commit()
42+
db.refresh(user)
43+
44+
# send mail (async if you prefer background task)
2145
try:
22-
if db.query(User).filter(User.email == email).first():
23-
raise HTTPException(status_code=400, detail="Email already exists")
24-
user = User(
25-
name=name,
26-
email=email,
27-
hashed_password=bcrypt.hash(password),
28-
department=department,
29-
role=role,
30-
manager_email=manager_email,
31-
)
32-
db.add(user)
33-
db.commit()
34-
return {"ok": True, "message": "User created successfully"}
46+
send_otp_email(email, otp)
3547
except Exception as e:
36-
return {"ok": False, "error": str(e)}
48+
# log error but user exists now — you can rollback if you prefer
49+
print("Warning: failed to send OTP", e)
3750

38-
# ✅ Login route
39-
@router.post("/login")
40-
def login_user(
41-
email: str = Form(...),
42-
password: str = Form(...),
43-
db: Session = Depends(get_db)
44-
):
45-
try:
46-
user = db.query(User).filter(User.email == email).first()
47-
if not user or not bcrypt.verify(password, user.hashed_password):
48-
raise HTTPException(status_code=401, detail="Invalid email or password")
49-
return {
50-
"ok": True,
51-
"message": "Login successful",
52-
"user": {"id": user.id, "name": user.name, "role": user.role},
53-
}
54-
except Exception as e:
55-
return {"ok": False, "error": str(e)}
56-
57-
# ✅ Get all users
58-
@router.get("/")
59-
def list_users(db: Session = Depends(get_db)):
60-
try:
61-
return db.query(User).all()
62-
except Exception as e:
63-
return {"ok": False, "error": str(e)}
64-
65-
# ✅ Delete user
66-
@router.delete("/{user_id}")
67-
def delete_user(user_id: int, db: Session = Depends(get_db)):
68-
try:
69-
user = db.get(User, user_id)
70-
if not user:
71-
raise HTTPException(status_code=404, detail="User not found")
72-
db.delete(user)
73-
db.commit()
74-
return {"ok": True, "message": "User deleted"}
75-
except Exception as e:
76-
return {"ok": False, "error": str(e)}
77-
78-
# ✅ Check Admin Users
79-
@router.get("/check-admin")
80-
def check_admin(db: Session = Depends(get_db)):
81-
try:
82-
users = db.query(User).all()
83-
return {"count": len(users), "users": [u.email for u in users]}
84-
except Exception as e:
85-
return {"ok": False, "error": str(e)}
86-
87-
# ✅ Seed Admin User (One-Time Setup) – FIXED bcrypt byte issue
88-
@router.post("/seed-admin")
89-
def seed_admin(db: Session = Depends(get_db)):
90-
try:
91-
92-
existing = db.query(User).filter(User.email == email).first()
93-
if existing:
94-
return {"ok": True, "note": "Admin already exists"}
95-
96-
# --- Fix bcrypt 72-byte password limit ---
97-
raw_password = "Edme@123"
98-
encoded = raw_password.encode("utf-8")
99-
if len(encoded) > 72:
100-
encoded = encoded[:72]
101-
safe_password = encoded.decode("utf-8", "ignore")
102-
hashed_password = bcrypt.hash(safe_password)
103-
# ------------------------------------------
104-
105-
admin = User(
106-
name="Admin",
107-
email=email,
108-
hashed_password=hashed_password,
109-
department="Finance",
110-
role="admin",
111-
manager_email=None,
112-
)
113-
db.add(admin)
114-
db.commit()
115-
return {"ok": True, "note": "Admin created successfully"}
116-
except Exception as e:
117-
return {"ok": False, "error": str(e)}
51+
return {"ok": True, "message": "User created and OTP sent"}

backend/app/utils/auth.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# app/utils/auth.py
2+
import random
3+
from datetime import datetime, timedelta
4+
from passlib.context import CryptContext
5+
import os, smtplib
6+
from email.mime.text import MIMEText
7+
from email.mime.multipart import MIMEMultipart
8+
9+
pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")
10+
11+
def hash_password(plain):
12+
return pwd_ctx.hash(plain)
13+
14+
def verify_password(plain, hashed):
15+
return pwd_ctx.verify(plain, hashed)
16+
17+
def generate_otp():
18+
return f"{random.randint(100000, 999999)}" # 6-digit string
19+
20+
def otp_expiry(minutes=10):
21+
return datetime.utcnow() + timedelta(minutes=minutes)
22+
23+
def send_otp_email(recipient_email: str, otp: str):
24+
# Configure via env vars
25+
sender_email = os.getenv("SMTP_SENDER_EMAIL")
26+
sender_password = os.getenv("SMTP_PASSWORD")
27+
smtp_server = os.getenv("SMTP_SERVER", "smtp.office365.com")
28+
smtp_port = int(os.getenv("SMTP_PORT", 587))
29+
30+
subject = "Your FAT-EIBL one-time password (OTP)"
31+
body = f"""
32+
Hello,
33+
34+
Use this one-time password (OTP) to sign in to FAT-EIBL: {otp}
35+
36+
This OTP will expire in 10 minutes.
37+
38+
If you didn't request this, contact your admin.
39+
40+
Regards,
41+
FAT-EIBL
42+
"""
43+
44+
msg = MIMEMultipart()
45+
msg["From"] = sender_email
46+
msg["To"] = recipient_email
47+
msg["Subject"] = subject
48+
msg.attach(MIMEText(body, "plain"))
49+
50+
with smtplib.SMTP(smtp_server, smtp_port) as server:
51+
server.starttls()
52+
server.login(sender_email, sender_password)
53+
server.send_message(msg)

frontend/index.html

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)