Skip to content

Commit 51cf300

Browse files
author
Uttam Singh
committed
Added admin user management
1 parent 7d070e2 commit 51cf300

File tree

9 files changed

+247
-37
lines changed

9 files changed

+247
-37
lines changed

backend/Dockerfile

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1+
# Use Python base image
12
FROM python:3.11-slim
23

4+
# Set working directory
35
WORKDIR /app
46

5-
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
6-
7-
# Copy requirements.txt from backend folder (Render context fix)
7+
# Copy and install dependencies
88
COPY backend/requirements.txt ./requirements.txt
9-
109
RUN pip install --no-cache-dir -r requirements.txt
1110

12-
# Copy the rest of the backend code
11+
# Copy the app source
1312
COPY backend/. .
1413

14+
# Expose FastAPI default port
15+
EXPOSE 8000
16+
17+
# Run the FastAPI app
1518
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

backend/app/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,4 @@ def ai_chat(prompt: str = Form(...)):
178178

179179
# --- USER ROUTES ---
180180
app.include_router(users.router, prefix="/users", tags=["Users"])
181+

backend/app/models/user.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
from sqlalchemy import Column, Integer, String
2-
from sqlalchemy.orm import declarative_base
3-
4-
Base = declarative_base()
1+
from sqlalchemy import Column, Integer, String, Boolean
2+
from app.database import Base
53

64
class User(Base):
75
__tablename__ = "users"
8-
96
id = Column(Integer, primary_key=True, index=True)
10-
username = Column(String(100), unique=True, nullable=False)
11-
password = Column(String(255), nullable=False)
12-
role = Column(String(50), default="member")
7+
name = Column(String(120), nullable=False)
8+
email = Column(String(255), unique=True, index=True, nullable=False)
9+
hashed_password = Column(String(255), nullable=False)
10+
department = Column(String(100)) # e.g., Accounts, Compliance, Ops
11+
role = Column(String(50)) # admin, auditor, auditee, manager
12+
manager_email = Column(String(255)) # reporting manager
13+
is_active = Column(Boolean, default=True)

backend/app/routers/users.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,47 @@
1-
from fastapi import APIRouter, HTTPException, Depends
1+
from fastapi import APIRouter, Depends, HTTPException, Form
2+
from passlib.hash import bcrypt
23
from sqlalchemy.orm import Session
3-
from app.main import get_db
4+
from app.database import get_db
5+
from app.models.user import User
46

5-
router = APIRouter(prefix="/users", tags=["Users"])
7+
router = APIRouter()
68

9+
# List all users
710
@router.get("/")
811
def list_users(db: Session = Depends(get_db)):
9-
# For now, just a placeholder
10-
return {"message": "User list (placeholder)"}
12+
return db.query(User).all()
1113

12-
@router.post("/login")
13-
def login(username: str, password: str, db: Session = Depends(get_db)):
14-
# Simple fake login logic (replace later)
15-
if username == "admin" and password == "admin123":
16-
return {"message": "Login successful", "role": "admin"}
17-
raise HTTPException(status_code=401, detail="Invalid credentials")
14+
# Create new user
15+
@router.post("/")
16+
def create_user(
17+
name: str = Form(...),
18+
email: str = Form(...),
19+
password: str = Form(...),
20+
department: str = Form(None),
21+
role: str = Form("auditee"),
22+
manager_email: str = Form(None),
23+
db: Session = Depends(get_db),
24+
):
25+
if db.query(User).filter(User.email == email).first():
26+
raise HTTPException(status_code=400, detail="Email already exists")
27+
user = User(
28+
name=name,
29+
email=email,
30+
hashed_password=bcrypt.hash(password),
31+
department=department,
32+
role=role,
33+
manager_email=manager_email,
34+
)
35+
db.add(user)
36+
db.commit()
37+
return {"ok": True}
38+
39+
# Delete user
40+
@router.delete("/{user_id}")
41+
def delete_user(user_id: int, db: Session = Depends(get_db)):
42+
user = db.get(User, user_id)
43+
if not user:
44+
raise HTTPException(status_code=404, detail="User not found")
45+
db.delete(user)
46+
db.commit()
47+
return {"ok": True}

