Nanny7 commited on
Commit
899bfab
·
1 Parent(s): dbfeb5e

feat: split auth into dedicated pages and enable flask auth routes

Browse files
.gitignore CHANGED
@@ -8,4 +8,7 @@ venv/
8
  .ruff_cache/
9
  .cache/
10
  Thumbs.db
11
- .DS_Store
 
 
 
 
8
  .ruff_cache/
9
  .cache/
10
  Thumbs.db
11
+ .DS_Store
12
+ users.db
13
+ *.db
14
+ auth_module/.jwt_secret
app.py CHANGED
@@ -175,6 +175,21 @@ def index():
175
  return render_template("index.html")
176
 
177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  @app.route("/health")
179
  def health():
180
  uptime = int(time.time() - START_TIME)
 
175
  return render_template("index.html")
176
 
177
 
178
+ @app.route("/login")
179
+ def login_page():
180
+ return render_template("login.html")
181
+
182
+
183
+ @app.route("/signup")
184
+ def signup_page():
185
+ return render_template("signup.html")
186
+
187
+
188
+ @app.route("/verify")
189
+ def verify_page():
190
+ return render_template("verify.html")
191
+
192
+
193
  @app.route("/health")
194
  def health():
195
  uptime = int(time.time() - START_TIME)
auth_module/flask_auth_routes.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ import random
4
+ from datetime import datetime, timedelta
5
+
6
+ from flask import Blueprint, jsonify, make_response, request
7
+
8
+ from .database import auth_db
9
+ from .security import create_access_token, decode_access_token, validate_password_strength
10
+
11
+
12
+ auth_blueprint = Blueprint("auth_blueprint", __name__)
13
+
14
+
15
+ def _run(coro):
16
+ return asyncio.run(coro)
17
+
18
+
19
+ def _extract_token_from_request():
20
+ auth_header = request.headers.get("Authorization", "").strip()
21
+ if auth_header.lower().startswith("bearer "):
22
+ return auth_header.split(" ", 1)[1].strip()
23
+
24
+ cookie_value = request.cookies.get("access_token", "").strip()
25
+ if cookie_value.lower().startswith("bearer "):
26
+ return cookie_value.split(" ", 1)[1].strip()
27
+ return cookie_value or None
28
+
29
+
30
+ def _get_current_user_or_error():
31
+ token = _extract_token_from_request()
32
+ if not token:
33
+ return None, (jsonify({"detail": "غير مصرح — سجّل دخولك أولاً"}), 401)
34
+
35
+ payload = decode_access_token(token)
36
+ if not payload:
37
+ return None, (jsonify({"detail": "جلسة منتهية — سجّل دخولك مرة أخرى"}), 401)
38
+
39
+ email = payload.get("sub", "")
40
+ user = _run(auth_db.get_user_by_email(email))
41
+ if not user:
42
+ return None, (jsonify({"detail": "المستخدم غير موجود"}), 401)
43
+
44
+ return user, None
45
+
46
+
47
+ def _is_production():
48
+ return bool(os.getenv("RENDER") or os.getenv("ENVIRONMENT", "").lower() == "production")
49
+
50
+
51
+ @auth_blueprint.post("/signup")
52
+ def signup():
53
+ data = request.get_json(silent=True) or {}
54
+ email = (data.get("email") or "").strip().lower()
55
+ password = data.get("password") or ""
56
+ full_name = (data.get("full_name") or "").strip()
57
+ phone = (data.get("phone") or "").strip() or None
58
+
59
+ valid, message = validate_password_strength(password)
60
+ if not valid:
61
+ return jsonify({"detail": message}), 400
62
+
63
+ if "@" not in email or "." not in email:
64
+ return jsonify({"detail": "البريد الإلكتروني غير صحيح"}), 400
65
+
66
+ if not full_name:
67
+ return jsonify({"detail": "الاسم الكامل مطلوب"}), 400
68
+
69
+ created_user = _run(auth_db.create_user(email, password, full_name, phone=phone))
70
+ if not created_user:
71
+ return jsonify({"detail": "البريد الإلكتروني مسجل بالفعل"}), 400
72
+
73
+ otp_code = str(random.randint(100000, 999999))
74
+ _run(auth_db.store_otp(created_user["id"], otp_code, "telegram_verify", minutes=10))
75
+
76
+ try:
77
+ from .config import auth_settings
78
+ from .nova_client import nova_client
79
+
80
+ if nova_client.is_configured:
81
+ _run(
82
+ nova_client.push_otp(
83
+ email=email,
84
+ code=otp_code,
85
+ app_id=auth_settings.APP_ID,
86
+ minutes=10,
87
+ )
88
+ )
89
+ except Exception:
90
+ pass
91
+
92
+ return jsonify(
93
+ {
94
+ "status": "pending_verification",
95
+ "message": "تم إنشاء الحساب! فعّل حسابك عبر تليجرام بوت @robovainova_bot ← /verify",
96
+ "user": created_user,
97
+ }
98
+ )
99
+
100
+
101
+ @auth_blueprint.post("/login")
102
+ def login():
103
+ data = request.get_json(silent=True) or {}
104
+ username_from_json = (data.get("username") or data.get("email") or "").strip()
105
+ password_from_json = data.get("password") or ""
106
+
107
+ username_from_form = (request.form.get("username") or "").strip()
108
+ password_from_form = request.form.get("password") or ""
109
+
110
+ username = username_from_json or username_from_form
111
+ password = password_from_json or password_from_form
112
+
113
+ if not username or not password:
114
+ return jsonify({"detail": "بيانات تسجيل الدخول غير مكتملة"}), 400
115
+
116
+ user = _run(auth_db.authenticate_user(username, password))
117
+ if not user:
118
+ return jsonify({"detail": "البريد الإلكتروني أو كلمة المرور غير صحيحة"}), 401
119
+
120
+ if not user.get("is_verified"):
121
+ return jsonify(
122
+ {
123
+ "detail": "الحساب غير مُفعّل. افتح بوت @robovainova_bot في تليجرام وأرسل /verify"
124
+ }
125
+ ), 403
126
+
127
+ access_token = create_access_token(data={"sub": user["email"]})
128
+ expires_at = (datetime.now() + timedelta(days=1)).isoformat()
129
+ _run(auth_db.create_session(user["id"], access_token, expires_at))
130
+
131
+ response = make_response(
132
+ jsonify({"access_token": access_token, "token_type": "bearer"})
133
+ )
134
+ response.set_cookie(
135
+ key="access_token",
136
+ value=f"Bearer {access_token}",
137
+ httponly=True,
138
+ secure=_is_production(),
139
+ max_age=86400,
140
+ samesite="Lax",
141
+ )
142
+ return response
143
+
144
+
145
+ @auth_blueprint.post("/logout")
146
+ def logout():
147
+ token = request.cookies.get("access_token", "")
148
+ if token.lower().startswith("bearer "):
149
+ token = token.split(" ", 1)[1].strip()
150
+
151
+ if token:
152
+ _run(auth_db.delete_session(token))
153
+
154
+ response = make_response(jsonify({"status": "success"}))
155
+ response.delete_cookie("access_token")
156
+ return response
157
+
158
+
159
+ @auth_blueprint.get("/me")
160
+ def me():
161
+ user, error_response = _get_current_user_or_error()
162
+ if error_response:
163
+ return error_response
164
+
165
+ return jsonify(
166
+ {
167
+ "id": user["id"],
168
+ "email": user["email"],
169
+ "full_name": user.get("full_name", ""),
170
+ "role": user.get("role", "user"),
171
+ "balance": user.get("balance", 0),
172
+ }
173
+ )
174
+
175
+
176
+ @auth_blueprint.post("/request-otp")
177
+ def request_otp():
178
+ data = request.get_json(silent=True) or {}
179
+ email = (data.get("email") or "").strip().lower()
180
+ if not email:
181
+ return jsonify({"detail": "البريد الإلكتروني مطلوب"}), 400
182
+
183
+ user = _run(auth_db.get_user_by_email_unverified(email))
184
+ if not user:
185
+ return jsonify({"detail": "لم يتم العثور على حساب بهذا البريد"}), 404
186
+
187
+ if user.get("is_verified"):
188
+ return jsonify({"status": "already_verified", "message": "الحساب مفعّل بالفعل ✅"})
189
+
190
+ otp_code = str(random.randint(100000, 999999))
191
+ _run(auth_db.store_otp(user["id"], otp_code, "telegram_verify", minutes=10))
192
+
193
+ try:
194
+ from .config import auth_settings
195
+ from .nova_client import nova_client
196
+
197
+ if nova_client.is_configured:
198
+ _run(
199
+ nova_client.push_otp(
200
+ email=email,
201
+ code=otp_code,
202
+ app_id=auth_settings.APP_ID,
203
+ minutes=10,
204
+ )
205
+ )
206
+ except Exception:
207
+ pass
208
+
209
+ return jsonify(
210
+ {
211
+ "status": "success",
212
+ "message": "تم إنشاء كود التحقق. افتح بوت RobovAI في تليجرام وأرسل /verify",
213
+ }
214
+ )
215
+
216
+
217
+ @auth_blueprint.post("/verify-otp")
218
+ def verify_otp():
219
+ data = request.get_json(silent=True) or {}
220
+ email = (data.get("email") or "").strip().lower()
221
+ code = (data.get("code") or "").strip()
222
+
223
+ if not email or not code:
224
+ return jsonify({"detail": "email و code مطلوبان"}), 400
225
+
226
+ user = _run(auth_db.get_user_by_email_unverified(email))
227
+ if not user:
228
+ return jsonify({"detail": "المستخدم غير موجود"}), 404
229
+
230
+ if user.get("is_verified"):
231
+ return jsonify({"status": "already_verified", "message": "الحساب مفعّل بالفعل ✅"})
232
+
233
+ valid = _run(auth_db.verify_otp(user["id"], code, "telegram_verify"))
234
+ if not valid:
235
+ return jsonify({"detail": "كود التحقق غير صحيح أو منتهي الصلاحية"}), 400
236
+
237
+ _run(auth_db.set_user_verified(user["id"]))
238
+
239
+ access_token = create_access_token(data={"sub": email})
240
+ expires_at = (datetime.now() + timedelta(days=1)).isoformat()
241
+ _run(auth_db.create_session(user["id"], access_token, expires_at))
242
+
243
+ response = make_response(
244
+ jsonify(
245
+ {
246
+ "status": "success",
247
+ "message": "تم تفعيل الحساب بنجاح! ✅",
248
+ "access_token": access_token,
249
+ "user": {
250
+ "id": user["id"],
251
+ "email": email,
252
+ "full_name": user.get("full_name", ""),
253
+ },
254
+ }
255
+ )
256
+ )
257
+ response.set_cookie(
258
+ key="access_token",
259
+ value=f"Bearer {access_token}",
260
+ httponly=True,
261
+ secure=_is_production(),
262
+ max_age=86400,
263
+ samesite="Lax",
264
+ )
265
+ return response
266
+
267
+
268
+ @auth_blueprint.get("/check-verified")
269
+ def check_verified():
270
+ email = (request.args.get("email") or "").strip().lower()
271
+ if not email:
272
+ return jsonify({"detail": "email مطلوب"}), 400
273
+
274
+ user = _run(auth_db.get_user_by_email_unverified(email))
275
+ if not user:
276
+ return jsonify({"detail": "المستخدم غير موجود"}), 404
277
+
278
+ if user.get("is_verified"):
279
+ return jsonify({"verified": True})
280
+
281
+ try:
282
+ from .nova_client import nova_client
283
+
284
+ if nova_client.is_configured:
285
+ remote_verified = _run(nova_client.check_verified(email))
286
+ if remote_verified:
287
+ _run(auth_db.set_user_verified(user["id"]))
288
+ return jsonify({"verified": True})
289
+ except Exception:
290
+ pass
291
+
292
+ return jsonify({"verified": False})
293
+
294
+
295
+ @auth_blueprint.delete("/delete-account")
296
+ def delete_account():
297
+ user, error_response = _get_current_user_or_error()
298
+ if error_response:
299
+ return error_response
300
+
301
+ _run(auth_db.delete_user_account(user["id"]))
302
+ response = make_response(
303
+ jsonify({"status": "success", "message": "تم حذف الحساب نهائياً"})
304
+ )
305
+ response.delete_cookie("access_token")
306
+ return response
static/css/style.css CHANGED
@@ -2621,6 +2621,17 @@ body.no-waveform .waveform-canvas {
2621
  .auth-footer {
2622
  margin-top: 10px;
2623
  }
 
 
 
 
 
 
 
 
 
 
 
2624
 
2625
  /* ============================================
2626
  RESPONSIVE ADJUSTMENTS (Phase 3)
 
2621
  .auth-footer {
2622
  margin-top: 10px;
2623
  }
2624
+ .auth-standalone {
2625
+ max-width: 460px;
2626
+ }
2627
+ .auth-page-panel {
2628
+ margin-top: 10px;
2629
+ }
2630
+ .auth-switch-line {
2631
+ margin-top: 14px;
2632
+ font-size: 0.82rem;
2633
+ color: var(--text-secondary);
2634
+ }
2635
 
2636
  /* ============================================
2637
  RESPONSIVE ADJUSTMENTS (Phase 3)
static/js/auth-pages.js ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const $ = (id) => document.getElementById(id);
3
+ const toastEl = $('toast');
4
+
5
+ function showToast(msg, type = '') {
6
+ if (!toastEl) return;
7
+ toastEl.textContent = msg;
8
+ toastEl.className = 'toast' + (type ? ' ' + type : '');
9
+ toastEl.classList.add('visible');
10
+ setTimeout(() => toastEl.classList.remove('visible'), 3000);
11
+ }
12
+
13
+ async function authRequest(path, options = {}) {
14
+ const res = await fetch(path, {
15
+ credentials: 'include',
16
+ ...options,
17
+ headers: {
18
+ 'Content-Type': 'application/json',
19
+ ...(options.headers || {}),
20
+ },
21
+ });
22
+
23
+ let payload = null;
24
+ try {
25
+ payload = await res.json();
26
+ } catch (e) {}
27
+
28
+ if (!res.ok) {
29
+ throw new Error(payload?.detail || payload?.error || 'Request failed');
30
+ }
31
+ return payload || {};
32
+ }
33
+
34
+ const loginBtn = $('auth-login-submit');
35
+ if (loginBtn) {
36
+ loginBtn.addEventListener('click', async () => {
37
+ try {
38
+ const email = ($('auth-login-email')?.value || '').trim();
39
+ const password = $('auth-login-password')?.value || '';
40
+ await authRequest('/auth/login', {
41
+ method: 'POST',
42
+ body: JSON.stringify({ email, password }),
43
+ });
44
+ showToast('Login successful', 'success');
45
+ setTimeout(() => {
46
+ window.location.href = '/';
47
+ }, 300);
48
+ } catch (e) {
49
+ showToast(e.message || 'Login failed', 'error');
50
+ }
51
+ });
52
+ }
53
+
54
+ const signupBtn = $('auth-signup-submit');
55
+ if (signupBtn) {
56
+ signupBtn.addEventListener('click', async () => {
57
+ try {
58
+ const full_name = ($('auth-signup-name')?.value || '').trim();
59
+ const email = ($('auth-signup-email')?.value || '').trim();
60
+ const phone = ($('auth-signup-phone')?.value || '').trim();
61
+ const password = $('auth-signup-password')?.value || '';
62
+ await authRequest('/auth/signup', {
63
+ method: 'POST',
64
+ body: JSON.stringify({ full_name, email, phone, password }),
65
+ });
66
+ showToast('Account created. Continue verification.', 'success');
67
+ setTimeout(() => {
68
+ window.location.href = `/verify?email=${encodeURIComponent(email)}`;
69
+ }, 400);
70
+ } catch (e) {
71
+ showToast(e.message || 'Signup failed', 'error');
72
+ }
73
+ });
74
+ }
75
+
76
+ const otpEmailInput = $('auth-otp-email');
77
+ if (otpEmailInput) {
78
+ const queryEmail = new URLSearchParams(window.location.search).get('email');
79
+ if (queryEmail) otpEmailInput.value = queryEmail;
80
+ }
81
+
82
+ const requestOtpBtn = $('auth-otp-request');
83
+ if (requestOtpBtn) {
84
+ requestOtpBtn.addEventListener('click', async () => {
85
+ try {
86
+ const email = ($('auth-otp-email')?.value || '').trim();
87
+ await authRequest('/auth/request-otp', {
88
+ method: 'POST',
89
+ body: JSON.stringify({ email }),
90
+ });
91
+ showToast('OTP requested. Check Telegram bot.', 'success');
92
+ } catch (e) {
93
+ showToast(e.message || 'OTP request failed', 'error');
94
+ }
95
+ });
96
+ }
97
+
98
+ const verifyOtpBtn = $('auth-otp-verify');
99
+ if (verifyOtpBtn) {
100
+ verifyOtpBtn.addEventListener('click', async () => {
101
+ try {
102
+ const email = ($('auth-otp-email')?.value || '').trim();
103
+ const code = ($('auth-otp-code')?.value || '').trim();
104
+ await authRequest('/auth/verify-otp', {
105
+ method: 'POST',
106
+ body: JSON.stringify({ email, code }),
107
+ });
108
+ showToast('Verified successfully. Redirecting to login.', 'success');
109
+ setTimeout(() => {
110
+ window.location.href = '/login';
111
+ }, 400);
112
+ } catch (e) {
113
+ showToast(e.message || 'OTP verification failed', 'error');
114
+ }
115
+ });
116
+ }
117
+
118
+ const checkBtn = $('auth-check-verified');
119
+ if (checkBtn) {
120
+ checkBtn.addEventListener('click', async () => {
121
+ try {
122
+ const email = ($('auth-otp-email')?.value || '').trim();
123
+ const data = await authRequest(
124
+ `/auth/check-verified?email=${encodeURIComponent(email)}`,
125
+ { method: 'GET' },
126
+ );
127
+ if (data.verified) {
128
+ showToast('Account is verified. Go login.', 'success');
129
+ setTimeout(() => {
130
+ window.location.href = '/login';
131
+ }, 300);
132
+ } else {
133
+ showToast('Still not verified yet.', 'error');
134
+ }
135
+ } catch (e) {
136
+ showToast(e.message || 'Check failed', 'error');
137
+ }
138
+ });
139
+ }
140
+ });
static/js/script.js CHANGED
@@ -960,7 +960,9 @@ document.addEventListener('DOMContentLoaded', () => {
960
  startBtn.addEventListener('click', () => {
961
  if (!currentUser) {
962
  showToast('Please login first', 'error');
963
- $('auth-modal')?.classList.remove('hidden');
 
 
964
  return;
965
  }
966
  onboardingScreen.classList.add('hidden');
@@ -1812,7 +1814,7 @@ document.addEventListener('DOMContentLoaded', () => {
1812
 
1813
  function bindAuthUI() {
1814
  authBtn?.addEventListener('click', () => {
1815
- $('auth-modal')?.classList.remove('hidden');
1816
  });
1817
 
1818
  $('auth-close')?.addEventListener('click', () => {
 
960
  startBtn.addEventListener('click', () => {
961
  if (!currentUser) {
962
  showToast('Please login first', 'error');
963
+ setTimeout(() => {
964
+ window.location.href = '/login';
965
+ }, 300);
966
  return;
967
  }
968
  onboardingScreen.classList.add('hidden');
 
1814
 
1815
  function bindAuthUI() {
1816
  authBtn?.addEventListener('click', () => {
1817
+ window.location.href = '/login';
1818
  });
1819
 
1820
  $('auth-close')?.addEventListener('click', () => {
templates/index.html CHANGED
@@ -516,105 +516,6 @@
516
  </div>
517
  </div>
518
 
519
- <!-- ============ AUTH MODAL ============ -->
520
- <div class="modal-overlay hidden" id="auth-modal">
521
- <div class="modal-card auth-card">
522
- <div class="modal-header">
523
- <h2>Account</h2>
524
- <button class="modal-close" id="auth-close">&times;</button>
525
- </div>
526
-
527
- <div class="auth-tabs" id="auth-tabs">
528
- <button class="auth-tab active" data-auth-tab="login">Login</button>
529
- <button class="auth-tab" data-auth-tab="signup">Sign Up</button>
530
- </div>
531
-
532
- <div class="auth-panel" id="auth-login-panel">
533
- <input
534
- class="voice-input"
535
- id="auth-login-email"
536
- type="email"
537
- placeholder="Email"
538
- autocomplete="email"
539
- />
540
- <input
541
- class="voice-input"
542
- id="auth-login-password"
543
- type="password"
544
- placeholder="Password"
545
- autocomplete="current-password"
546
- />
547
- <div class="auth-actions">
548
- <button class="btn-primary" id="auth-login-submit">Login</button>
549
- </div>
550
- </div>
551
-
552
- <div class="auth-panel hidden" id="auth-signup-panel">
553
- <input
554
- class="voice-input"
555
- id="auth-signup-name"
556
- type="text"
557
- placeholder="Full name"
558
- autocomplete="name"
559
- />
560
- <input
561
- class="voice-input"
562
- id="auth-signup-email"
563
- type="email"
564
- placeholder="Email"
565
- autocomplete="email"
566
- />
567
- <input
568
- class="voice-input"
569
- id="auth-signup-phone"
570
- type="text"
571
- placeholder="Phone (optional)"
572
- autocomplete="tel"
573
- />
574
- <input
575
- class="voice-input"
576
- id="auth-signup-password"
577
- type="password"
578
- placeholder="Password"
579
- autocomplete="new-password"
580
- />
581
- <div class="auth-actions">
582
- <button class="btn-primary" id="auth-signup-submit">Create account</button>
583
- </div>
584
-
585
- <div class="auth-otp-box">
586
- <label class="voice-label" for="auth-otp-email">OTP verification</label>
587
- <input
588
- class="voice-input"
589
- id="auth-otp-email"
590
- type="email"
591
- placeholder="Email for OTP"
592
- autocomplete="email"
593
- />
594
- <div class="auth-actions">
595
- <button class="btn-secondary" id="auth-otp-request">Request OTP</button>
596
- </div>
597
- <input
598
- class="voice-input"
599
- id="auth-otp-code"
600
- type="text"
601
- placeholder="6-digit code"
602
- maxlength="6"
603
- inputmode="numeric"
604
- />
605
- <div class="auth-actions">
606
- <button class="btn-primary" id="auth-otp-verify">Verify OTP</button>
607
- </div>
608
- </div>
609
- </div>
610
-
611
- <div class="auth-current" id="auth-current"></div>
612
- <div class="auth-actions auth-footer">
613
- <button class="btn-secondary" id="auth-logout">Logout</button>
614
- </div>
615
- </div>
616
- </div>
617
-
618
  <!-- ============ SIDE PANEL (Grammar + Vocab) ============ -->
619
  <div class="side-panel hidden" id="side-panel">
620
  <div class="panel-header">
 
516
  </div>
517
  </div>
518
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
  <!-- ============ SIDE PANEL (Grammar + Vocab) ============ -->
520
  <div class="side-panel hidden" id="side-panel">
521
  <div class="panel-header">
templates/login.html ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en" dir="ltr" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Login — Echo Tutor</title>
7
+ <link
8
+ rel="stylesheet"
9
+ href="{{ url_for('static', filename='css/style.css') }}"
10
+ />
11
+ </head>
12
+ <body>
13
+ <canvas id="particles-canvas"></canvas>
14
+ <div class="toast" id="toast"></div>
15
+
16
+ <div class="onboarding-screen">
17
+ <div class="onboarding-content auth-standalone">
18
+ <h1 class="onboarding-title">Welcome <span class="accent">Back</span></h1>
19
+ <p class="onboarding-subtitle">Login to continue your sessions</p>
20
+
21
+ <div class="auth-panel auth-page-panel">
22
+ <input class="voice-input" id="auth-login-email" type="email" placeholder="Email" autocomplete="email" />
23
+ <input class="voice-input" id="auth-login-password" type="password" placeholder="Password" autocomplete="current-password" />
24
+ <div class="auth-actions">
25
+ <button class="btn-primary" id="auth-login-submit">Login</button>
26
+ </div>
27
+ </div>
28
+
29
+ <p class="auth-switch-line">
30
+ New here?
31
+ <a href="/signup" class="accent">Create account</a>
32
+ </p>
33
+ </div>
34
+ </div>
35
+
36
+ <script src="{{ url_for('static', filename='js/auth-pages.js') }}"></script>
37
+ </body>
38
+ </html>
templates/signup.html ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en" dir="ltr" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Sign Up — Echo Tutor</title>
7
+ <link
8
+ rel="stylesheet"
9
+ href="{{ url_for('static', filename='css/style.css') }}"
10
+ />
11
+ </head>
12
+ <body>
13
+ <canvas id="particles-canvas"></canvas>
14
+ <div class="toast" id="toast"></div>
15
+
16
+ <div class="onboarding-screen">
17
+ <div class="onboarding-content auth-standalone">
18
+ <h1 class="onboarding-title">Create <span class="accent">Account</span></h1>
19
+ <p class="onboarding-subtitle">Step 1: registration</p>
20
+
21
+ <div class="auth-panel auth-page-panel">
22
+ <input class="voice-input" id="auth-signup-name" type="text" placeholder="Full name" autocomplete="name" />
23
+ <input class="voice-input" id="auth-signup-email" type="email" placeholder="Email" autocomplete="email" />
24
+ <input class="voice-input" id="auth-signup-phone" type="text" placeholder="Phone (optional)" autocomplete="tel" />
25
+ <input class="voice-input" id="auth-signup-password" type="password" placeholder="Password" autocomplete="new-password" />
26
+ <div class="auth-actions">
27
+ <button class="btn-primary" id="auth-signup-submit">Create account</button>
28
+ </div>
29
+ </div>
30
+
31
+ <p class="auth-switch-line">
32
+ Already have account?
33
+ <a href="/login" class="accent">Login</a>
34
+ </p>
35
+ </div>
36
+ </div>
37
+
38
+ <script src="{{ url_for('static', filename='js/auth-pages.js') }}"></script>
39
+ </body>
40
+ </html>
templates/verify.html ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en" dir="ltr" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Verify OTP — Echo Tutor</title>
7
+ <link
8
+ rel="stylesheet"
9
+ href="{{ url_for('static', filename='css/style.css') }}"
10
+ />
11
+ </head>
12
+ <body>
13
+ <canvas id="particles-canvas"></canvas>
14
+ <div class="toast" id="toast"></div>
15
+
16
+ <div class="onboarding-screen">
17
+ <div class="onboarding-content auth-standalone">
18
+ <h1 class="onboarding-title">Verify <span class="accent">Account</span></h1>
19
+ <p class="onboarding-subtitle">Step 2: activation via Telegram OTP</p>
20
+
21
+ <div class="auth-panel auth-page-panel">
22
+ <input class="voice-input" id="auth-otp-email" type="email" placeholder="Email for OTP" autocomplete="email" />
23
+ <div class="auth-actions">
24
+ <button class="btn-secondary" id="auth-otp-request">Request OTP</button>
25
+ </div>
26
+ <input class="voice-input" id="auth-otp-code" type="text" placeholder="6-digit code" maxlength="6" inputmode="numeric" />
27
+ <div class="auth-actions">
28
+ <button class="btn-primary" id="auth-otp-verify">Verify OTP</button>
29
+ <button class="btn-secondary" id="auth-check-verified">Check status</button>
30
+ </div>
31
+ </div>
32
+
33
+ <p class="auth-switch-line">
34
+ After verification, go to
35
+ <a href="/login" class="accent">Login</a>
36
+ </p>
37
+ </div>
38
+ </div>
39
+
40
+ <script src="{{ url_for('static', filename='js/auth-pages.js') }}"></script>
41
+ </body>
42
+ </html>