roshcheeku commited on
Commit
6153d81
·
verified ·
1 Parent(s): c7ec816

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +358 -0
app.py ADDED
@@ -0,0 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_file
2
+ from flask_cors import CORS
3
+ from datetime import datetime, timedelta
4
+ from pymongo import MongoClient
5
+ from bson import ObjectId
6
+ import jwt, bcrypt, secrets, io, csv, re
7
+
8
+ app = Flask(__name__)
9
+ CORS(app)
10
+
11
+ # -----------------------------
12
+ # Config
13
+ # -----------------------------
14
+ app.config["SECRET_KEY"] = "supersecretjwtkey"
15
+ app.config["JWT_EXP_DELTA_SECONDS"] = 3600
16
+
17
+ # ✅ MongoDB Atlas connection
18
+ client = MongoClient(
19
+ "mongodb+srv://drroshini16_db_user:RPmF63fvR3eiZvjD@cluster0.pdu4tsr.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"
20
+ )
21
+ db = client["student_admin"]
22
+
23
+ # -----------------------------
24
+ # Helper Functions
25
+ # -----------------------------
26
+ def encode_token(user_id, role):
27
+ payload = {
28
+ "user_id": str(user_id),
29
+ "role": role,
30
+ "exp": datetime.utcnow() + timedelta(seconds=app.config["JWT_EXP_DELTA_SECONDS"])
31
+ }
32
+ return jwt.encode(payload, app.config["SECRET_KEY"], algorithm="HS256")
33
+
34
+ def decode_token(token):
35
+ try:
36
+ return jwt.decode(token, app.config["SECRET_KEY"], algorithms=["HS256"])
37
+ except:
38
+ return None
39
+
40
+ def token_required(role=None):
41
+ def decorator(f):
42
+ def wrapper(*args, **kwargs):
43
+ token = request.headers.get("Authorization", "").replace("Bearer ", "")
44
+ if not token:
45
+ return jsonify({"error": "Authentication required"}), 401
46
+ data = decode_token(token)
47
+ if not data:
48
+ return jsonify({"error": "Invalid or expired token"}), 401
49
+ if role and data.get("role") != role:
50
+ return jsonify({"error": "Forbidden"}), 403
51
+ return f(*args, **kwargs, user_id=data["user_id"], role=data["role"])
52
+ wrapper.__name__ = f.__name__
53
+ return wrapper
54
+ return decorator
55
+
56
+ def hash_password(password):
57
+ return bcrypt.hashpw(password.encode(), bcrypt.gensalt())
58
+
59
+ def verify_password(password, hashed):
60
+ return bcrypt.checkpw(password.encode(), hashed)
61
+
62
+ def safe_objectid(oid):
63
+ try:
64
+ return ObjectId(oid)
65
+ except:
66
+ return None
67
+
68
+ def validate_password(password: str):
69
+ """Simple password policy: at least 8 chars, 1 digit, 1 letter"""
70
+ if len(password) < 8:
71
+ return False
72
+ if not re.search(r"[A-Za-z]", password):
73
+ return False
74
+ if not re.search(r"[0-9]", password):
75
+ return False
76
+ return True
77
+
78
+ # -----------------------------
79
+ # AUTH ROUTES
80
+ # -----------------------------
81
+ @app.route("/auth/signup", methods=["POST"])
82
+ def signup():
83
+ data = request.json
84
+ email = data["email"].lower()
85
+ if db.users.find_one({"email": email}):
86
+ return jsonify({"error": "Email already exists"}), 400
87
+ if not validate_password(data["password"]):
88
+ return jsonify({"error": "Weak password (min 8 chars, include letters & numbers)"}), 400
89
+ hashed = hash_password(data["password"])
90
+ user = {
91
+ "name": data["name"],
92
+ "email": email,
93
+ "password": hashed,
94
+ "role": "student",
95
+ "phone": "",
96
+ "dob": "",
97
+ "address": "",
98
+ "profile_picture": "",
99
+ "blocked": False
100
+ }
101
+ res = db.users.insert_one(user)
102
+ return jsonify({"id": str(res.inserted_id), "message": "User created"}), 201
103
+
104
+ @app.route("/auth/login", methods=["POST"])
105
+ def login():
106
+ data = request.json
107
+ email = data["email"].lower()
108
+ user = db.users.find_one({"email": email})
109
+ if not user or not verify_password(data["password"], user["password"]):
110
+ return jsonify({"error": "Invalid credentials"}), 401
111
+ if user.get("blocked", False):
112
+ return jsonify({"error": "User blocked"}), 403
113
+ token = encode_token(user["_id"], user["role"])
114
+ return jsonify({"token": token})
115
+
116
+ @app.route("/auth/forgot-password", methods=["POST"])
117
+ def forgot_password():
118
+ email = request.json.get("email", "").lower()
119
+ user = db.users.find_one({"email": email})
120
+ if not user:
121
+ return jsonify({"error": "Email not found"}), 404
122
+ reset_token = secrets.token_urlsafe(16)
123
+ db.users.update_one(
124
+ {"_id": user["_id"]},
125
+ {"$set": {"reset_token": reset_token, "reset_expires": datetime.utcnow() + timedelta(hours=1)}}
126
+ )
127
+ # ⚠️ In production, send email with reset link instead of returning token
128
+ return jsonify({"reset_token": reset_token})
129
+
130
+ @app.route("/auth/reset-password", methods=["POST"])
131
+ def reset_password():
132
+ data = request.json
133
+ email = data["email"].lower()
134
+ user = db.users.find_one({"email": email, "reset_token": data["token"]})
135
+ if not user or datetime.utcnow() > user.get("reset_expires", datetime.utcnow()):
136
+ return jsonify({"error": "Invalid or expired token"}), 400
137
+ if not validate_password(data["new_password"]):
138
+ return jsonify({"error": "Weak password (min 8 chars, include letters & numbers)"}), 400
139
+ hashed = hash_password(data["new_password"])
140
+ db.users.update_one(
141
+ {"_id": user["_id"]},
142
+ {"$set": {"password": hashed}, "$unset": {"reset_token": "", "reset_expires": ""}}
143
+ )
144
+ return jsonify({"message": "Password reset successful"})
145
+
146
+ # -----------------------------
147
+ # PROFILE ROUTES
148
+ # -----------------------------
149
+ @app.route("/profile", methods=["GET"])
150
+ @token_required()
151
+ def get_profile(user_id, role):
152
+ obj_id = safe_objectid(user_id)
153
+ if not obj_id:
154
+ return jsonify({"error": "Invalid user ID"}), 400
155
+ user = db.users.find_one({"_id": obj_id}, {"password": 0})
156
+ if not user:
157
+ return jsonify({"error": "User not found"}), 404
158
+ user["_id"] = str(user["_id"])
159
+ return jsonify(user)
160
+
161
+ @app.route("/profile", methods=["PUT"])
162
+ @token_required()
163
+ def update_profile(user_id, role):
164
+ obj_id = safe_objectid(user_id)
165
+ if not obj_id:
166
+ return jsonify({"error": "Invalid user ID"}), 400
167
+ data = request.json
168
+ allowed_fields = ["name", "phone", "dob", "address", "profile_picture"]
169
+ update_fields = {k: v for k, v in data.items() if k in allowed_fields}
170
+ db.users.update_one({"_id": obj_id}, {"$set": update_fields})
171
+ return jsonify({"message": "Profile updated"})
172
+
173
+ @app.route("/profile/change-password", methods=["POST"])
174
+ @token_required()
175
+ def change_password(user_id, role):
176
+ obj_id = safe_objectid(user_id)
177
+ if not obj_id:
178
+ return jsonify({"error": "Invalid user ID"}), 400
179
+ data = request.json
180
+ user = db.users.find_one({"_id": obj_id})
181
+ if not user or not verify_password(data["current_password"], user["password"]):
182
+ return jsonify({"error": "Current password incorrect"}), 400
183
+ if not validate_password(data["new_password"]):
184
+ return jsonify({"error": "Weak password"}), 400
185
+ db.users.update_one({"_id": obj_id}, {"$set": {"password": hash_password(data["new_password"])}})
186
+ return jsonify({"message": "Password changed"})
187
+
188
+ # -----------------------------
189
+ # COURSES ROUTES (Admin)
190
+ # -----------------------------
191
+ @app.route("/courses", methods=["POST"])
192
+ @token_required(role="admin")
193
+ def add_course(user_id, role):
194
+ data = request.json
195
+ if db.courses.find_one({"code": data["code"]}):
196
+ return jsonify({"error": "Course already exists"}), 400
197
+ course = {
198
+ "code": data["code"],
199
+ "title": data["title"],
200
+ "description": data.get("description", ""),
201
+ "created_at": datetime.utcnow()
202
+ }
203
+ db.courses.insert_one(course)
204
+ return jsonify({"message": "Course added"})
205
+
206
+ @app.route("/courses/<code>", methods=["DELETE"])
207
+ @token_required(role="admin")
208
+ def delete_course(user_id, role, code):
209
+ db.courses.delete_one({"code": code})
210
+ return jsonify({"message": "Course deleted"})
211
+
212
+ @app.route("/courses", methods=["GET"])
213
+ @token_required()
214
+ def list_courses(user_id, role):
215
+ courses = list(db.courses.find({}, {"_id": 0}))
216
+ return jsonify(courses)
217
+
218
+ # -----------------------------
219
+ # FEEDBACK ROUTES
220
+ # -----------------------------
221
+ @app.route("/feedback", methods=["POST"])
222
+ @token_required(role="student")
223
+ def submit_feedback(user_id, role):
224
+ data = request.json
225
+ if not all(k in data for k in ["course_code", "rating", "message"]):
226
+ return jsonify({"error": "Missing fields"}), 400
227
+ user_doc = db.users.find_one({"_id": safe_objectid(user_id)})
228
+ course_doc = db.courses.find_one({"code": data["course_code"]})
229
+ if not course_doc:
230
+ return jsonify({"error": "Course not found"}), 404
231
+ db.feedback.insert_one({
232
+ "course_code": data["course_code"],
233
+ "rating": data["rating"],
234
+ "message": data["message"],
235
+ "student_id": user_id,
236
+ "student_name": user_doc["name"],
237
+ "created_at": datetime.utcnow()
238
+ })
239
+ return jsonify({"message": "Feedback submitted"}), 201
240
+
241
+ @app.route("/feedback", methods=["GET"])
242
+ @token_required(role="student")
243
+ def list_my_feedback(user_id, role):
244
+ page = int(request.args.get("page", 1))
245
+ per_page = int(request.args.get("per_page", 5))
246
+ feedbacks = list(db.feedback.find({"student_id": user_id}).skip((page-1)*per_page).limit(per_page))
247
+ for f in feedbacks:
248
+ f["_id"] = str(f["_id"])
249
+ return jsonify(feedbacks)
250
+
251
+ @app.route("/feedback/<feedback_id>", methods=["PUT"])
252
+ @token_required(role="student")
253
+ def edit_feedback(user_id, role, feedback_id):
254
+ fid = safe_objectid(feedback_id)
255
+ if not fid:
256
+ return jsonify({"error": "Invalid feedback ID"}), 400
257
+ data = request.json
258
+ db.feedback.update_one({"_id": fid, "student_id": user_id}, {"$set": data})
259
+ return jsonify({"message": "Feedback updated"})
260
+
261
+ @app.route("/feedback/<feedback_id>", methods=["DELETE"])
262
+ @token_required(role="student")
263
+ def delete_feedback(user_id, role, feedback_id):
264
+ fid = safe_objectid(feedback_id)
265
+ if not fid:
266
+ return jsonify({"error": "Invalid feedback ID"}), 400
267
+ db.feedback.delete_one({"_id": fid, "student_id": user_id})
268
+ return jsonify({"message": "Feedback deleted"})
269
+
270
+ # -----------------------------
271
+ # ADMIN FEEDBACK MANAGEMENT
272
+ # -----------------------------
273
+ @app.route("/admin/stats", methods=["GET"])
274
+ @token_required(role="admin")
275
+ def admin_stats(user_id, role):
276
+ total_feedback = db.feedback.count_documents({})
277
+ total_students = db.users.count_documents({"role": "student"})
278
+ pipeline = [{"$group": {"_id": "$course_code", "average_rating": {"$avg": "$rating"}, "count": {"$sum": 1}}}]
279
+ trends = list(db.feedback.aggregate(pipeline))
280
+ return jsonify({"total_feedback": total_feedback, "total_students": total_students, "feedback_trends": trends})
281
+
282
+ @app.route("/admin/students", methods=["GET"])
283
+ @token_required(role="admin")
284
+ def list_students(user_id, role):
285
+ students = list(db.users.find({"role": "student"}, {"password": 0}))
286
+ for s in students:
287
+ s["_id"] = str(s["_id"])
288
+ return jsonify(students)
289
+
290
+ @app.route("/admin/students/<student_id>/block", methods=["POST"])
291
+ @token_required(role="admin")
292
+ def block_unblock_student(user_id, role, student_id):
293
+ obj_id = safe_objectid(student_id)
294
+ if not obj_id:
295
+ return jsonify({"error": "Invalid student ID"}), 400
296
+ action = request.json.get("action")
297
+ if action not in ["block", "unblock"]:
298
+ return jsonify({"error": "Invalid action, must be 'block' or 'unblock'"}), 400
299
+ blocked = True if action == "block" else False
300
+ db.users.update_one({"_id": obj_id}, {"$set": {"blocked": blocked}})
301
+ return jsonify({"message": f"Student {'blocked' if blocked else 'unblocked'}"})
302
+
303
+ @app.route("/admin/students/<student_id>", methods=["DELETE"])
304
+ @token_required(role="admin")
305
+ def delete_student(user_id, role, student_id):
306
+ obj_id = safe_objectid(student_id)
307
+ if not obj_id:
308
+ return jsonify({"error": "Invalid student ID"}), 400
309
+ db.users.delete_one({"_id": obj_id})
310
+ return jsonify({"message": "Student deleted"})
311
+
312
+ @app.route("/admin/feedback", methods=["GET"])
313
+ @token_required(role="admin")
314
+ def view_all_feedback(user_id, role):
315
+ query = {}
316
+ course_filter = request.args.get("course")
317
+ rating_filter = request.args.get("rating")
318
+ student_filter = request.args.get("student")
319
+ if course_filter:
320
+ query["course_code"] = course_filter
321
+ if rating_filter:
322
+ try:
323
+ query["rating"] = int(rating_filter)
324
+ except:
325
+ return jsonify({"error": "Invalid rating filter"}), 400
326
+ if student_filter:
327
+ query["student_name"] = student_filter
328
+ feedbacks = list(db.feedback.find(query))
329
+ for f in feedbacks:
330
+ f["_id"] = str(f["_id"])
331
+ return jsonify(feedbacks)
332
+
333
+ @app.route("/feedback/export", methods=["GET"])
334
+ @token_required(role="admin")
335
+ def export_feedback(user_id, role):
336
+ feedbacks = list(db.feedback.find({}))
337
+ output = io.StringIO()
338
+ writer = csv.writer(output)
339
+ writer.writerow(["course_code","student_name","rating","message","created_at"])
340
+ for f in feedbacks:
341
+ writer.writerow([f["course_code"], f["student_name"], f["rating"], f["message"], f["created_at"]])
342
+ output.seek(0)
343
+ return send_file(io.BytesIO(output.getvalue().encode()), mimetype="text/csv", as_attachment=True, download_name="feedback_export.csv")
344
+
345
+ # -----------------------------
346
+ # RUN APP
347
+ # -----------------------------
348
+ if __name__ == "__main__":
349
+ # Create default admin if not exists
350
+ if not db.users.find_one({"email": "admin123@example.com"}):
351
+ db.users.insert_one({
352
+ "name": "Admin User",
353
+ "email": "admin123@example.com",
354
+ "password": hash_password("Admin@1234"),
355
+ "role": "admin"
356
+ })
357
+ port = int(os.environ.get("PORT", 7860)) # use HF-provided port or default 7860
358
+ app.run(host="0.0.0.0", port=port, debug=True)