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" - > + {/* 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"} + ) : ( 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 ( -
-
-
-

Account Settings

-

- Manage your security and preferences -

-
- - {/* Change Password Section */} -
-
-
- - - -
-

Change Password

-
- -
-
- - setCurrentPassword(e.target.value)} - className="w-full px-4 py-3 bg-neutral-950 border border-neutral-800 text-sm focus:outline-none focus:ring-1 focus:ring-cyan-500 transition-all" - placeholder="••••••••" - required - /> -
- -
-
- - setNewPassword(e.target.value)} - className="w-full px-4 py-3 bg-neutral-950 border border-neutral-800 text-sm focus:outline-none focus:ring-1 focus:ring-cyan-500 transition-all" - placeholder="Min. 8 characters" - required - /> -
-
- - setConfirmPassword(e.target.value)} - className="w-full px-4 py-3 bg-neutral-950 border border-neutral-800 text-sm focus:outline-none focus:ring-1 focus:ring-cyan-500 transition-all" - placeholder="Re-enter new password" - required - /> -
-
- - {pwdMessage && ( -
- - {pwdMessage.type === "success" ? ( - - ) : ( - - )} - - {pwdMessage.text} -
- )} - -
- -
-
-
- - {/* Danger Zone */} -
-
-
- - - -
-

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. -

-
-
- ) : ( - - )} -
-
-
-
- - {/* 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. -

- -
- - -
-
-
- )} -
- ); -}; - -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" && ( -
-
-

- {message} -

-
- )} - - {status === "success" && ( -
-
- - - -
-

- Action Confirmed -

-

{message}

-

- Redirecting to home page in 5 seconds... -

-
- )} - - {status === "error" && ( -
-
- - - -
-

- Verification Error -

-

- {message} -

- -
- )} -
-
-
- ); -}; - -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);