Skip to content

Commit 20774d7

Browse files
author
Uttam Singh
committed
Added invite-based user creation flow and updated models
1 parent 928b558 commit 20774d7

File tree

6 files changed

+195
-92
lines changed

6 files changed

+195
-92
lines changed

backend/app/database.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
from sqlalchemy import create_engine
22
from sqlalchemy.orm import sessionmaker, declarative_base
33
import os
4+
from dotenv import load_dotenv
5+
6+
load_dotenv()
47

58
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./app.db")
69

7-
engine = create_engine(
8-
DATABASE_URL,
9-
connect_args={"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}
10-
)
10+
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
1111
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
12-
Base = declarative_base()
1312

14-
def get_db():
15-
db = SessionLocal()
16-
try:
17-
yield db
18-
finally:
19-
db.close()
13+
# Global Base for ALL models
14+
Base = declarative_base()

backend/app/main.py

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
11
from fastapi import FastAPI, UploadFile, File, Depends, HTTPException
22
from fastapi.middleware.cors import CORSMiddleware
3-
from sqlalchemy import create_engine, Column, Integer, String, Date, Text
4-
from sqlalchemy.orm import sessionmaker, declarative_base, Session
3+
from sqlalchemy import Column, Integer, String, Date, Text
4+
from sqlalchemy.orm import Session
55
from datetime import date
66
from dotenv import load_dotenv
7-
from pydantic import BaseModel
8-
import os, shutil
7+
import os
8+
import shutil
99

1010
# ================================================
11-
# 1️⃣ Load Environment Variables & Database Setup
11+
# 1️⃣ Load Environment Variables + Shared Database
1212
# ================================================
1313
load_dotenv()
1414

15-
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./app.db")
16-
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
17-
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
18-
Base = declarative_base()
15+
# ❗ IMPORT SHARED BASE + ENGINE (do NOT create Base here)
16+
from app.database import Base, engine, SessionLocal
1917

2018
# ================================================
21-
# 2️⃣ Local Models for Internal Tables (Task + Audit Log)
19+
# 2️⃣ Models (Task + AuditLog) using shared Base
2220
# ================================================
2321
class Task(Base):
2422
__tablename__ = "tasks"
23+
2524
id = Column(Integer, primary_key=True, index=True)
2625
title = Column(String(255), nullable=False)
2726
department = Column(String(100), nullable=True)
@@ -35,26 +34,29 @@ class Task(Base):
3534

3635
class AuditLog(Base):
3736
__tablename__ = "audit_logs"
37+
3838
id = Column(Integer, primary_key=True)
3939
action = Column(String(50))
4040
detail = Column(Text)
4141

4242

4343
# ================================================
44-
# 3️⃣ FastAPI App Initialization
44+
# 3️⃣ FastAPI App Init
4545
# ================================================
4646
app = FastAPI(title="FAT-EIBL (Edme) – API")
4747

48-
# Enable CORS for the frontend
49-
allow = os.getenv("ALLOW_ORIGINS", "*").split(",")
48+
# CORS setup
49+
allowed_origins = os.getenv("ALLOW_ORIGINS", "*").split(",")
50+
5051
app.add_middleware(
5152
CORSMiddleware,
52-
allow_origins=allow,
53+
allow_origins=allowed_origins,
5354
allow_credentials=True,
5455
allow_methods=["*"],
5556
allow_headers=["*"],
5657
)
5758

59+
5860
# ================================================
5961
# 4️⃣ Database Dependency
6062
# ================================================
@@ -65,63 +67,60 @@ def get_db():
6567
finally:
6668
db.close()
6769

70+
6871
# ================================================
69-
# 5️⃣ Import Routers (Users, Forgot Password)
72+
# 5️⃣ Routers
7073
# ================================================
7174
from app.routers import users, forgot_password
7275

7376
app.include_router(users.router, prefix="/users", tags=["Users"])
74-
app.include_router(forgot_password.router, prefix="/users", tags=["Forgot Password"])
77+
app.include_router(forgot_password.router, prefix="/auth", tags=["Forgot Password"])
78+
7579

7680
# ================================================
77-
# 6️⃣ Health Check Endpoint
81+
# 6️⃣ Health Check
7882
# ================================================
7983
@app.get("/health")
8084
def health_check():
81-
"""Simple endpoint to verify backend health"""
82-
return {"status": "ok", "message": "Backend is running properly"}
85+
return {"status": "ok", "message": "Backend running"}
86+
8387

8488
# ================================================
85-
# 7️⃣ File Upload Handler (Optional)
89+
# 7️⃣ File Upload
8690
# ================================================
8791
UPLOAD_DIR = os.path.join(os.getcwd(), "uploads")
8892
os.makedirs(UPLOAD_DIR, exist_ok=True)
8993

94+
9095
@app.post("/upload/{task_id}")
9196
async def upload_file(task_id: int, file: UploadFile = File(...), db: Session = Depends(get_db)):
92-
"""Attach a file to a specific task"""
93-
obj = db.get(Task, task_id)
94-
if not obj:
97+
task = db.get(Task, task_id)
98+
if not task:
9599
raise HTTPException(status_code=404, detail="Task not found")
96100

97-
safe_name = f"task_{task_id}_" + os.path.basename(file.filename)
98-
dest = os.path.join(UPLOAD_DIR, safe_name)
101+
filename = f"task_{task_id}_" + os.path.basename(file.filename)
102+
path = os.path.join(UPLOAD_DIR, filename)
99103

100-
with open(dest, "wb") as buffer:
104+
with open(path, "wb") as buffer:
101105
shutil.copyfileobj(file.file, buffer)
102106

103-
obj.attachment = safe_name
104-
db.add(AuditLog(action="upload", detail=f"Task ID {task_id} uploaded file {safe_name}"))
107+
task.attachment = filename
108+
db.add(AuditLog(action="upload", detail=f"File uploaded for task {task_id}"))
105109
db.commit()
106110

107-
return {"task_id": task_id, "filename": safe_name}
111+
return {"ok": True, "filename": filename}
112+
108113

109114
# ================================================
110-
# 8️⃣ Database Initialization Endpoint (/create-db)
115+
# 8️⃣ Create All Tables
111116
# ================================================
112117
from app.models.user import User
113118
from app.models.otp import OtpModel
114-
from app.database import Base as DBBase, engine as DBEngine
115119

116120
@app.get("/create-db")
117-
def create_database():
118-
"""
119-
🔧 Create all database tables if they don't exist.
120-
Run this once on Render or local after migrations.
121-
"""
121+
def create_db():
122122
try:
123-
DBBase.metadata.create_all(bind=DBEngine)
124123
Base.metadata.create_all(bind=engine)
125-
return {"ok": True, "message": "✅ All database tables created successfully"}
124+
return {"ok": True, "message": "Tables created"}
126125
except Exception as e:
127126
return {"ok": False, "error": str(e)}

backend/app/models/user.py

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

54
class User(Base):
65
__tablename__ = "users"
6+
77
id = Column(Integer, primary_key=True, index=True)
8-
name = Column(String(255))
8+
name = Column(String(255), nullable=False)
99
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")
1410

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)
11+
# New: password saved ONLY after invite is accepted
12+
hashed_password = Column(String(255), nullable=True)
13+
14+
department = Column(String(255), nullable=True)
15+
role = Column(String(50), nullable=False, default="auditee")
16+
manager_email = Column(String(255), nullable=True)
17+
18+
# NEW FIELDS FOR INVITE FLOW
19+
status = Column(String(50), nullable=False, default="invited") # invited / active
20+
invite_token_hash = Column(String(255), nullable=True)
21+
invite_expires_at = Column(DateTime, nullable=True)

