Anish-530 commited on
Commit
c041d0d
·
1 Parent(s): fe56754

Added email code verif

Browse files
backend/app/ai/explanation_formatter.py CHANGED
@@ -39,5 +39,14 @@ def format_explanation_with_llm(structured_data: dict) -> str:
39
  return _fallback_string_formatter(structured_data)
40
 
41
  def _fallback_string_formatter(data: dict) -> str:
 
 
42
  reasons = [r["reason"] for r in data.get("region_analysis", [])]
43
- return f"Label: {data['label']} ({data['confidence_level']} Confidence). Reasons: {' | '.join(reasons)}"
 
 
 
 
 
 
 
 
39
  return _fallback_string_formatter(structured_data)
40
 
41
  def _fallback_string_formatter(data: dict) -> str:
42
+ label = data.get("label", "Unknown").lower()
43
+ conf = data.get("confidence_level", "Medium").lower()
44
  reasons = [r["reason"] for r in data.get("region_analysis", [])]
45
+ reasons_str = ", and ".join(reasons) if reasons else "based on visual and structural analysis"
46
+
47
+ if "ai" in label or "fake" in label:
48
+ return f"This media appears to be AI-generated with {conf} confidence, primarily flagged because of {reasons_str}."
49
+ elif "real" in label or "authentic" in label:
50
+ return f"This media appears to be authentic with {conf} confidence, showing natural characteristics."
51
+ else:
52
+ return f"This media is flagged as {label} due to suspicious patterns like {reasons_str}."
backend/app/api/auth_routes.py CHANGED
@@ -114,9 +114,9 @@ def reset_password(data: PasswordResetConfirm, request: Request, db: Session = D
114
 
115
  @router.post("/verify-email")
116
  def verify_email(data: VerifyEmailConfirm, db: Session = Depends(get_db)):
117
- user = confirm_email_verification(db, data.token)
118
  if not user:
119
- raise HTTPException(status_code=400, detail="Invalid or expired verification token")
120
  token = create_access_token({"user_id": user.id})
121
  return {"message": "Email successfully verified!", "access_token": token}
122
 
@@ -129,7 +129,7 @@ def resend_verification_endpoint(
129
  raw_token = resend_verification(db, data.email)
130
  if raw_token:
131
  background_tasks.add_task(send_verification_email, data.email, raw_token)
132
- return {"message": "If the account exists and is unverified, a new link has been sent."}
133
 
134
  @router.get("/me")
135
  def get_current_active_user(
 
114
 
115
  @router.post("/verify-email")
116
  def verify_email(data: VerifyEmailConfirm, db: Session = Depends(get_db)):
117
+ user = confirm_email_verification(db, data.email, data.code)
118
  if not user:
119
+ raise HTTPException(status_code=400, detail="Invalid or expired verification code")
120
  token = create_access_token({"user_id": user.id})
121
  return {"message": "Email successfully verified!", "access_token": token}
122
 
 
129
  raw_token = resend_verification(db, data.email)
130
  if raw_token:
131
  background_tasks.add_task(send_verification_email, data.email, raw_token)
132
+ return {"message": "If the account exists and is unverified, a verification code has been sent."}
133
 
134
  @router.get("/me")
135
  def get_current_active_user(
backend/app/api/feedback_routes.py CHANGED
@@ -5,6 +5,7 @@ from app.models.feedback_model import Feedback
5
  from app.core.auth_dependancy import get_current_user
6
  from app.schemas.feedback_schema import FeedbackCreate
7
  from app.ai.meta_classifier import retrain_from_feedback
 
8
 
9
  router = APIRouter(prefix="/feedback", tags=["Feedback"])
10
 
@@ -15,13 +16,18 @@ def submit_feedback(
15
  db: Session = Depends(get_db),
16
  user = Depends(get_current_user)
17
  ):
 
 
 
 
 
18
  # Check if user already submitted feedback for this file
19
- existing = db.query(Feedback).filter(Feedback.file_id == payload.file_id, Feedback.user_id == user.id).first()
20
  if existing:
21
  raise HTTPException(status_code=400, detail="Feedback already submitted for this file.")
22
 
23
  fb = Feedback(
24
- file_id=payload.file_id,
25
  user_id=user.id,
26
  label=payload.label,
27
  confidence=payload.confidence,
 
5
  from app.core.auth_dependancy import get_current_user
6
  from app.schemas.feedback_schema import FeedbackCreate
7
  from app.ai.meta_classifier import retrain_from_feedback
8
+ from app.api.file_routes import decode_id
9
 
10
  router = APIRouter(prefix="/feedback", tags=["Feedback"])
11
 
 
16
  db: Session = Depends(get_db),
17
  user = Depends(get_current_user)
18
  ):
19
+ try:
20
+ real_file_id = decode_id(payload.file_id)
21
+ except HTTPException:
22
+ raise HTTPException(status_code=400, detail="Invalid file ID")
23
+
24
  # Check if user already submitted feedback for this file
25
+ existing = db.query(Feedback).filter(Feedback.file_id == real_file_id, Feedback.user_id == user.id).first()
26
  if existing:
27
  raise HTTPException(status_code=400, detail="Feedback already submitted for this file.")
28
 
29
  fb = Feedback(
30
+ file_id=real_file_id,
31
  user_id=user.id,
32
  label=payload.label,
33
  confidence=payload.confidence,
backend/app/core/config.py CHANGED
@@ -7,7 +7,7 @@ class Settings(BaseSettings):
7
  MODEL_DIR: str = "../models/"
8
  UPLOAD_DIR: str = "uploads/"
9
  LOG_LEVEL: str = "INFO"
10
- ENABLE_LLM_EXPLANATION: bool = False
11
  HUGGINGFACE_API_KEY: str | None = None
12
  API_URL: str = "https://api-inference.huggingface.co/models/mistralai/Mistral-7B-Instruct-v0.3"
13
  SESSION_SECRET_KEY: str = "fallback_secret_if_not_found"
 
7
  MODEL_DIR: str = "../models/"
8
  UPLOAD_DIR: str = "uploads/"
9
  LOG_LEVEL: str = "INFO"
10
+ ENABLE_LLM_EXPLANATION: bool = True
11
  HUGGINGFACE_API_KEY: str | None = None
12
  API_URL: str = "https://api-inference.huggingface.co/models/mistralai/Mistral-7B-Instruct-v0.3"
13
  SESSION_SECRET_KEY: str = "fallback_secret_if_not_found"
backend/app/core/logger.py CHANGED
@@ -40,31 +40,32 @@ logger.add(
40
  enqueue=True
41
  )
42
 
43
- # 6. Betterstack Logtail Cloud Sink (free remote logging)
44
- # Set LOGTAIL_SOURCE_TOKEN in your HuggingFace Space secrets to enable.
45
- _LOGTAIL_TOKEN = os.environ.get("LOGTAIL_SOURCE_TOKEN", "")
46
- if _LOGTAIL_TOKEN:
47
- def _logtail_sink(message):
 
 
 
 
 
 
48
  record = message.record
49
  try:
50
- _requests.post(
51
- "https://in.logs.betterstack.com",
52
- headers={
53
- "Authorization": f"Bearer {_LOGTAIL_TOKEN}",
54
- "Content-Type": "application/json",
55
- },
56
- json={
57
- "message": record["message"],
58
- "level": record["level"].name,
59
  "request_id": record["extra"].get("request_id", "SYSTEM"),
60
  "logger": record["name"],
61
  "function": record["function"],
62
  "line": record["line"],
63
- "dt": record["time"].isoformat(),
64
- },
65
- timeout=3,
66
  )
67
  except Exception:
68
  pass # Never let cloud logging failure affect the app
69
 
70
- logger.add(_logtail_sink, level="INFO", enqueue=True)
 
40
  enqueue=True
41
  )
42
 
43
+ # 6. New Relic Cloud Sink
44
+ # Set NEW_RELIC_LICENSE_KEY and NEW_RELIC_APP_NAME in your environment.
45
+ _NR_KEY = os.environ.get("NEW_RELIC_LICENSE_KEY", "")
46
+ if _NR_KEY:
47
+ import newrelic.agent
48
+ try:
49
+ newrelic.agent.initialize()
50
+ except Exception:
51
+ pass
52
+
53
+ def _newrelic_sink(message):
54
  record = message.record
55
  try:
56
+ # Send to New Relic Logs APM
57
+ newrelic.agent.record_log_event(
58
+ message=record["message"],
59
+ level=record["level"].name,
60
+ timestamp=int(record["time"].timestamp() * 1000),
61
+ attributes={
 
 
 
62
  "request_id": record["extra"].get("request_id", "SYSTEM"),
63
  "logger": record["name"],
64
  "function": record["function"],
65
  "line": record["line"],
66
+ }
 
 
67
  )
68
  except Exception:
69
  pass # Never let cloud logging failure affect the app
70
 
71
+ logger.add(_newrelic_sink, level="INFO", enqueue=True)
backend/app/schemas/auth_schema.py CHANGED
@@ -12,7 +12,8 @@ class PasswordResetConfirm(BaseModel):
12
  new_password: str
13
 
14
  class VerifyEmailConfirm(BaseModel):
15
- token: str
 
16
 
17
  class ResendVerificationRequest(BaseModel):
18
  email: EmailStr
 
12
  new_password: str
13
 
14
  class VerifyEmailConfirm(BaseModel):
15
+ email: EmailStr
16
+ code: str
17
 
18
  class ResendVerificationRequest(BaseModel):
19
  email: EmailStr
backend/app/schemas/feedback_schema.py CHANGED
@@ -2,7 +2,7 @@ from pydantic import BaseModel
2
  from typing import Optional
3
 
4
  class FeedbackCreate(BaseModel):
5
- file_id: int
6
  label: str
7
  confidence: float
8
  freq_score: float
 
2
  from typing import Optional
3
 
4
  class FeedbackCreate(BaseModel):
5
+ file_id: str
6
  label: str
7
  confidence: float
8
  freq_score: float
backend/app/services/auth_service.py CHANGED
@@ -92,17 +92,14 @@ def confirm_password_reset(db: Session, token: str, new_password: str, request:
92
  log_audit_event(db, "password_reset", target_user.id, request)
93
  return True
94
 
95
- def confirm_email_verification(db: Session, token: str) -> bool:
96
- unverified_users = db.query(User).filter(User.is_verified == False).all()
97
-
98
- target_user = None
99
- for user in unverified_users:
100
- if user.verification_token_hash and verify_password(token, user.verification_token_hash):
101
- target_user = user
102
- break
103
 
104
  if not target_user:
105
  return None
 
 
 
106
 
107
  if datetime.now(UTC) > target_user.verification_token_expire_at.replace(tzinfo=UTC):
108
  return None
@@ -120,9 +117,9 @@ def resend_verification(db: Session, email: str) -> str | None:
120
  if not user or user.is_verified:
121
  return None
122
 
123
- raw_token = secrets.token_urlsafe(32)
124
  hashed_token = hash_password(raw_token)
125
- expire_date = datetime.now(UTC) + timedelta(hours=24)
126
 
127
  user.verification_token_hash = hashed_token
128
  user.verification_token_expire_at = expire_date
 
92
  log_audit_event(db, "password_reset", target_user.id, request)
93
  return True
94
 
95
+ def confirm_email_verification(db: Session, email: str, code: str) -> User | None:
96
+ target_user = db.query(User).filter(User.email == email, User.is_verified == False).first()
 
 
 
 
 
 
97
 
98
  if not target_user:
99
  return None
100
+
101
+ if not target_user.verification_token_hash or not verify_password(code, target_user.verification_token_hash):
102
+ return None
103
 
104
  if datetime.now(UTC) > target_user.verification_token_expire_at.replace(tzinfo=UTC):
105
  return None
 
117
  if not user or user.is_verified:
118
  return None
119
 
120
+ raw_token = f"{secrets.randbelow(900000) + 100000}"
121
  hashed_token = hash_password(raw_token)
122
+ expire_date = datetime.now(UTC) + timedelta(minutes=10)
123
 
124
  user.verification_token_hash = hashed_token
125
  user.verification_token_expire_at = expire_date
backend/app/services/email_service.py CHANGED
@@ -60,8 +60,6 @@ async def send_reset_password_email(email_to: str, raw_token: str):
60
  logger.error(f"Failed to send password reset email via SMTP: {str(e)}")
61
 
62
  async def send_verification_email(email_to: str, raw_token: str):
63
- verify_link = f"{settings.FRONTEND_URL}/verify-email?token={raw_token}"
64
-
65
  html_content = f"""
66
  <div style="font-family: 'Inter', Arial, sans-serif; max-width: 600px; margin: 0 auto; background-color: #080c14; border: 1px solid #1f2937; border-radius: 16px; overflow: hidden;">
67
  <div style="background-color: #11141d; padding: 40px; text-align: center; border-bottom: 1px solid #1f2937;">
@@ -70,12 +68,12 @@ async def send_verification_email(email_to: str, raw_token: str):
70
  </div>
71
  <div style="padding: 40px; color: #d0c4bb; line-height: 1.6;">
72
  <h2 style="color: #fde8d6; margin-top: 0; font-size: 20px;">Welcome to the Engine.</h2>
73
- <p>Please confirm your neural-link authorization by verifying your email address.</p>
74
- <p>Click the secure uplink button below to activate your account. <strong style="color: #fde8d6;">This link will expire in 24 hours.</strong></p>
75
  <div style="text-align: center; margin: 40px 0;">
76
- <a href="{verify_link}" style="background-color: #fde8d6; color: #080c14; padding: 14px 32px; text-decoration: none; border-radius: 8px; font-weight: bold; display: inline-block; text-transform: uppercase; letter-spacing: 1px; font-size: 14px;">
77
- Verify Authorization
78
- </a>
79
  </div>
80
  <div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #1f2937; font-size: 11px; text-align: center; opacity: 0.5; text-transform: uppercase; letter-spacing: 1px;">
81
  © 2026 SPOTIX KINETIC. ENGINEERED FOR THE ETHEREAL.
@@ -85,7 +83,7 @@ async def send_verification_email(email_to: str, raw_token: str):
85
  """
86
 
87
  if not settings.SMTP_USER or not settings.SMTP_SERVER:
88
- logger.warning(f"[DEV MODE] SMTP credentials missing. Verif link: {verify_link}")
89
  return
90
 
91
  try:
 
60
  logger.error(f"Failed to send password reset email via SMTP: {str(e)}")
61
 
62
  async def send_verification_email(email_to: str, raw_token: str):
 
 
63
  html_content = f"""
64
  <div style="font-family: 'Inter', Arial, sans-serif; max-width: 600px; margin: 0 auto; background-color: #080c14; border: 1px solid #1f2937; border-radius: 16px; overflow: hidden;">
65
  <div style="background-color: #11141d; padding: 40px; text-align: center; border-bottom: 1px solid #1f2937;">
 
68
  </div>
69
  <div style="padding: 40px; color: #d0c4bb; line-height: 1.6;">
70
  <h2 style="color: #fde8d6; margin-top: 0; font-size: 20px;">Welcome to the Engine.</h2>
71
+ <p>Please confirm your neural-link authorization by entering the following verification code.</p>
72
+ <p><strong style="color: #fde8d6;">This code will expire in 10 minutes.</strong></p>
73
  <div style="text-align: center; margin: 40px 0;">
74
+ <div style="background-color: #1f2937; color: #fde8d6; padding: 20px; border-radius: 8px; font-weight: bold; display: inline-block; letter-spacing: 4px; font-size: 32px; font-family: monospace;">
75
+ {raw_token}
76
+ </div>
77
  </div>
78
  <div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #1f2937; font-size: 11px; text-align: center; opacity: 0.5; text-transform: uppercase; letter-spacing: 1px;">
79
  © 2026 SPOTIX KINETIC. ENGINEERED FOR THE ETHEREAL.
 
83
  """
84
 
85
  if not settings.SMTP_USER or not settings.SMTP_SERVER:
86
+ logger.warning(f"[DEV MODE] SMTP credentials missing. Verification code: {raw_token}")
87
  return
88
 
89
  try:
backend/app/services/user_service.py CHANGED
@@ -16,9 +16,9 @@ def create_user(db: Session, email: str, username: str, password: str) -> tuple[
16
 
17
  hashed_password = hash_password(password)
18
 
19
- raw_token = secrets.token_urlsafe(32)
20
  hashed_token = hash_password(raw_token)
21
- expire_date = datetime.now(UTC) + timedelta(hours=24)
22
 
23
  new_user = User(
24
  email=email,
 
16
 
17
  hashed_password = hash_password(password)
18
 
19
+ raw_token = f"{secrets.randbelow(900000) + 100000}"
20
  hashed_token = hash_password(raw_token)
21
+ expire_date = datetime.now(UTC) + timedelta(minutes=10)
22
 
23
  new_user = User(
24
  email=email,
backend/requirements.txt CHANGED
@@ -180,3 +180,4 @@ websocket-client==1.9.0
180
  wrapt==2.1.1
181
 
182
  boto3==1.34.84
 
 
180
  wrapt==2.1.1
181
 
182
  boto3==1.34.84
183
+ newrelic==9.13.0
frontend/app/login/page.tsx CHANGED
@@ -6,11 +6,12 @@ import { useRouter } from "next/navigation";
6
  import { Turnstile } from "@marsidev/react-turnstile";
7
 
8
  export default function LoginPage() {
9
- const [mode, setMode] = useState<'login' | 'signup'>('login');
10
  const [identifier, setIdentifier] = useState("");
11
  const [email, setEmail] = useState("");
12
  const [username, setUsername] = useState("");
13
  const [password, setPassword] = useState("");
 
14
  const [error, setError] = useState("");
15
  const [successMessage, setSuccessMessage] = useState("");
16
  const [loading, setLoading] = useState(false);
@@ -86,24 +87,30 @@ export default function LoginPage() {
86
  });
87
  localStorage.setItem("access_token", res.data.access_token);
88
  window.location.href = "/dashboard";
89
- } else {
90
  const res = await axios.post(`${baseUrl}/users/register`, { email, username, password }, {
91
  headers: { "cf-turnstile-response": turnstileToken }
92
  });
93
- setSuccessMessage(res.data.message || "Account created! Check your email to verify.");
94
- setMode('login');
95
  setLoading(false);
 
 
 
 
96
  }
97
  } catch (err: any) {
98
  if (!err.response) {
99
  // Network error — backend unreachable or CORS
100
- setError("Unable to reach the server. Please try again shortly.");
101
  } else {
102
  const detail = err.response?.data?.detail;
103
  if (Array.isArray(detail)) {
104
  setError(detail[0]?.msg || "Validation error. Please check your inputs.");
 
 
105
  } else {
106
- setError(detail || "Authentication failed. Please check your credentials.");
107
  }
108
  }
109
  setLoading(false);
@@ -170,55 +177,71 @@ export default function LoginPage() {
170
  )}
171
 
172
  <form onSubmit={handleSubmit} className="space-y-4">
173
- {mode === 'signup' && (
174
  <div className="relative group">
175
- <User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#d0c4bb]/40 group-focus-within:text-[var(--theme-text)] transition-colors" />
176
  <input
177
  type="text"
178
- required={mode === 'signup'}
179
- value={username}
180
- onChange={(e) => setUsername(e.target.value)}
181
- placeholder="Username"
182
- className="w-full bg-theme-text/5 border border-theme-border rounded-xl py-4 pl-12 pr-4 text-[var(--theme-text)] placeholder-[#d0c4bb]/60 focus:outline-none focus:border-theme-border/50 focus:bg-theme-text/10 transition-all font-mono text-sm"
183
- />
184
- </div>
185
- )}
186
- {mode === 'login' ? (
187
- <div className="relative group">
188
- <Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#d0c4bb]/40 group-focus-within:text-[var(--theme-text)] transition-colors" />
189
- <input
190
- type="text"
191
- required={mode === 'login'}
192
- value={identifier}
193
- onChange={(e) => setIdentifier(e.target.value)}
194
- placeholder="Email or Username"
195
- className="w-full bg-theme-text/5 border border-theme-border rounded-xl py-4 pl-12 pr-4 text-[var(--theme-text)] placeholder-[#d0c4bb]/60 focus:outline-none focus:border-theme-border/50 focus:bg-theme-text/10 transition-all font-mono text-sm"
196
  />
197
  </div>
198
  ) : (
199
- <div className="relative group">
200
- <Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#d0c4bb]/40 group-focus-within:text-[var(--theme-text)] transition-colors" />
201
- <input
202
- type="email"
203
- required={mode === 'signup'}
204
- value={email}
205
- onChange={(e) => setEmail(e.target.value)}
206
- placeholder="Email"
207
- className="w-full bg-theme-text/5 border border-theme-border rounded-xl py-4 pl-12 pr-4 text-[var(--theme-text)] placeholder-[#d0c4bb]/60 focus:outline-none focus:border-theme-border/50 focus:bg-theme-text/10 transition-all font-mono text-sm"
208
- />
209
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  )}
211
- <div className="relative group">
212
- <KeyRound className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#d0c4bb]/40 group-focus-within:text-[var(--theme-text)] transition-colors" />
213
- <input
214
- type="password"
215
- required
216
- value={password}
217
- onChange={(e) => setPassword(e.target.value)}
218
- placeholder="Password"
219
- className="w-full bg-theme-text/5 border border-theme-border rounded-xl py-4 pl-12 pr-4 text-[var(--theme-text)] placeholder-[#d0c4bb]/60 focus:outline-none focus:border-theme-border/50 focus:bg-theme-text/10 transition-all font-mono text-sm"
220
- />
221
- </div>
222
 
223
  <div className="flex justify-center mt-4">
224
  <Turnstile
@@ -238,7 +261,7 @@ export default function LoginPage() {
238
  className="w-full mt-2 py-4 rounded-xl relative overflow-hidden group dash-border bg-theme-text/5 hover:bg-theme-text/10 transition"
239
  >
240
  <span className="relative z-10 font-bold tracking-widest text-sm uppercase flex items-center justify-center gap-2">
241
- {loading && mode === 'login' ? 'Authorizing...' : loading && mode === 'signup' ? 'Registering...' : mode === 'login' ? 'Login' : 'Register'}
242
  <ArrowRight className={`w-4 h-4 transition-transform group-hover:translate-x-1 ${loading ? 'hidden' : ''}`} />
243
  </span>
244
  </button>
@@ -246,11 +269,23 @@ export default function LoginPage() {
246
 
247
  <div className="mt-4 text-center">
248
  <button
 
249
  onClick={() => setMode(mode === 'login' ? 'signup' : 'login')}
250
  className="text-xs text-[#d0c4bb]/60 hover:text-[var(--theme-text)] transition uppercase tracking-widest cursor-pointer"
251
  >
252
  {mode === 'login' ? "Don't have an Account? Create one." : "Already have an Account? Login."}
253
  </button>
 
 
 
 
 
 
 
 
 
 
 
254
  </div>
255
 
256
  <div className="mt-8 flex items-center justify-center gap-4">
 
6
  import { Turnstile } from "@marsidev/react-turnstile";
7
 
8
  export default function LoginPage() {
9
+ const [mode, setMode] = useState<'login' | 'signup' | 'verify'>('login');
10
  const [identifier, setIdentifier] = useState("");
11
  const [email, setEmail] = useState("");
12
  const [username, setUsername] = useState("");
13
  const [password, setPassword] = useState("");
14
+ const [verificationCode, setVerificationCode] = useState("");
15
  const [error, setError] = useState("");
16
  const [successMessage, setSuccessMessage] = useState("");
17
  const [loading, setLoading] = useState(false);
 
87
  });
88
  localStorage.setItem("access_token", res.data.access_token);
89
  window.location.href = "/dashboard";
90
+ } else if (mode === 'signup') {
91
  const res = await axios.post(`${baseUrl}/users/register`, { email, username, password }, {
92
  headers: { "cf-turnstile-response": turnstileToken }
93
  });
94
+ setSuccessMessage(res.data.message || "Account created! Please enter your verification code.");
95
+ setMode('verify');
96
  setLoading(false);
97
+ } else if (mode === 'verify') {
98
+ const res = await axios.post(`${baseUrl}/auth/verify-email`, { email: email || identifier, code: verificationCode });
99
+ localStorage.setItem("access_token", res.data.access_token);
100
+ window.location.href = "/dashboard";
101
  }
102
  } catch (err: any) {
103
  if (!err.response) {
104
  // Network error — backend unreachable or CORS
105
+ setError(err.message || "Unable to reach the server. Please try again shortly.");
106
  } else {
107
  const detail = err.response?.data?.detail;
108
  if (Array.isArray(detail)) {
109
  setError(detail[0]?.msg || "Validation error. Please check your inputs.");
110
+ } else if (typeof detail === 'string') {
111
+ setError(detail);
112
  } else {
113
+ setError("Server returned an unexpected error. Please try again.");
114
  }
115
  }
116
  setLoading(false);
 
177
  )}
178
 
179
  <form onSubmit={handleSubmit} className="space-y-4">
180
+ {mode === 'verify' ? (
181
  <div className="relative group">
182
+ <KeyRound className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#d0c4bb]/40 group-focus-within:text-[var(--theme-text)] transition-colors" />
183
  <input
184
  type="text"
185
+ required={mode === 'verify'}
186
+ value={verificationCode}
187
+ onChange={(e) => setVerificationCode(e.target.value)}
188
+ placeholder="6-Digit Verification Code"
189
+ className="w-full bg-theme-text/5 border border-theme-border rounded-xl py-4 pl-12 pr-4 text-[var(--theme-text)] placeholder-[#d0c4bb]/60 focus:outline-none focus:border-theme-border/50 focus:bg-theme-text/10 transition-all font-mono text-sm tracking-widest"
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  />
191
  </div>
192
  ) : (
193
+ <>
194
+ {mode === 'signup' && (
195
+ <div className="relative group">
196
+ <User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#d0c4bb]/40 group-focus-within:text-[var(--theme-text)] transition-colors" />
197
+ <input
198
+ type="text"
199
+ required={mode === 'signup'}
200
+ value={username}
201
+ onChange={(e) => setUsername(e.target.value)}
202
+ placeholder="Username"
203
+ className="w-full bg-theme-text/5 border border-theme-border rounded-xl py-4 pl-12 pr-4 text-[var(--theme-text)] placeholder-[#d0c4bb]/60 focus:outline-none focus:border-theme-border/50 focus:bg-theme-text/10 transition-all font-mono text-sm"
204
+ />
205
+ </div>
206
+ )}
207
+ {mode === 'login' ? (
208
+ <div className="relative group">
209
+ <Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#d0c4bb]/40 group-focus-within:text-[var(--theme-text)] transition-colors" />
210
+ <input
211
+ type="text"
212
+ required={mode === 'login'}
213
+ value={identifier}
214
+ onChange={(e) => setIdentifier(e.target.value)}
215
+ placeholder="Email or Username"
216
+ className="w-full bg-theme-text/5 border border-theme-border rounded-xl py-4 pl-12 pr-4 text-[var(--theme-text)] placeholder-[#d0c4bb]/60 focus:outline-none focus:border-theme-border/50 focus:bg-theme-text/10 transition-all font-mono text-sm"
217
+ />
218
+ </div>
219
+ ) : (
220
+ <div className="relative group">
221
+ <Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#d0c4bb]/40 group-focus-within:text-[var(--theme-text)] transition-colors" />
222
+ <input
223
+ type="email"
224
+ required={mode === 'signup'}
225
+ value={email}
226
+ onChange={(e) => setEmail(e.target.value)}
227
+ placeholder="Email"
228
+ className="w-full bg-theme-text/5 border border-theme-border rounded-xl py-4 pl-12 pr-4 text-[var(--theme-text)] placeholder-[#d0c4bb]/60 focus:outline-none focus:border-theme-border/50 focus:bg-theme-text/10 transition-all font-mono text-sm"
229
+ />
230
+ </div>
231
+ )}
232
+ <div className="relative group">
233
+ <KeyRound className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-[#d0c4bb]/40 group-focus-within:text-[var(--theme-text)] transition-colors" />
234
+ <input
235
+ type="password"
236
+ required
237
+ value={password}
238
+ onChange={(e) => setPassword(e.target.value)}
239
+ placeholder="Password"
240
+ className="w-full bg-theme-text/5 border border-theme-border rounded-xl py-4 pl-12 pr-4 text-[var(--theme-text)] placeholder-[#d0c4bb]/60 focus:outline-none focus:border-theme-border/50 focus:bg-theme-text/10 transition-all font-mono text-sm"
241
+ />
242
+ </div>
243
+ </>
244
  )}
 
 
 
 
 
 
 
 
 
 
 
245
 
246
  <div className="flex justify-center mt-4">
247
  <Turnstile
 
261
  className="w-full mt-2 py-4 rounded-xl relative overflow-hidden group dash-border bg-theme-text/5 hover:bg-theme-text/10 transition"
262
  >
263
  <span className="relative z-10 font-bold tracking-widest text-sm uppercase flex items-center justify-center gap-2">
264
+ {loading && mode === 'login' ? 'Authorizing...' : loading && mode === 'signup' ? 'Registering...' : loading && mode === 'verify' ? 'Verifying...' : mode === 'login' ? 'Login' : mode === 'signup' ? 'Register' : 'Verify Code'}
265
  <ArrowRight className={`w-4 h-4 transition-transform group-hover:translate-x-1 ${loading ? 'hidden' : ''}`} />
266
  </span>
267
  </button>
 
269
 
270
  <div className="mt-4 text-center">
271
  <button
272
+ type="button"
273
  onClick={() => setMode(mode === 'login' ? 'signup' : 'login')}
274
  className="text-xs text-[#d0c4bb]/60 hover:text-[var(--theme-text)] transition uppercase tracking-widest cursor-pointer"
275
  >
276
  {mode === 'login' ? "Don't have an Account? Create one." : "Already have an Account? Login."}
277
  </button>
278
+ {mode === 'login' && (
279
+ <div className="mt-2">
280
+ <button
281
+ type="button"
282
+ onClick={() => setMode('verify')}
283
+ className="text-xs text-[#d0c4bb]/40 hover:text-[var(--theme-text)] transition uppercase tracking-widest cursor-pointer"
284
+ >
285
+ Have a verification code?
286
+ </button>
287
+ </div>
288
+ )}
289
  </div>
290
 
291
  <div className="mt-8 flex items-center justify-center gap-4">