backend/app/schemas/user.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from pydantic import BaseModel
2+
3+
class UserCreate(BaseModel):
4+
username: str
5+
password: str
6+
role: str = "user"
7+
8+
class UserLogin(BaseModel):
9+
username: str
10+
password: str
11+
12+
class UserOut(BaseModel):
13+
id: int
14+
username: str
15+
role: str
16+
17+
class Config:
18+
from_attributes = True

frontend/src/assets/logo.png.png

-9.88 KB
Binary file not shown.

frontend/src/main.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ ReactDOM.createRoot(document.getElementById("root")).render(
88
<App />
99
</React.StrictMode>
1010
);
11+
import AdminUsers from "./pages/AdminUsers.jsx";
12+
13+
// Add inside Routes:
14+
<Route path="/admin/users" element={<AdminUsers />} />
15+

frontend/src/pages/AdminUsers.jsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import React, { useState, useEffect } from "react";
2+
const API = import.meta.env.VITE_API_URL;
3+
4+
export default function AdminUsers() {
5+
const [users, setUsers] = useState([]);
6+
const [form, setForm] = useState({
7+
name: "",
8+
email: "",
9+
password: "",
10+
department: "",
11+
role: "auditee",
12+
manager_email: "",
13+
});
14+
15+
const loadUsers = async () => {
16+
const res = await fetch(`${API}/users`);
17+
const data = await res.json();
18+
setUsers(data);
19+
};
20+
21+
useEffect(() => {
22+
loadUsers();
23+
}, []);
24+
25+
const createUser = async (e) => {
26+
e.preventDefault();
27+
const fd = new FormData();
28+
Object.entries(form).forEach(([k, v]) => fd.append(k, v));
29+
const res = await fetch(`${API}/users`, { method: "POST", body: fd });
30+
if (res.ok) {
31+
alert("User created");
32+
loadUsers();
33+
} else alert("Error creating user");
34+
};
35+
36+
const deleteUser = async (id) => {
37+
if (!window.confirm("Are you sure?")) return;
38+
await fetch(`${API}/users/${id}`, { method: "DELETE" });
39+
loadUsers();
40+
};
41+
42+
return (
43+
<div style={{ maxWidth: "800px", margin: "40px auto" }}>
44+
<h2>Admin – User Management</h2>
45+
<form onSubmit={createUser} style={{ display: "grid", gap: 10 }}>
46+
<input placeholder="Name" onChange={(e) => setForm({ ...form, name: e.target.value })} />
47+
<input placeholder="Email" onChange={(e) => setForm({ ...form, email: e.target.value })} />
48+
<input type="password" placeholder="Password" onChange={(e) => setForm({ ...form, password: e.target.value })} />
49+
<input placeholder="Department" onChange={(e) => setForm({ ...form, department: e.target.value })} />
50+
<input placeholder="Manager Email" onChange={(e) => setForm({ ...form, manager_email: e.target.value })} />
51+
<select onChange={(e) => setForm({ ...form, role: e.target.value })}>
52+
<option value="auditee">Auditee</option>
53+
<option value="auditor">Auditor</option>
54+
<option value="manager">Manager</option>
55+
<option value="admin">Admin</option>
56+
</select>
57+
<button>Create User</button>
58+
</form>
59+
60+
<h3 style={{ marginTop: "20px" }}>User List</h3>
61+
<table border="1" cellPadding="8" width="100%">
62+
<thead>
63+
<tr>
64+
<th>Name</th>
65+
<th>Email</th>
66+
<th>Dept</th>
67+
<th>Role</th>
68+
<th>Manager</th>
69+
<th>Delete</th>
70+
</tr>
71+
</thead>
72+
<tbody>
73+
{users.map((u) => (
74+
<tr key={u.id}>
75+
<td>{u.name}</td>
76+
<td>{u.email}</td>
77+
<td>{u.department}</td>
78+
<td>{u.role}</td>
79+
<td>{u.manager_email}</td>
80+
<td>
81+
<button onClick={() => deleteUser(u.id)}></button>
82+
</td>
83+
</tr>
84+
))}
85+
</tbody>
86+
</table>
87+
</div>
88+
);
89+
}

frontend/src/pages/Login.jsx

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,64 @@ import logo from "../assets/logo.png";
33
import "./Login.css";
44

