APINOW-service commited on
Commit
feeca53
·
verified ·
1 Parent(s): 3fc2398

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +277 -0
app.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import random
4
+ import smtplib
5
+ from datetime import datetime
6
+ from functools import wraps
7
+ from email.mime.text import MIMEText
8
+ from email.mime.multipart import MIMEMultipart
9
+
10
+ # Flask & Extensions
11
+ from flask import Flask, request, jsonify
12
+ from flask_limiter import Limiter
13
+ from flask_cors import CORS
14
+ from flask_limiter.util import get_remote_address
15
+ from dotenv import load_dotenv
16
+
17
+ # REQUIRED FOR HUGGING FACE SPACES (Proxy Fix)
18
+ from werkzeug.middleware.proxy_fix import ProxyFix
19
+
20
+ # ================= LOAD ENV =================
21
+ load_dotenv()
22
+
23
+ app = Flask(__name__)
24
+
25
+ # ================= HUGGING FACE CONFIGURATION =================
26
+ # Hugging Face runs behind a proxy (Nginx). We must tell Flask to trust
27
+ # the X-Forwarded-For headers so Flask-Limiter sees the real user IP,
28
+ # not the proxy IP.
29
+ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
30
+
31
+ CORS(app)
32
+
33
+ # ================= BASIC CONFIG =================
34
+ YOUR_WEB_APP_NAME = os.getenv("WEBAPPNAME", "xyzapp")
35
+ CURRENT_YEAR = str(datetime.now().year)
36
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
37
+
38
+ # Ensure your Templates folder exists in the Space files
39
+ TEMPLATE_PATH = os.path.join(BASE_DIR, "Templates", "template1.html")
40
+ REMOVE_WATERMARK = False
41
+
42
+ # ================= RATE LIMIT =================
43
+ # With ProxyFix applied above, get_remote_address will now correctly
44
+ # grab the real user's IP address.
45
+ limiter = Limiter(
46
+ key_func=get_remote_address,
47
+ app=app,
48
+ default_limits=["200 per hour"],
49
+ storage_uri="memory://" # Explicitly use memory storage
50
+ )
51
+
52
+ # ================= SMTP CONFIG =================
53
+ SMTP_SERVER = "smtp.gmail.com"
54
+ SMTP_PORT = 587
55
+
56
+ GMAIL_EMAIL = os.getenv("GMAIL_EMAIL")
57
+ APP_PASSWORD = os.getenv("APP_PASSWORD")
58
+ FROM_EMAIL = os.getenv("FROM_EMAIL", GMAIL_EMAIL)
59
+
60
+ # ================= SECURITY CONFIG =================
61
+ OTP_EXPIRY_SECONDS = 300
62
+ MAX_ATTEMPTS = 5
63
+ LOCK_TIME = 600
64
+ RESEND_COOLDOWN = 60
65
+
66
+ IP_MAX_ATTEMPTS = 10
67
+ IP_BLOCK_TIME = 60
68
+
69
+ # ================= STORES (IN-MEMORY) =================
70
+ # Note: In-memory stores will reset if the Space restarts/sleeps.
71
+ otp_store = {}
72
+ ip_store = {}
73
+
74
+ # ================= HTML TEMPLATE =================
75
+ def load_template(path: str) -> str:
76
+ # Fallback if file is missing to prevent crash
77
+ if not os.path.exists(path):
78
+ return "<html><body><h1>Your OTP is {{OTP_CODE}}</h1></body></html>"
79
+
80
+ with open(path, "r", encoding="utf-8") as file:
81
+ return file.read()
82
+
83
+ def apply_template_changes(template: str) -> str:
84
+ template = template.replace("{{APP_NAME}}", YOUR_WEB_APP_NAME)
85
+ template = template.replace("{{YEAR}}", CURRENT_YEAR)
86
+
87
+ if REMOVE_WATERMARK:
88
+ template = template.replace(
89
+ "MailOTP Guard — made with ❤️ by TechBitForge",
90
+ ""
91
+ )
92
+ return template
93
+
94
+ # Load template once on startup
95
+ HTML_TEMPLATE = apply_template_changes(load_template(TEMPLATE_PATH))
96
+
97
+ # ================= IP BLOCK SYSTEM =================
98
+ def is_ip_blocked(ip):
99
+ record = ip_store.get(ip)
100
+ if not record:
101
+ return False
102
+
103
+ if time.time() > record["blocked_until"]:
104
+ ip_store.pop(ip)
105
+ return False
106
+
107
+ return True
108
+
109
+ def register_ip_failure(ip):
110
+ now = time.time()
111
+ record = ip_store.setdefault(ip, {
112
+ "attempts": 0,
113
+ "blocked_until": 0
114
+ })
115
+
116
+ record["attempts"] += 1
117
+ if record["attempts"] >= IP_MAX_ATTEMPTS:
118
+ record["blocked_until"] = now + IP_BLOCK_TIME
119
+
120
+ # ================= SEND EMAIL =================
121
+ def send_email(email, otp):
122
+ if not GMAIL_EMAIL or not APP_PASSWORD:
123
+ print("Error: SMTP Credentials missing in Environment Variables")
124
+ raise Exception("SMTP Config Missing")
125
+
126
+ html = HTML_TEMPLATE.replace("{{OTP_CODE}}", otp)
127
+
128
+ msg = MIMEMultipart()
129
+ msg["From"] = FROM_EMAIL
130
+ msg["To"] = email
131
+ msg["Subject"] = f"{YOUR_WEB_APP_NAME} OTP Verification"
132
+ msg.attach(MIMEText(html, "html"))
133
+
134
+ with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
135
+ server.starttls()
136
+ server.login(GMAIL_EMAIL, APP_PASSWORD)
137
+ server.send_message(msg)
138
+
139
+ # ================= ROUTE: HOME =================
140
+ @app.route("/")
141
+ def home():
142
+ return jsonify({"status": "running", "msg": f"Welcome to {YOUR_WEB_APP_NAME} API"})
143
+
144
+ # ================= ROUTE: SEND OTP =================
145
+ @app.route("/send-otp", methods=["POST"])
146
+ @limiter.limit("5 per minute")
147
+ def send_otp():
148
+ # Behind ProxyFix, remote_addr is now safe
149
+ ip = request.remote_addr
150
+
151
+ if is_ip_blocked(ip):
152
+ return jsonify({"error": "IP blocked for 1 minute"}), 429
153
+
154
+ data = request.get_json(silent=True) or {}
155
+ email = data.get("email")
156
+
157
+ if not email:
158
+ register_ip_failure(ip)
159
+ return jsonify({"error": "Email required"}), 400
160
+
161
+ now = time.time()
162
+ otp = str(random.randint(100000, 999999))
163
+
164
+ otp_store[email] = {
165
+ "otp": otp,
166
+ "expires": now + OTP_EXPIRY_SECONDS,
167
+ "attempts": 0,
168
+ "locked_until": 0,
169
+ "last_sent": now
170
+ }
171
+
172
+ try:
173
+ send_email(email, otp)
174
+ return jsonify({"message": "OTP sent successfully"})
175
+ except Exception as e:
176
+ print(f"Mail Error: {e}") # Print error to HF Space logs
177
+ register_ip_failure(ip)
178
+ return jsonify({"error": "Failed to send email"}), 500
179
+
180
+ # ================= ROUTE: RESEND OTP =================
181
+ @app.route("/resend-otp", methods=["POST"])
182
+ @limiter.limit("3 per minute")
183
+ def resend_otp():
184
+ ip = request.remote_addr
185
+
186
+ if is_ip_blocked(ip):
187
+ return jsonify({"error": "IP blocked for 1 minute"}), 429
188
+
189
+ data = request.get_json(silent=True) or {}
190
+ email = data.get("email")
191
+
192
+ if not email:
193
+ register_ip_failure(ip)
194
+ return jsonify({"error": "Email required"}), 400
195
+
196
+ record = otp_store.get(email)
197
+ if not record:
198
+ return jsonify({"error": "OTP not requested"}), 400
199
+
200
+ now = time.time()
201
+
202
+ if record["locked_until"] > now:
203
+ return jsonify({"error": "Account locked"}), 429
204
+
205
+ if now - record["last_sent"] < RESEND_COOLDOWN:
206
+ return jsonify({
207
+ "error": "Please wait before resending OTP",
208
+ "retry_after": int(RESEND_COOLDOWN - (now - record["last_sent"]))
209
+ }), 429
210
+
211
+ otp = str(random.randint(100000, 999999))
212
+
213
+ record.update({
214
+ "otp": otp,
215
+ "expires": now + OTP_EXPIRY_SECONDS,
216
+ "attempts": 0,
217
+ "last_sent": now
218
+ })
219
+
220
+ try:
221
+ send_email(email, otp)
222
+ return jsonify({"message": "OTP resent successfully"})
223
+ except Exception:
224
+ register_ip_failure(ip)
225
+ return jsonify({"error": "Failed to resend OTP"}), 500
226
+
227
+ # ================= ROUTE: VERIFY OTP =================
228
+ @app.route("/verify-otp", methods=["POST"])
229
+ @limiter.limit("10 per minute")
230
+ def verify_otp():
231
+ ip = request.remote_addr
232
+
233
+ if is_ip_blocked(ip):
234
+ return jsonify({"error": "IP blocked for 1 minute"}), 429
235
+
236
+ data = request.get_json(silent=True) or {}
237
+ email = data.get("email")
238
+ user_otp = data.get("otp")
239
+
240
+ if not email or not user_otp:
241
+ register_ip_failure(ip)
242
+ return jsonify({"error": "Missing fields"}), 400
243
+
244
+ record = otp_store.get(email)
245
+ if not record:
246
+ register_ip_failure(ip)
247
+ return jsonify({"error": "OTP not found"}), 400
248
+
249
+ now = time.time()
250
+
251
+ if record["locked_until"] > now:
252
+ return jsonify({"error": "Account locked"}), 429
253
+
254
+ if now > record["expires"]:
255
+ otp_store.pop(email)
256
+ return jsonify({"error": "OTP expired"}), 400
257
+
258
+ if record["otp"] != user_otp:
259
+ record["attempts"] += 1
260
+ register_ip_failure(ip)
261
+
262
+ if record["attempts"] >= MAX_ATTEMPTS:
263
+ record["locked_until"] = now + LOCK_TIME
264
+ return jsonify({"error": "Account locked for 10 minutes"}), 429
265
+
266
+ return jsonify({
267
+ "error": "Invalid OTP",
268
+ "remaining_attempts": MAX_ATTEMPTS - record["attempts"]
269
+ }), 400
270
+
271
+ otp_store.pop(email)
272
+ return jsonify({"message": "OTP verified successfully"})
273
+
274
+ # ================= RUN =================
275
+ if __name__ == "__main__":
276
+ # Hugging Face Spaces default port is 7860
277
+ app.run(host="0.0.0.0", port=7860, debug=False)