diff --git a/client/cyber_lens/src/App.tsx b/client/cyber_lens/src/App.tsx
index bd3675a..7195b62 100644
--- a/client/cyber_lens/src/App.tsx
+++ b/client/cyber_lens/src/App.tsx
@@ -11,7 +11,6 @@ import Footer from "./components/Footer";
import Home from "./pages/Home";
import History from "./pages/History";
import News from "./pages/News";
-
import Login from "./pages/Login";
import Signup from "./pages/Signup";
import SentEmail from "./pages/SentEmailConfirmation";
@@ -20,8 +19,6 @@ import RequestPasswordReset from "./pages/RequestPasswordReset";
import ResetPassword from "./pages/ResetPassword";
import Analytics from "./pages/Analytics";
import NewsDetail from "./pages/NewsDetail";
-import Settings from "./pages/Settings";
-import VerifyAction from "./pages/VerifyAction";
const Layout = () => {
return (
@@ -46,7 +43,6 @@ function App() {
} />
} />
} />
- } />
{/* -------- Auth Pages (NO Navbar / Footer) -------- */}
@@ -54,7 +50,6 @@ function App() {
} />
} />
} />
- } />
} />
} />
diff --git a/client/cyber_lens/src/components/Navbar.tsx b/client/cyber_lens/src/components/Navbar.tsx
index a646a6d..abf1970 100644
--- a/client/cyber_lens/src/components/Navbar.tsx
+++ b/client/cyber_lens/src/components/Navbar.tsx
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from "react";
import { useAuth } from "../hooks/useAuth";
import Avatar from "./Avatar";
+
const Navbar = () => {
const [open, setOpen] = useState(false);
const [profileDropdownOpen, setProfileDropdownOpen] = useState(false);
@@ -13,18 +14,20 @@ const Navbar = () => {
const [isSigningOut, setIsSigningOut] = useState(false);
const handleLogout = () => {
- if (isSigningOut) return;
+ if (isSigningOut) return;
+
+ setIsSigningOut(true);
+
+ setTimeout(() => {
+ logout();
+ setProfileDropdownOpen(false);
+ setOpen(false);
+ navigate("/");
+ setIsSigningOut(false);
+ }, 800);
+};
- setIsSigningOut(true);
- setTimeout(() => {
- logout();
- setProfileDropdownOpen(false);
- setOpen(false);
- navigate("/");
- setIsSigningOut(false);
- }, 800);
- };
// Close dropdown when clicking outside
useEffect(() => {
@@ -130,14 +133,10 @@ const Navbar = () => {
Profile
- setProfileDropdownOpen(false)}
- className="w-full px-4 py-2 text-left text-sm text-slate-300 hover:bg-slate-800 transition-colors flex items-center gap-3"
- >
+
Settings
-
+
{/* Logout */}
@@ -155,6 +154,7 @@ const Navbar = () => {
{isSigningOut ? "Signing out…" : "Logout"}
+
)}
@@ -181,11 +181,7 @@ const Navbar = () => {
{/* Mobile Menu */}
{open && (
- setOpen(false)}
- className={linkClass}
- >
+ setOpen(false)} className={linkClass}>
Home
@@ -206,11 +202,11 @@ const Navbar = () => {
setOpen(false)}
className={linkClass}
>
- Settings
+ Analytics
{isAuthenticated && user ? (
@@ -227,19 +223,19 @@ const Navbar = () => {
-
-
- {isSigningOut ? "Signing out…" : "Logout"}
-
+ >
+
+ {isSigningOut ? "Signing out…" : "Logout"}
+
>
) : (
diff --git a/client/cyber_lens/src/pages/Settings.tsx b/client/cyber_lens/src/pages/Settings.tsx
deleted file mode 100644
index 07eeeb2..0000000
--- a/client/cyber_lens/src/pages/Settings.tsx
+++ /dev/null
@@ -1,378 +0,0 @@
-import React, { useState } from "react";
-import { httpJson } from "../utils/httpClient";
-
-const Settings: React.FC = () => {
- // State for Change Password
- const [currentPassword, setCurrentPassword] = useState("");
- const [newPassword, setNewPassword] = useState("");
- const [confirmPassword, setConfirmPassword] = useState("");
- const [pwdLoading, setPwdLoading] = useState(false);
- const [pwdMessage, setPwdMessage] = useState<{
- type: "success" | "error";
- text: string;
- } | null>(null);
-
- // State for Delete Account
- const [showDeleteModal, setShowDeleteModal] = useState(false);
- const [deleteLoading, setDeleteLoading] = useState(false);
- const [deleteRequested, setDeleteRequested] = useState(false);
-
- const handleChangePassword = async (e: React.FormEvent) => {
- e.preventDefault();
- setPwdMessage(null);
-
- // Validation
- if (newPassword !== confirmPassword) {
- setPwdMessage({ type: "error", text: "New passwords do not match" });
- return;
- }
-
- if (newPassword === currentPassword) {
- setPwdMessage({
- type: "error",
- text: "New password must be different from current password",
- });
- return;
- }
-
- if (newPassword.length < 8) {
- setPwdMessage({
- type: "error",
- text: "New password must be at least 8 characters long",
- });
- return;
- }
-
- setPwdLoading(true);
- try {
- await httpJson("/auth/change-password", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ currentPassword, newPassword }),
- });
- setPwdMessage({
- type: "success",
- text: "Password updated successfully",
- });
- setCurrentPassword("");
- setNewPassword("");
- setConfirmPassword("");
- } catch (err) {
- setPwdMessage({
- type: "error",
- text: err instanceof Error ? err.message : "Failed to update password",
- });
- } finally {
- setPwdLoading(false);
- }
- };
-
- const handleRequestDelete = async () => {
- setDeleteLoading(true);
- try {
- await httpJson("/auth/request-delete", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- });
- setDeleteRequested(true);
- setShowDeleteModal(false);
- } catch (err) {
- alert(err instanceof Error ? err.message : "Failed to request deletion");
- } finally {
- setDeleteLoading(false);
- }
- };
-
- return (
-
-
-
-
- {/* Change Password Section */}
-
-
- {/* Danger Zone */}
-
-
-
-
-
-
- Delete Account
-
-
- Permanently remove your account and all associated data. This
- action is irreversible. All lookup history and settings will be
- wiped.
-
-
- {deleteRequested ? (
-
-
-
-
-
-
- Verification Required
-
-
- A magic link has been sent to your inbox. Please click the
- link within 15 minutes to confirm account deletion.
-
-
-
- ) : (
-
setShowDeleteModal(true)}
- className="inline-flex items-center justify-center px-6 py-3 border border-red-800 text-red-500 font-semibold hover:bg-red-500 hover:text-white transition-all text-sm uppercase tracking-wider"
- >
- Terminate Account Access
-
- )}
-
-
-
-
-
- {/* Delete Confirmation Modal */}
- {showDeleteModal && (
-
-
-
-
-
- Confirm Deletion
-
-
- To prevent accidents, we require email verification. Click below
- to receive a one-time magic link. Your account stays active until
- you confirm via the email.
-
-
-
-
- {deleteLoading ? (
-
-
-
-
-
- Executing Request...
-
- ) : (
- "Dispatch Verification Link"
- )}
-
-
setShowDeleteModal(false)}
- className="w-full px-6 py-4 bg-transparent border border-neutral-700 text-neutral-400 font-bold hover:bg-neutral-800 hover:text-neutral-200 transition-all uppercase tracking-widest text-xs"
- >
- Abort Action
-
-
-
-
- )}
-
- );
-};
-
-export default Settings;
diff --git a/client/cyber_lens/src/pages/VerifyAction.tsx b/client/cyber_lens/src/pages/VerifyAction.tsx
deleted file mode 100644
index 70788ea..0000000
--- a/client/cyber_lens/src/pages/VerifyAction.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { useSearchParams, useNavigate } from "react-router-dom";
-import { httpJson } from "../utils/httpClient";
-
-const VerifyAction: React.FC = () => {
- const [searchParams] = useSearchParams();
- const navigate = useNavigate();
- const [status, setStatus] = useState<"verifying" | "success" | "error">(
- "verifying",
- );
- const [message, setMessage] = useState("Verifying your security token...");
-
- useEffect(() => {
- const token = searchParams.get("token");
- const type = searchParams.get("type");
-
- if (!token || !type) {
- setStatus("error");
- setMessage("Invalid security link or missing verification data.");
- return;
- }
-
- const performVerification = async () => {
- try {
- if (type === "delete_account") {
- await httpJson("/auth/verify-delete", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ token }),
- });
- setStatus("success");
- setMessage(
- "Your account and all associated data have been permanently removed.",
- );
-
- // Clear any local session/identity if applicable
- // In this architecture, we might want to reset the anonymous-client-id
- localStorage.removeItem("anonymous-client-id");
-
- setTimeout(() => navigate("/"), 5000);
- } else {
- setStatus("error");
- setMessage("Unrecognized verification sequence.");
- }
- } catch (err) {
- setStatus("error");
- setMessage(
- err instanceof Error ? err.message : "Security verification failed.",
- );
- }
- };
-
- performVerification();
- }, [searchParams, navigate]);
-
- return (
-
-
-
- {status === "verifying" && (
-
- )}
-
- {status === "success" && (
-
-
-
- Action Confirmed
-
-
{message}
-
- Redirecting to home page in 5 seconds...
-
-
- )}
-
- {status === "error" && (
-
-
-
- Verification Error
-
-
- {message}
-
-
navigate("/")}
- className="w-full py-4 bg-neutral-800 text-neutral-200 font-bold hover:bg-neutral-700 transition-colors uppercase tracking-widest text-xs"
- >
- Return to Safety
-
-
- )}
-
-
-
- );
-};
-
-export default VerifyAction;
diff --git a/server/src/app.ts b/server/src/app.ts
index 82738e7..80d0db0 100644
--- a/server/src/app.ts
+++ b/server/src/app.ts
@@ -10,7 +10,6 @@ import resolveRuntimeOwner from "./middleware/resolveRuntimeOwner";
import newsRouter from "./routes/news";
import lookupRouter from "./routes/lookup";
import historyRouter from "./routes/history";
-import authRouter from "./routes/auth";
import analyticsRouter from "./routes/analytics";
import router from "./routes";
import { runNewsScraper } from "./services/newsScraper";
@@ -38,16 +37,31 @@ app.use(resolveOwner);
app.use(authenticateUserOptional);
app.use(resolveRuntimeOwner);
+// TEMPORARY MIDDLEWARE TO PRINT THE OWNER TYPE
+// app.use((req, _res, next) => {
+// const owner = req.owner;
+
+// if (owner?.type === "user") {
+// console.info(`[owner] user ${owner.id}`);
+// } else if (owner?.type === "anonymous") {
+// console.info(`[owner] anonymous ${owner.id}`);
+// } else {
+// console.info("[owner] unresolved");
+// }
+
+// next();
+// });
+
app.get("/", (_req, res) => {
res.json({ status: "ok" });
});
+app.use("/", router);
+app.use("/lookup", lookupRouter);
+app.use("/history", historyRouter);
// Protected routes: require a valid JWT
app.use("/analytics", authenticateUser, requireVerifiedEmail, analyticsRouter);
app.use("/", router);
app.use("/news", newsRouter);
-app.use("/lookup", lookupRouter);
-app.use("/history", historyRouter);
-app.use("/auth", authRouter);
export default app;
diff --git a/server/src/db/migrations/007_create_users_table.sql b/server/src/db/migrations/007_create_users_table.sql
deleted file mode 100644
index 48b0398..0000000
--- a/server/src/db/migrations/007_create_users_table.sql
+++ /dev/null
@@ -1,19 +0,0 @@
--- Create users table for platform access
-CREATE TABLE IF NOT EXISTS users (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- email VARCHAR(255) UNIQUE NOT NULL,
- password_hash VARCHAR(255) NOT NULL,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
-
--- Create magic_links table for secure account operations (email verification, deletion, etc.)
-CREATE TABLE IF NOT EXISTS magic_links (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- user_id UUID REFERENCES users(id) ON DELETE CASCADE,
- token_hash VARCHAR(255) NOT NULL,
- expires_at TIMESTAMP NOT NULL,
- used_at TIMESTAMP,
- type VARCHAR(50) NOT NULL,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts
deleted file mode 100644
index bdd0ee4..0000000
--- a/server/src/routes/auth.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-import { Router, Request, Response } from "express";
-import pool from "../db";
-import crypto from "crypto";
-import {
- createMagicLink,
- sendMagicLinkEmail,
- verifyMagicLink,
-} from "../utils/magicLink";
-
-const router = Router();
-
-// TODO: Use bcrypt in production
-function hashPassword(password: string): string {
- return crypto.createHash("sha256").update(password).digest("hex");
-}
-
-
-// POST /auth/change-password
-router.post("/change-password", async (req: Request, res: Response) => {
- const { currentPassword, newPassword } = req.body;
- const owner = req.owner;
-
- if (owner.type !== "user") {
- res.status(401).json({ error: "Authenticated user account required" });
- return;
- }
-
- try {
- const userResult = await pool.query(
- "SELECT password_hash FROM users WHERE id = $1",
- [owner.id]
- );
-
- if (userResult.rows.length === 0) {
- res.status(404).json({ error: "User profile not found" });
- return;
- }
-
- const { password_hash } = userResult.rows[0];
-
- if (hashPassword(currentPassword) !== password_hash) {
- res.status(400).json({ error: "The current password you entered is incorrect" });
- return;
- }
-
- await pool.query(
- "UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2",
- [hashPassword(newPassword), owner.id]
- );
-
- res.json({ message: "Security credentials updated successfully" });
- } catch (error) {
- console.error("Change password failure:", error);
- res.status(500).json({ error: "An internal error occurred while updating security settings" });
- }
-});
-
-// POST /auth/request-delete
-router.post("/request-delete", async (req: Request, res: Response) => {
- const owner = req.owner;
-
- if (owner.type !== "user") {
- res.status(401).json({ error: "Authenticated user session required" });
- return;
- }
-
- try {
- const userResult = await pool.query("SELECT email FROM users WHERE id = $1", [
- owner.id,
- ]);
-
- if (userResult.rows.length === 0) {
- res.status(404).json({ error: "User identity not found" });
- return;
- }
-
- const { email } = userResult.rows[0];
- const token = await createMagicLink(owner.id, "delete_account");
-
- await sendMagicLinkEmail(email, token, "delete_account");
-
- res.json({ message: "Verification link dispatched to your email" });
- } catch (error) {
- console.error("Delete request failure:", error);
- res.status(500).json({ error: "Failed to initiate account deletion sequence" });
- }
-});
-
-// POST /auth/verify-delete
-router.post("/verify-delete", async (req: Request, res: Response) => {
- const { token } = req.body;
-
- if (!token) {
- res.status(400).json({ error: "Verification token is missing" });
- return;
- }
-
- try {
- const userId = await verifyMagicLink(token, "delete_account");
-
- if (!userId) {
- res.status(400).json({ error: "The verification link is invalid or has expired" });
- return;
- }
-
- const client = await pool.connect();
- try {
- await client.query("BEGIN");
-
- // Clear all historical data associated with the user
- await client.query(
- "DELETE FROM ioc_history WHERE owner_type = 'user' AND owner_id = $1",
- [userId]
- );
-
- // Finalize account termination
- await client.query("DELETE FROM users WHERE id = $1", [userId]);
-
- await client.query("COMMIT");
- res.json({ message: "Account and associated history terminated successfully" });
- } catch (transactionError) {
- await client.query("ROLLBACK");
- throw transactionError;
- } finally {
- client.release();
- }
- } catch (error) {
- console.error("Delete verification failure:", error);
- res.status(500).json({ error: "A system error occurred during account termination" });
- }
-});
-
-/**
- * MOCK SIGNUP (For Testing Purposes)
- * Allows creation of a test user to verify Settings functionality
- */
-router.post("/signup", async (req: Request, res: Response) => {
- const { email, password } = req.body;
- try {
- const result = await pool.query(
- "INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id",
- [email, hashPassword(password)]
- );
- res.json({ success: true, userId: result.rows[0].id });
- } catch (error) {
- res.status(400).json({ error: "User already exists or invalid data" });
- }
-});
-
-export default router;
diff --git a/server/src/utils/magicLink.ts b/server/src/utils/magicLink.ts
deleted file mode 100644
index 56db327..0000000
--- a/server/src/utils/magicLink.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import crypto from "crypto";
-import pool from "../db";
-
-// Generate secure random token
-export function generateSecureToken(): string {
- return crypto.randomBytes(32).toString("hex");
-}
-
-
-// Hash token for storage
-export function hashToken(token: string): string {
- return crypto.createHash("sha256").update(token).digest("hex");
-}
-
-
-/**
- * Creates and stores a magic link in the database
- */
-export async function createMagicLink(
- userId: string,
- type: "delete_account" | "verify_email",
- expiresInMinutes = 15
-): Promise {
- const token = generateSecureToken();
- const tokenHash = hashToken(token);
- const expiresAt = new Date();
- expiresAt.setMinutes(expiresAt.getMinutes() + expiresInMinutes);
-
- await pool.query(
- "INSERT INTO magic_links (user_id, token_hash, expires_at, type) VALUES ($1, $2, $3, $4)",
- [userId, tokenHash, expiresAt, type]
- );
-
- return token;
-}
-
-/**
- * Validates a magic link token and returns the associated userId
- */
-export async function verifyMagicLink(
- token: string,
- type: string
-): Promise {
- const tokenHash = hashToken(token);
-
- const result = await pool.query(
- "SELECT user_id FROM magic_links WHERE token_hash = $1 AND type = $2 AND expires_at > NOW() AND used_at IS NULL",
- [tokenHash, type]
- );
-
- if (result.rows.length === 0) {
- return null;
- }
-
- const userId = result.rows[0].user_id;
-
- // Atomically mark token as used to prevent replay attacks
- await pool.query(
- "UPDATE magic_links SET used_at = NOW() WHERE token_hash = $1",
- [tokenHash]
- );
-
- return userId;
-}
-
-// Mock email sender - logs to console only
-export async function sendMagicLinkEmail(
- email: string,
- token: string,
- type: string
-): Promise {
- const baseUrl = process.env.CORS_ORIGIN || "http://localhost:5173";
- // The link points to a verification endpoint that will handle the logic
- const link = `${baseUrl}/verify-action?token=${token}&type=${type}`;
-
- console.log("\n--- [INTERNAL MAIL SERVICE] ---");
- console.log(`Target: ${email}`);
- console.log(`Action: ${type.toUpperCase()}`);
- console.log(`Verification URL: ${link}`);
- console.log("Validity: 15 minutes (Single Use Only)");
- console.log("-------------------------------\n");
-}
diff --git a/server/src/utils/resolveOwner.ts b/server/src/utils/resolveOwner.ts
index 71e490b..8a92f52 100644
--- a/server/src/utils/resolveOwner.ts
+++ b/server/src/utils/resolveOwner.ts
@@ -40,16 +40,8 @@ async function resolveOwner(
}
try {
- // Check if this is a registered user
- const userResult = await pool.query("SELECT id FROM users WHERE id = $1", [clientId]);
-
- if (userResult.rows.length > 0) {
- req.owner = { type: "user", id: clientId };
- } else {
- // Fallback to anonymous client logic
- await ensureAnonymousClient(clientId);
- req.owner = { type: "anonymous", id: clientId };
- }
+ await ensureAnonymousClient(clientId);
+ req.owner = { type: "anonymous", id: clientId };
next();
} catch (error) {
console.error("Failed to resolve owner", error);