Anish commited on
Commit
a16341c
·
1 Parent(s): ee7c0fb

[UI/UX] Added Account deletion, with some new features too.

Browse files
backend/app/api/auth_routes.py CHANGED
@@ -8,6 +8,7 @@ from app.services.oauth_service import handle_oauth_login
8
  from app.services.email_service import send_reset_password_email, send_verification_email
9
  from app.security.turnstile import verify_turnstile_token
10
  from app.core.auth_dependancy import get_current_user
 
11
 
12
  router = APIRouter(prefix="/auth", tags=["Auth"])
13
 
@@ -108,10 +109,11 @@ def reset_password(data: PasswordResetConfirm, request: Request, db: Session = D
108
 
109
  @router.post("/verify-email")
110
  def verify_email(data: VerifyEmailConfirm, db: Session = Depends(get_db)):
111
- success = confirm_email_verification(db, data.token)
112
- if not success:
113
  raise HTTPException(status_code=400, detail="Invalid or expired verification token")
114
- return {"message": "Email successfully verified!"}
 
115
 
116
  @router.post("/resend-verification")
117
  def resend_verification_endpoint(
 
8
  from app.services.email_service import send_reset_password_email, send_verification_email
9
  from app.security.turnstile import verify_turnstile_token
10
  from app.core.auth_dependancy import get_current_user
11
+ from app.utils.jwt_handler import create_access_token
12
 
13
  router = APIRouter(prefix="/auth", tags=["Auth"])
14
 
 
109
 
110
  @router.post("/verify-email")
111
  def verify_email(data: VerifyEmailConfirm, db: Session = Depends(get_db)):
112
+ user = confirm_email_verification(db, data.token)
113
+ if not user:
114
  raise HTTPException(status_code=400, detail="Invalid or expired verification token")
115
+ token = create_access_token({"user_id": user.id})
116
+ return {"message": "Email successfully verified!", "access_token": token}
117
 
118
  @router.post("/resend-verification")
119
  def resend_verification_endpoint(
backend/app/api/user_routes.py CHANGED
@@ -5,6 +5,9 @@ from app.services.user_service import create_user
5
  from app.db.session import get_db
6
  from app.services.email_service import send_verification_email
7
  from app.security.turnstile import verify_turnstile_token
 
 
 
8
 
9
  router = APIRouter(prefix="/users", tags=["Users"])
10
 
@@ -39,4 +42,15 @@ async def register(
39
  return {
40
  "message": "User Created Successfully! Please check your email to verify.",
41
  "user_id": new_user.id
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
5
  from app.db.session import get_db
6
  from app.services.email_service import send_verification_email
7
  from app.security.turnstile import verify_turnstile_token
8
+ from app.core.auth_dependancy import get_current_user
9
+ from app.models.user_model import User
10
+ from app.services.user_service import delete_user_account
11
 
12
  router = APIRouter(prefix="/users", tags=["Users"])
13
 
 
42
  return {
43
  "message": "User Created Successfully! Please check your email to verify.",
44
  "user_id": new_user.id
45
+ }
46
+
47
+ @router.delete("/delete")
48
+ def delete_account(
49
+ db: Session = Depends(get_db),
50
+ current_user: User = Depends(get_current_user)
51
+ ):
52
+ try:
53
+ delete_user_account(db, current_user.id)
54
+ return {"message": "Account successfully deleted"}
55
+ except Exception as e:
56
+ raise HTTPException(status_code=500, detail=str(e))
backend/app/services/auth_service.py CHANGED
@@ -102,17 +102,17 @@ def confirm_email_verification(db: Session, token: str) -> bool:
102
  break
103
 
104
  if not target_user:
105
- return False
106
 
107
  if datetime.now(UTC) > target_user.verification_token_expire_at.replace(tzinfo=UTC):
108
- return False
109
 
110
  target_user.is_verified = True
111
  target_user.verification_token_hash = None
112
  target_user.verification_token_expire_at = None
113
  db.commit()
114
 
115
- return True
116
 
117
  def resend_verification(db: Session, email: str) -> str | None:
118
  user = db.query(User).filter(User.email == email).first()
 
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
109
 
110
  target_user.is_verified = True
111
  target_user.verification_token_hash = None
112
  target_user.verification_token_expire_at = None
113
  db.commit()
114
 
115
+ return target_user
116
 
117
  def resend_verification(db: Session, email: str) -> str | None:
118
  user = db.query(User).filter(User.email == email).first()
backend/app/services/email_service.py CHANGED
@@ -7,19 +7,24 @@ logger = logging.getLogger(__name__)
7
  async def send_reset_password_email(email_to: str, raw_token: str):
8
  reset_link = f"{settings.FRONTEND_URL}/reset-password?token={raw_token}"
9
  html_content = f"""
10
- <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; color: #333;">
11
- <h2>Password Reset Request</h2>
12
- <p>You recently requested to reset your password for your account.</p>
13
- <p>Click the button below to reset it. <strong>This link will expire perfectly in 15 minutes.</strong></p>
14
- <div style="text-align: center; margin: 30px 0;">
15
- <a href="{reset_link}" style="background-color: #4CAF50; color: white; padding: 14px 20px; text-decoration: none; border-radius: 4px; font-weight: bold; display: inline-block;">
16
- Reset Password
17
- </a>
 
 
 
 
 
 
 
 
 
18
  </div>
19
- <p>If you did not request a password reset, please ignore this email or contact support if you have concerns.</p>
20
- <p style="font-size: 12px; color: #777; margin-top: 40px;">
21
- This is an automated message from our system.
22
- </p>
23
  </div>
24
  """
25
 
@@ -27,14 +32,15 @@ async def send_reset_password_email(email_to: str, raw_token: str):
27
  logger.warning(f"[DEV MODE] SMTP credentials missing. To manually test, use this link: {reset_link}")
28
  return
29
  try:
 
30
  conf = ConnectionConfig(
31
  MAIL_USERNAME=settings.SMTP_USER,
32
  MAIL_PASSWORD=settings.SMTP_PASSWORD,
33
  MAIL_FROM=settings.SMTP_SENDER_EMAIL or "noreply@localhost.com",
34
  MAIL_PORT=settings.SMTP_PORT,
35
  MAIL_SERVER=settings.SMTP_SERVER,
36
- MAIL_STARTTLS=True,
37
- MAIL_SSL_TLS=False,
38
  USE_CREDENTIALS=True,
39
  VALIDATE_CERTS=True
40
  )
@@ -57,14 +63,23 @@ async def send_verification_email(email_to: str, raw_token: str):
57
  verify_link = f"{settings.FRONTEND_URL}/verify-email?token={raw_token}"
58
 
59
  html_content = f"""
60
- <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; color: #333;">
61
- <h2>Welcome to Spotix!</h2>
62
- <p>Please confirm your email address to activate your account.</p>
63
- <p>Click the button below to verify. <strong>This link will expire in 24 hours.</strong></p>
64
- <div style="text-align: center; margin: 30px 0;">
65
- <a href="{verify_link}" style="background-color: #2196F3; color: white; padding: 14px 20px; text-decoration: none; border-radius: 4px; font-weight: bold; display: inline-block;">
66
- Verify My Email
67
- </a>
 
 
 
 
 
 
 
 
 
68
  </div>
69
  </div>
70
  """
@@ -74,14 +89,15 @@ async def send_verification_email(email_to: str, raw_token: str):
74
  return
75
 
76
  try:
 
77
  conf = ConnectionConfig(
78
  MAIL_USERNAME=settings.SMTP_USER,
79
  MAIL_PASSWORD=settings.SMTP_PASSWORD,
80
  MAIL_FROM=settings.SMTP_SENDER_EMAIL or "noreply@localhost.com",
81
  MAIL_PORT=settings.SMTP_PORT,
82
  MAIL_SERVER=settings.SMTP_SERVER,
83
- MAIL_STARTTLS=True,
84
- MAIL_SSL_TLS=False,
85
  USE_CREDENTIALS=True,
86
  VALIDATE_CERTS=True
87
  )
 
7
  async def send_reset_password_email(email_to: str, raw_token: str):
8
  reset_link = f"{settings.FRONTEND_URL}/reset-password?token={raw_token}"
9
  html_content = f"""
10
+ <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;">
11
+ <div style="background-color: #11141d; padding: 40px; text-align: center; border-bottom: 1px solid #1f2937;">
12
+ <h1 style="color: #fde8d6; margin: 0; font-size: 28px; letter-spacing: 2px; text-transform: uppercase; font-weight: 900;">SPOTIX</h1>
13
+ </div>
14
+ <div style="padding: 40px; color: #d0c4bb; line-height: 1.6;">
15
+ <h2 style="color: #fde8d6; margin-top: 0; font-size: 20px;">Password Reset Request</h2>
16
+ <p>You recently requested to reset your password for your Spotix account.</p>
17
+ <p>Click the secure link below to reset it. <strong style="color: #fde8d6;">This link will expire perfectly in 15 minutes.</strong></p>
18
+ <div style="text-align: center; margin: 40px 0;">
19
+ <a href="{reset_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;">
20
+ Reset Password
21
+ </a>
22
+ </div>
23
+ <p style="font-size: 14px; opacity: 0.8;">If you did not request a password reset, please ignore this email or contact support if you have concerns.</p>
24
+ <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;">
25
+ © 2026 SPOTIX KINETIC. ENGINEERED FOR THE ETHEREAL.
26
+ </div>
27
  </div>
 
 
 
 
28
  </div>
29
  """
30
 
 
32
  logger.warning(f"[DEV MODE] SMTP credentials missing. To manually test, use this link: {reset_link}")
33
  return
34
  try:
35
+ is_ssl = int(settings.SMTP_PORT) == 465
36
  conf = ConnectionConfig(
37
  MAIL_USERNAME=settings.SMTP_USER,
38
  MAIL_PASSWORD=settings.SMTP_PASSWORD,
39
  MAIL_FROM=settings.SMTP_SENDER_EMAIL or "noreply@localhost.com",
40
  MAIL_PORT=settings.SMTP_PORT,
41
  MAIL_SERVER=settings.SMTP_SERVER,
42
+ MAIL_STARTTLS=not is_ssl,
43
+ MAIL_SSL_TLS=is_ssl,
44
  USE_CREDENTIALS=True,
45
  VALIDATE_CERTS=True
46
  )
 
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;">
68
+ <h1 style="color: #fde8d6; margin: 0; font-size: 28px; letter-spacing: 2px; text-transform: uppercase; font-weight: 900;">SPOTIX</h1>
69
+ <p style="color: #d0c4bb; margin: 10px 0 0 0; font-size: 12px; letter-spacing: 3px; text-transform: uppercase; opacity: 0.7;">Identity Uplink</p>
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.
82
+ </div>
83
  </div>
84
  </div>
85
  """
 
89
  return
90
 
91
  try:
92
+ is_ssl = int(settings.SMTP_PORT) == 465
93
  conf = ConnectionConfig(
94
  MAIL_USERNAME=settings.SMTP_USER,
95
  MAIL_PASSWORD=settings.SMTP_PASSWORD,
96
  MAIL_FROM=settings.SMTP_SENDER_EMAIL or "noreply@localhost.com",
97
  MAIL_PORT=settings.SMTP_PORT,
98
  MAIL_SERVER=settings.SMTP_SERVER,
99
+ MAIL_STARTTLS=not is_ssl,
100
+ MAIL_SSL_TLS=is_ssl,
101
  USE_CREDENTIALS=True,
102
  VALIDATE_CERTS=True
103
  )
backend/app/services/file_delete_service.py CHANGED
@@ -15,6 +15,10 @@ def delete_file_service(db: Session, file_id: int, user_id: int):
15
 
16
  active_storage.delete(file.filepath)
17
 
 
 
 
 
18
  db.delete(file)
19
  db.commit()
20
 
 
15
 
16
  active_storage.delete(file.filepath)
17
 
18
+ # Delete related jobs first to prevent FK IntegrityError in SQLite
19
+ from app.models.job_model import Job
20
+ db.query(Job).filter(Job.file_id == file.id).delete()
21
+
22
  db.delete(file)
23
  db.commit()
24
 
backend/app/services/user_service.py CHANGED
@@ -2,7 +2,10 @@ import secrets
2
  from datetime import timedelta, datetime, UTC
3
  from sqlalchemy.orm import Session
4
  from app.models.user_model import User
 
 
5
  from app.utils.security import hash_password
 
6
 
7
  def create_user(db: Session, email: str, username: str, password: str) -> tuple[User | None, str | None]:
8
  if db.query(User).filter(User.email == email).first():
@@ -29,5 +32,25 @@ def create_user(db: Session, email: str, username: str, password: str) -> tuple[
29
  db.add(new_user)
30
  db.commit()
31
  db.refresh(new_user)
32
-
33
- return new_user, raw_token
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  from datetime import timedelta, datetime, UTC
3
  from sqlalchemy.orm import Session
4
  from app.models.user_model import User
5
+ from app.models.file_model import File
6
+ from app.models.user_issue_model import UserIssue
7
  from app.utils.security import hash_password
8
+ from app.services.file_delete_service import delete_file_service
9
 
10
  def create_user(db: Session, email: str, username: str, password: str) -> tuple[User | None, str | None]:
11
  if db.query(User).filter(User.email == email).first():
 
32
  db.add(new_user)
33
  db.commit()
34
  db.refresh(new_user)
35
+ return new_user, raw_token
36
+
37
+ def delete_user_account(db: Session, user_id: int):
38
+ # 1. Fetch user
39
+ user = db.query(User).filter(User.id == user_id).first()
40
+ if not user:
41
+ raise ValueError("User not found")
42
+
43
+ # 2. Delete all files (and Cloudinary storage media)
44
+ files = db.query(File).filter(File.owner_id == user_id).all()
45
+ for file in files:
46
+ try:
47
+ delete_file_service(db, file.id, user_id)
48
+ except Exception:
49
+ pass # ignore if already deleted or error, ensure we keep deleting others
50
+
51
+ # 3. Delete all User Issues
52
+ db.query(UserIssue).filter(UserIssue.user_id == user_id).delete()
53
+
54
+ # 4. Delete the user
55
+ db.delete(user)
56
+ db.commit()
backend/app/worker/tasks.py CHANGED
@@ -46,7 +46,14 @@ def process_file_task(self, file_id: int):
46
  from app.models.file_model import File
47
  db_file = db.query(File).filter(File.id == file_id).first()
48
 
49
- if db_file and db_file.filetype and db_file.filetype.startswith('video/'):
 
 
 
 
 
 
 
50
  from app.services.video_processor import process_video_pipeline
51
  process_video_pipeline(file_id, db_file.filepath, db)
52
  else:
 
46
  from app.models.file_model import File
47
  db_file = db.query(File).filter(File.id == file_id).first()
48
 
49
+ if not db_file:
50
+ logger.info(f"File {file_id} not found (possibly user deleted account). Aborting task.")
51
+ job.status = "CANCELLED"
52
+ job.finished_at = datetime.now(UTC)
53
+ db.commit()
54
+ return
55
+
56
+ if db_file.filetype and db_file.filetype.startswith('video/'):
57
  from app.services.video_processor import process_video_pipeline
58
  process_video_pipeline(file_id, db_file.filepath, db)
59
  else:
backend/main.py CHANGED
@@ -38,6 +38,7 @@ app.add_middleware(
38
  allow_headers=["*"],
39
  )
40
 
 
41
  app.mount("/static", StaticFiles(directory="."), name="static")
42
 
43
  app.state.limiter = limiter
 
38
  allow_headers=["*"],
39
  )
40
 
41
+ # Trigger reload
42
  app.mount("/static", StaticFiles(directory="."), name="static")
43
 
44
  app.state.limiter = limiter
frontend/app/favicon.ico CHANGED

Git LFS Details

  • SHA256: 2b8ad2d33455a8f736fc3a8ebf8f0bdea8848ad4c0db48a2833bd0f9cd775932
  • Pointer size: 130 Bytes
  • Size of remote file: 25.9 kB

Git LFS Details

  • SHA256: 5f24b2461684838810422ea2e4b389f440f97e720206d6ff7598925b24a13ed1
  • Pointer size: 131 Bytes
  • Size of remote file: 860 kB
frontend/app/layout.tsx CHANGED
@@ -19,7 +19,7 @@ const geistMono = Geist_Mono({
19
 
20
  export const metadata: Metadata = {
21
  title: "Spotix",
22
- description: "Detect AI generated Image/Video",
23
  };
24
 
25
  import { AuthProvider } from "@/contexts/AuthContext";
 
19
 
20
  export const metadata: Metadata = {
21
  title: "Spotix",
22
+ description: "Spotix is a modern, dark-themed SaaS platform that helps users detect whether images, videos, and audio are AI-generated or authentic. It uses multi-signal analysis—including noise patterns, frequency characteristics, and temporal consistency—to produce a confidence score along with visual explanations like heatmaps. The experience is smooth and minimal, with real-time processing, a unified upload-to-result flow, and a dashboard for tracking past analyses. Spotix focuses on clarity, trust, and explainability rather than black-box predictions.",
23
  };
24
 
25
  import { AuthProvider } from "@/contexts/AuthContext";
frontend/app/login/page.tsx CHANGED
@@ -132,7 +132,7 @@ export default function LoginPage() {
132
  </button>
133
  </div>
134
 
135
- <div className="z-10 w-full max-w-md relative mt-4 mb-16">
136
  <div className="bg-[var(--theme-bg)]/80 backdrop-blur-3xl border border-theme-border shadow-[0_0_80px_rgba(0,0,0,0.8)] rounded-3xl p-8 relative overflow-hidden">
137
  <div className="absolute -top-40 -right-40 w-80 h-80 bg-theme-text/5 rounded-full blur-[80px] pointer-events-none"></div>
138
 
@@ -155,51 +155,51 @@ export default function LoginPage() {
155
  <form onSubmit={handleSubmit} className="space-y-4">
156
  {mode === 'signup' && (
157
  <div className="relative group">
158
- <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-border)] transition-colors" />
159
  <input
160
  type="text"
161
  required={mode === 'signup'}
162
  value={username}
163
  onChange={(e) => setUsername(e.target.value)}
164
  placeholder="Username"
165
- 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]/40 focus:outline-none focus:border-theme-border/50 focus:bg-theme-text/10 transition-all font-mono text-sm"
166
  />
167
  </div>
168
  )}
169
  {mode === 'login' ? (
170
  <div className="relative group">
171
- <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-border)] transition-colors" />
172
  <input
173
  type="text"
174
  required={mode === 'login'}
175
  value={identifier}
176
  onChange={(e) => setIdentifier(e.target.value)}
177
  placeholder="Email or Username"
178
- 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]/40 focus:outline-none focus:border-theme-border/50 focus:bg-theme-text/10 transition-all font-mono text-sm"
179
  />
180
  </div>
181
  ) : (
182
  <div className="relative group">
183
- <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-border)] transition-colors" />
184
  <input
185
  type="email"
186
  required={mode === 'signup'}
187
  value={email}
188
  onChange={(e) => setEmail(e.target.value)}
189
  placeholder="Email"
190
- 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]/40 focus:outline-none focus:border-theme-border/50 focus:bg-theme-text/10 transition-all font-mono text-sm"
191
  />
192
  </div>
193
  )}
194
  <div className="relative group">
195
- <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-border)] transition-colors" />
196
  <input
197
  type="password"
198
  required
199
  value={password}
200
  onChange={(e) => setPassword(e.target.value)}
201
  placeholder="Password"
202
- 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]/40 focus:outline-none focus:border-theme-border/50 focus:bg-theme-text/10 transition-all font-mono text-sm"
203
  />
204
  </div>
205
 
 
132
  </button>
133
  </div>
134
 
135
+ <div className="z-10 w-full max-w-md relative mt-0 mb-2">
136
  <div className="bg-[var(--theme-bg)]/80 backdrop-blur-3xl border border-theme-border shadow-[0_0_80px_rgba(0,0,0,0.8)] rounded-3xl p-8 relative overflow-hidden">
137
  <div className="absolute -top-40 -right-40 w-80 h-80 bg-theme-text/5 rounded-full blur-[80px] pointer-events-none"></div>
138
 
 
155
  <form onSubmit={handleSubmit} className="space-y-4">
156
  {mode === 'signup' && (
157
  <div className="relative group">
158
+ <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" />
159
  <input
160
  type="text"
161
  required={mode === 'signup'}
162
  value={username}
163
  onChange={(e) => setUsername(e.target.value)}
164
  placeholder="Username"
165
+ 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"
166
  />
167
  </div>
168
  )}
169
  {mode === 'login' ? (
170
  <div className="relative group">
171
+ <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" />
172
  <input
173
  type="text"
174
  required={mode === 'login'}
175
  value={identifier}
176
  onChange={(e) => setIdentifier(e.target.value)}
177
  placeholder="Email or Username"
178
+ 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"
179
  />
180
  </div>
181
  ) : (
182
  <div className="relative group">
183
+ <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" />
184
  <input
185
  type="email"
186
  required={mode === 'signup'}
187
  value={email}
188
  onChange={(e) => setEmail(e.target.value)}
189
  placeholder="Email"
190
+ 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"
191
  />
192
  </div>
193
  )}
194
  <div className="relative group">
195
+ <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" />
196
  <input
197
  type="password"
198
  required
199
  value={password}
200
  onChange={(e) => setPassword(e.target.value)}
201
  placeholder="Password"
202
+ 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"
203
  />
204
  </div>
205
 
frontend/app/page.tsx CHANGED
@@ -1,7 +1,7 @@
1
  "use client";
2
 
3
  import React, { useEffect, useRef, useState, useCallback } from "react";
4
- import { useRouter } from "next/navigation";
5
  import axios from "axios";
6
  import { ArrowRight, CloudUpload, CheckCircle, Menu, X, Fingerprint, Activity, Network } from "lucide-react";
7
  import UploadZone from "@/components/upload/UploadZone";
@@ -11,6 +11,20 @@ export default function LandingPage() {
11
  const router = useRouter();
12
 
13
  const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  // --- Footer Copyright and Dynamic Date ---
16
  const [year, setYear] = useState(new Date().getFullYear());
@@ -299,6 +313,14 @@ export default function LandingPage() {
299
  </div>
300
  )}
301
 
 
 
 
 
 
 
 
 
302
  {/* Global Navbar */}
303
  <Navbar />
304
 
 
1
  "use client";
2
 
3
  import React, { useEffect, useRef, useState, useCallback } from "react";
4
+ import { useRouter, useSearchParams } from "next/navigation";
5
  import axios from "axios";
6
  import { ArrowRight, CloudUpload, CheckCircle, Menu, X, Fingerprint, Activity, Network } from "lucide-react";
7
  import UploadZone from "@/components/upload/UploadZone";
 
11
  const router = useRouter();
12
 
13
  const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
14
+ const searchParams = useSearchParams();
15
+ const [deletedToast, setDeletedToast] = useState(false);
16
+
17
+ useEffect(() => {
18
+ if (searchParams.get("deleted") === "true") {
19
+ setDeletedToast(true);
20
+ setTimeout(() => setDeletedToast(false), 5000);
21
+
22
+ // Clean up URL
23
+ const url = new URL(window.location.href);
24
+ url.searchParams.delete("deleted");
25
+ window.history.replaceState({}, '', url.toString());
26
+ }
27
+ }, [searchParams]);
28
 
29
  // --- Footer Copyright and Dynamic Date ---
30
  const [year, setYear] = useState(new Date().getFullYear());
 
313
  </div>
314
  )}
315
 
316
+ {/* Deleted Account Toast */}
317
+ {deletedToast && (
318
+ <div className="fixed top-24 left-1/2 -translate-x-1/2 z-[100] bg-green-500/10 border border-green-500/20 text-green-400 px-6 py-3 rounded-full flex items-center gap-3 shadow-[0_0_40px_rgba(34,197,94,0.2)] animate-[fade-in_0.5s_ease-out]">
319
+ <CheckCircle className="w-5 h-5" />
320
+ <span className="font-bold tracking-widest uppercase text-sm">Account Deleted Successfully</span>
321
+ </div>
322
+ )}
323
+
324
  {/* Global Navbar */}
325
  <Navbar />
326
 
frontend/app/profile/page.tsx CHANGED
@@ -16,6 +16,11 @@ export default function ProfilePage() {
16
  const [statusMsg, setStatusMsg] = useState<{type: "error" | "success", text: string} | null>(null);
17
  const [localAvatar, setLocalAvatar] = useState<string | null>(null);
18
 
 
 
 
 
 
19
  // Handle cursor physics (Globally handled now, we just need the local parallax if any, but profile page doesn't have a dash-grid)
20
 
21
  useEffect(() => {
@@ -80,6 +85,21 @@ export default function ProfilePage() {
80
  reader.readAsDataURL(file);
81
  };
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  if (loading) return null;
84
 
85
  return (
@@ -185,9 +205,9 @@ export default function ProfilePage() {
185
  <div className="border border-red-500/20 bg-red-500/5 rounded-2xl p-6 flex justify-between items-center">
186
  <div>
187
  <h4 className="text-red-400 font-semibold mb-1">Delete Account</h4>
188
- <p className="text-sm text-[var(--theme-text)]/50">Permanently delete your account and all associated data.</p>
189
  </div>
190
- <button className="bg-red-500/10 text-red-400 border border-red-500/20 px-4 py-2 rounded-lg font-bold text-sm uppercase hover:bg-red-500/20 transition-colors !cursor-none" onClick={() => alert('Please contact support to delete your account.')}>
191
  Delete
192
  </button>
193
  </div>
@@ -195,6 +215,57 @@ export default function ProfilePage() {
195
  </div>
196
  </div>
197
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  </div>
199
  );
200
  }
 
16
  const [statusMsg, setStatusMsg] = useState<{type: "error" | "success", text: string} | null>(null);
17
  const [localAvatar, setLocalAvatar] = useState<string | null>(null);
18
 
19
+ // Delete Modal States
20
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
21
+ const [deleteInput, setDeleteInput] = useState("");
22
+ const [isDeleting, setIsDeleting] = useState(false);
23
+
24
  // Handle cursor physics (Globally handled now, we just need the local parallax if any, but profile page doesn't have a dash-grid)
25
 
26
  useEffect(() => {
 
85
  reader.readAsDataURL(file);
86
  };
87
 
88
+ const handleDeleteAccount = async () => {
89
+ if (deleteInput !== "DELETE") return;
90
+ setIsDeleting(true);
91
+ try {
92
+ await apiLayer.deleteAccount();
93
+ updateUser(null);
94
+ localStorage.removeItem("access_token");
95
+ window.location.href = "/?deleted=true";
96
+ } catch (err: any) {
97
+ setStatusMsg({ type: "error", text: err.response?.data?.detail || "Failed to delete account." });
98
+ setIsDeleting(false);
99
+ setIsDeleteModalOpen(false);
100
+ }
101
+ };
102
+
103
  if (loading) return null;
104
 
105
  return (
 
205
  <div className="border border-red-500/20 bg-red-500/5 rounded-2xl p-6 flex justify-between items-center">
206
  <div>
207
  <h4 className="text-red-400 font-semibold mb-1">Delete Account</h4>
208
+ <p className="text-sm text-[var(--theme-text)]/50">This will permanently delete your account, all analyses, and uploaded media. This action cannot be undone.</p>
209
  </div>
210
+ <button className="bg-red-500/10 text-red-400 border border-red-500/20 px-4 py-2 rounded-lg font-bold text-sm uppercase hover:bg-red-500/20 transition-colors !cursor-none" onClick={() => setIsDeleteModalOpen(true)}>
211
  Delete
212
  </button>
213
  </div>
 
215
  </div>
216
  </div>
217
  </div>
218
+
219
+ {/* Delete Account Modal */}
220
+ {isDeleteModalOpen && (
221
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
222
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity" onClick={() => setIsDeleteModalOpen(false)}></div>
223
+ <div className="relative bg-[#080c13] border border-red-500/20 rounded-2xl w-full max-w-md p-8 shadow-[0_0_40px_rgba(239,68,68,0.15)] flex flex-col items-center text-center transform transition-all">
224
+ <div className="w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center mb-6">
225
+ <Trash2 className="w-8 h-8 text-red-500" />
226
+ </div>
227
+ <h2 className="text-2xl font-black text-white uppercase tracking-widest mb-2">Delete Account</h2>
228
+ <p className="text-white/60 text-sm mb-6">
229
+ This will permanently delete your account, all analyses, and uploaded media. This action cannot be undone.
230
+ </p>
231
+
232
+ <div className="w-full mb-8">
233
+ <label className="block text-left text-xs uppercase tracking-widest text-red-400/80 mb-2 font-bold">Type "DELETE" to confirm</label>
234
+ <input
235
+ type="text"
236
+ value={deleteInput}
237
+ onChange={(e) => setDeleteInput(e.target.value.toUpperCase())}
238
+ placeholder="DELETE"
239
+ className="w-full bg-red-500/5 border border-red-500/20 rounded-lg p-3 text-center outline-none focus:border-red-500/50 text-white transition-colors !cursor-none uppercase"
240
+ />
241
+ </div>
242
+
243
+ <div className="flex w-full gap-4">
244
+ <button
245
+ onClick={() => {
246
+ setIsDeleteModalOpen(false);
247
+ setDeleteInput("");
248
+ }}
249
+ disabled={isDeleting}
250
+ className="flex-1 border border-white/10 text-white/70 py-3 rounded-lg font-bold text-xs uppercase hover:bg-white/5 hover:text-white transition-colors !cursor-none disabled:opacity-50"
251
+ >
252
+ Cancel
253
+ </button>
254
+ <button
255
+ onClick={handleDeleteAccount}
256
+ disabled={deleteInput !== "DELETE" || isDeleting}
257
+ className="flex-1 bg-red-500 text-white py-3 rounded-lg font-bold text-xs uppercase hover:bg-red-600 transition-colors !cursor-none disabled:opacity-50 disabled:cursor-not-allowed flex justify-center items-center"
258
+ >
259
+ {isDeleting ? (
260
+ <div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
261
+ ) : (
262
+ "Confirm Delete"
263
+ )}
264
+ </button>
265
+ </div>
266
+ </div>
267
+ </div>
268
+ )}
269
  </div>
270
  );
271
  }
frontend/app/verify-email/page.tsx ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useEffect, useState } from "react";
4
+ import { useSearchParams, useRouter } from "next/navigation";
5
+ import axios from "axios";
6
+ import { CheckCircle, XCircle, ArrowRight, ArrowLeft } from "lucide-react";
7
+ import { useRef } from "react";
8
+
9
+ export default function VerifyEmailPage() {
10
+ const searchParams = useSearchParams();
11
+ const router = useRouter();
12
+ const token = searchParams.get("token");
13
+
14
+ const [status, setStatus] = useState<"verifying" | "success" | "error">("verifying");
15
+ const [message, setMessage] = useState("Establishing Neural Uplink...");
16
+ const hasAttempted = useRef(false);
17
+
18
+ useEffect(() => {
19
+ if (!token) {
20
+ setStatus("error");
21
+ setMessage("Invalid Authorization Link.");
22
+ return;
23
+ }
24
+
25
+ if (hasAttempted.current) return;
26
+ hasAttempted.current = true;
27
+
28
+ const verifyToken = async () => {
29
+ try {
30
+ const res = await axios.post("http://localhost:8000/auth/verify-email", { token });
31
+ setStatus("success");
32
+ setMessage("Identity Verified. Re-routing to Dashboard...");
33
+
34
+ // Automatically log the user in
35
+ if (res.data.access_token) {
36
+ localStorage.setItem("access_token", res.data.access_token);
37
+ // Slight delay for cinematic effect before redirecting
38
+ setTimeout(() => {
39
+ window.location.href = "/dashboard";
40
+ }, 2000);
41
+ }
42
+ } catch (err: any) {
43
+ setStatus("error");
44
+ setMessage(err.response?.data?.detail || "Verification failed. The link may have expired.");
45
+ }
46
+ };
47
+
48
+ verifyToken();
49
+ }, [token]);
50
+
51
+ return (
52
+ <div className="min-h-screen bg-[var(--theme-bg)] text-[var(--theme-text)] font-sans relative flex items-center justify-center p-6 overflow-hidden">
53
+ {/* Base Styles & Cursor */}
54
+ <style dangerouslySetInnerHTML={{
55
+ __html: `
56
+ html, body, a, button, [role="button"], input, select, textarea, .cursor-pointer { cursor: none !important; }
57
+ body { background-color: var(--theme-bg); }
58
+ `
59
+ }} />
60
+
61
+ {/* Background Effects */}
62
+ <div className="absolute inset-0 bg-[var(--theme-bg)] z-0"></div>
63
+ <div className="absolute inset-0 opacity-40 pointer-events-none bg-[radial-gradient(circle_at_50%_50%,_#11141d_0%,_var(--theme-bg)_100%)] z-[1]"></div>
64
+ <div className="absolute inset-0 z-[2] opacity-[0.03] pointer-events-none" style={{ backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.90' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E")` }}></div>
65
+
66
+ {/* Content Container */}
67
+ <div className="relative z-10 w-full max-w-md bg-[var(--theme-text)]/5 p-10 rounded-[2rem] border border-[var(--theme-border)] backdrop-blur-xl text-center shadow-[0_0_80px_rgba(0,0,0,0.5)]">
68
+
69
+ {status === "verifying" && (
70
+ <div className="flex flex-col items-center animate-pulse">
71
+ <div className="w-16 h-16 rounded-full border-4 border-[var(--theme-text)]/20 border-t-[var(--theme-text)] animate-spin mb-6"></div>
72
+ <h2 className="text-2xl font-black uppercase tracking-widest mb-2">Analyzing Node</h2>
73
+ <p className="text-[var(--theme-text)]/60 text-sm font-mono uppercase">{message}</p>
74
+ </div>
75
+ )}
76
+
77
+ {status === "success" && (
78
+ <div className="flex flex-col items-center">
79
+ <div className="w-16 h-16 rounded-full bg-green-500/10 flex items-center justify-center border border-green-500/30 mb-6 shadow-[0_0_40px_rgba(34,197,94,0.3)]">
80
+ <CheckCircle className="w-8 h-8 text-green-400" />
81
+ </div>
82
+ <h2 className="text-2xl font-black uppercase tracking-widest mb-2 text-green-400">Uplink Established</h2>
83
+ <p className="text-[var(--theme-text)]/60 text-sm font-mono uppercase mb-8">{message}</p>
84
+
85
+ <div className="w-full bg-[var(--theme-text)]/10 h-1 rounded-full overflow-hidden">
86
+ <div className="bg-[var(--theme-text)] h-full animate-[progress_2s_ease-in-out_forwards]"></div>
87
+ </div>
88
+ <style dangerouslySetInnerHTML={{__html: `
89
+ @keyframes progress {
90
+ 0% { width: 0%; }
91
+ 100% { width: 100%; }
92
+ }
93
+ `}} />
94
+ </div>
95
+ )}
96
+
97
+ {status === "error" && (
98
+ <div className="flex flex-col items-center">
99
+ <div className="w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center border border-red-500/30 mb-6 shadow-[0_0_40px_rgba(239,68,68,0.3)]">
100
+ <XCircle className="w-8 h-8 text-red-400" />
101
+ </div>
102
+ <h2 className="text-2xl font-black uppercase tracking-widest mb-2 text-red-400">Uplink Failed</h2>
103
+ <p className="text-[var(--theme-text)]/60 text-sm font-mono uppercase mb-8">{message}</p>
104
+
105
+ <button
106
+ onClick={() => router.push('/login')}
107
+ className="w-full group relative overflow-hidden rounded-full bg-[var(--theme-text)] text-[var(--theme-bg)] px-8 py-4 font-bold tracking-widest text-xs uppercase transition-all duration-300 hover:shadow-[0_0_40px_rgba(253,232,214,0.3)] hover:scale-105 flex items-center justify-center gap-3 !cursor-none"
108
+ >
109
+ <ArrowLeft className="w-4 h-4 transition-transform duration-300 group-hover:-translate-x-1" />
110
+ <span>Return to Access Portal</span>
111
+ </button>
112
+ </div>
113
+ )}
114
+ </div>
115
+ </div>
116
+ );
117
+ }
frontend/lib/api.ts CHANGED
@@ -37,5 +37,8 @@ export const apiLayer = {
37
  updateUsername: (username: string) => api.put('/profile/username', { username }),
38
 
39
  // Feedback
40
- submitFeedback: (data: { issue_type: string, message: string }) => api.post('/issues/', data)
 
 
 
41
  };
 
37
  updateUsername: (username: string) => api.put('/profile/username', { username }),
38
 
39
  // Feedback
40
+ submitFeedback: (data: { issue_type: string, message: string }) => api.post('/issues/', data),
41
+
42
+ // Account deletion
43
+ deleteAccount: () => api.delete('/users/delete')
44
  };