55
export default function Login() {
6-
const [email, setEmail] = useState("");
6+
const [username, setUsername] = useState("");
77
const [password, setPassword] = useState("");
8+
const [loading, setLoading] = useState(false);
89

9-
const handleSubmit = (e) => {
10+
// ✅ TEMP ADMIN LOGIN (for testing)
11+
const ADMIN_ID = "[email protected]";
12+
const ADMIN_PASS = "Admin@123";
13+
14+
const handleSubmit = async (e) => {
1015
e.preventDefault();
11-
// Replace this with your backend API call later
12-
if (email === "[email protected]" && password === "password123") {
16+
setLoading(true);
17+
18+
// ✅ Step 1: Local hardcoded admin check (for testing)
19+
if (username === ADMIN_ID && password === ADMIN_PASS) {
20+
alert("Admin login successful!");
21+
localStorage.setItem("role", "admin");
1322
window.location.href = "/dashboard";
14-
} else {
15-
alert("Invalid credentials");
23+
setLoading(false);
24+
return;
25+
}
26+
27+
// ✅ Step 2: (optional) Try backend login if not admin
28+
try {
29+
const response = await fetch(
30+
"https://fat-eibl-backend.onrender.com/users/login",
31+
{
32+
method: "POST",
33+
headers: { "Content-Type": "application/json" },
34+
body: JSON.stringify({ username, password }),
35+
}
36+
);
37+
38+
const data = await response.json();
39+
40+
if (response.ok) {
41+
localStorage.setItem("token", data.access_token);
42+
localStorage.setItem("role", data.role);
43+
44+
if (data.role === "admin") {
45+
window.location.href = "/dashboard";
46+
} else {
47+
window.location.href = "/user-dashboard";
48+
}
49+
} else {
50+
alert(data.detail || "Invalid username or password");
51+
}
52+
} catch (error) {
53+
console.error("Login failed:", error);
54+
alert("Server error. Please try again later.");
55+
} finally {
56+
setLoading(false);
1657
}
1758
};
1859

60+
const handleForgotPassword = () => {
61+
alert("Forgot Password feature coming soon. Please contact Admin.");
62+
};
63+
1964
return (
2065
<div
2166
style={{
@@ -27,7 +72,11 @@ export default function Login() {
2772
backgroundColor: "#f5f9ff",
2873
}}
2974
>
30-
<img src={logo} alt="Company Logo" style={{ width: "120px", marginBottom: "20px" }} />
75+
<img
76+
src={logo}
77+
alt="Company Logo"
78+
style={{ width: "120px", marginBottom: "20px" }}
79+
/>
3180
<h1 style={{ color: "#004aad" }}>Welcome to FAT-EIBL</h1>
3281
<p style={{ color: "#003b80", marginBottom: "30px" }}>
3382
Finance Audit Tracker – Edme Insurance Brokers Limited
@@ -45,10 +94,10 @@ export default function Login() {
4594
}}
4695
>
4796
<input
48-
type="email"
49-
placeholder="Email"
50-
value={email}
51-
onChange={(e) => setEmail(e.target.value)}
97+
type="text"
98+
placeholder="Enter Username / Email"
99+
value={username}
100+
onChange={(e) => setUsername(e.target.value)}
52101
style={{
53102
width: "100%",
54103
padding: "10px",
@@ -60,7 +109,7 @@ export default function Login() {
60109
/>
61110
<input
62111
type="password"
63-
placeholder="Password"
112+
placeholder="Enter Password"
64113
value={password}
65114
onChange={(e) => setPassword(e.target.value)}
66115
style={{
@@ -72,6 +121,7 @@ export default function Login() {
72121
}}
73122
required
74123
/>
124+
75125
<button
76126
type="submit"
77127
style={{
@@ -83,9 +133,22 @@ export default function Login() {
83133
cursor: "pointer",
84134
width: "100%",
85135
}}
136+
disabled={loading}
86137
>
87-
Login
138+
{loading ? "Logging in..." : "Login"}
88139
</button>
140+
141+
<p
142+
onClick={handleForgotPassword}
143+
style={{
144+
color: "#004aad",
145+
marginTop: "15px",
146+
cursor: "pointer",
147+
textDecoration: "underline",
148+
}}
149+
>
150+
Forgot Password?
151+
</p>
89152
</form>
90153
</div>
91154
);

0 commit comments

Comments
 (0)