backend/app/routers/users.py

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,117 @@
1-
# app/routers/users.py (or your existing users router)
21
from fastapi import APIRouter, Depends, HTTPException
32
from sqlalchemy.orm import Session
43
from app.database import get_db
54
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
5+
from app.utils.auth import hash_password, send_invite_email
6+
from datetime import datetime, timedelta
7+
import secrets, hashlib
88

99
router = APIRouter()
1010

11-
@router.post("/")
12-
def create_user(
11+
12+
# ------------------------------------------------------
13+
# 1. SEND INVITE (called from frontend AdminUsers.jsx)
14+
# ------------------------------------------------------
15+
@router.post("/invite")
16+
def invite_user(
1317
name: str,
1418
email: str,
15-
password: str,
1619
department: str = None,
1720
manager_email: str = None,
1821
role: str = "auditee",
1922
db: Session = Depends(get_db),
2023
):
21-
# check exist
24+
# Check if user exists
2225
if db.query(User).filter(User.email == email).first():
2326
raise HTTPException(status_code=400, detail="Email already registered")
2427

25-
hashed = hash_password(password)
26-
otp = generate_otp()
27-
expiry = otp_expiry(10)
28+
# Generate invite token
29+
raw_token = secrets.token_urlsafe(32)
30+
hashed_token = hashlib.sha256(raw_token.encode()).hexdigest()
31+
expiry = datetime.utcnow() + timedelta(hours=24)
2832

33+
# Create user
2934
user = User(
3035
name=name,
3136
email=email,
32-
password=hashed,
3337
department=department,
3438
manager_email=manager_email,
3539
role=role,
36-
otp_code=otp,
37-
otp_expires_at=expiry,
40+
status="invited",
41+
invite_token_hash=hashed_token,
42+
invite_expires_at=expiry,
3843
first_login=True,
3944
)
4045
db.add(user)
4146
db.commit()
4247
db.refresh(user)
4348

44-
# send mail (async if you prefer background task)
49+
# Invite link
50+
invite_link = f"https://{YOUR_RENDER_FRONTEND_DOMAIN}/set-password?token={raw_token}&email={email}"
51+
52+
# Send invite email
4553
try:
46-
send_otp_email(email, otp)
54+
send_invite_email(email, invite_link)
4755
except Exception as e:
48-
# log error but user exists now — you can rollback if you prefer
49-
print("Warning: failed to send OTP", e)
56+
print("Failed to send invite email:", e)
57+
58+
return {"ok": True, "message": "Invite sent successfully"}
59+
60+
61+
# ------------------------------------------------------
62+
# 2. COMPLETE INVITATION (User sets password)
63+
# ------------------------------------------------------
64+
@router.post("/complete-invite")
65+
def complete_invite(
66+
email: str,
67+
token: str,
68+
password: str,
69+
db: Session = Depends(get_db),
70+
):
71+
user = db.query(User).filter(User.email == email).first()
72+
73+
if not user or user.status != "invited":
74+
raise HTTPException(status_code=400, detail="Invalid invitation")
75+
76+
# Check expiry
77+
if datetime.utcnow() > user.invite_expires_at:
78+
raise HTTPException(status_code=400, detail="Invite expired")
79+
80+
# Verify token
81+
hashed_input = hashlib.sha256(token.encode()).hexdigest()
82+
if hashed_input != user.invite_token_hash:
83+
raise HTTPException(status_code=400, detail="Invalid token")
84+
85+
# Set password & activate
86+
user.password = hash_password(password)
87+
user.status = "active"
88+
user.invite_token_hash = None
89+
user.invite_expires_at = None
90+
91+
db.commit()
92+
db.refresh(user)
93+
94+
return {"ok": True, "message": "Password set successfully. You may login now."}
95+
96+
97+
# ------------------------------------------------------
98+
# 3. GET USERS (Used by AdminUsers.jsx)
99+
# ------------------------------------------------------
100+
@router.get("/")
101+
def get_users(db: Session = Depends(get_db)):
102+
return db.query(User).all()
103+
104+
105+
# ------------------------------------------------------
106+
# 4. DELETE USER (Used by AdminUsers.jsx)
107+
# ------------------------------------------------------
108+
@router.delete("/{user_id}")
109+
def delete_user(user_id: int, db: Session = Depends(get_db)):
110+
user = db.query(User).filter(User.id == user_id).first()
111+
if not user:
112+
raise HTTPException(status_code=404, detail="User not found")
113+
114+
db.delete(user)
115+
db.commit()
50116

51-
return {"ok": True, "message": "User created and OTP sent"}
117+
return {"ok": True, "message": "User deleted"}

frontend/src.zip

26 KB
Binary file not shown.

0 commit comments

Comments
 (0)