diff --git a/gadget-code/frontend/src/App.tsx b/gadget-code/frontend/src/App.tsx index 064ec3d..bab203d 100644 --- a/gadget-code/frontend/src/App.tsx +++ b/gadget-code/frontend/src/App.tsx @@ -46,6 +46,7 @@ export function setStoredProject(slug: string | null) { interface AppContextType { user: User | null; + updateUser: (user: User) => void; currentProject: string | null; setCurrentProject: (slug: string | null) => void; onSignOut: () => void; @@ -97,7 +98,20 @@ export default function App() { socketClient.connect(token); }; - const handleSignOut = () => { + const handleUpdateUser = (updatedUser: User) => { + setStoredUser(updatedUser); + setUser(updatedUser); + }; + + const handleSignOut = async () => { + try { + await fetch('/api/v1/auth/sign-out', { + method: 'GET', + credentials: 'include', + }); + } catch { + // Ignore errors — we're signing out regardless + } localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(PROJECT_KEY); setStoredUser(null); @@ -121,7 +135,7 @@ export default function App() { } return ( - +
diff --git a/gadget-code/frontend/src/pages/Settings.tsx b/gadget-code/frontend/src/pages/Settings.tsx index 110079b..c28a5a0 100644 --- a/gadget-code/frontend/src/pages/Settings.tsx +++ b/gadget-code/frontend/src/pages/Settings.tsx @@ -6,7 +6,7 @@ import { useAppContext } from "../App"; const PERSONA_MAX = 500; export default function Settings() { - const { user, setStatusMessage } = useAppContext(); + const { user, updateUser, setStatusMessage } = useAppContext(); const navigate = useNavigate(); const [displayName, setDisplayName] = useState(user?.displayName ?? ""); @@ -76,12 +76,8 @@ export default function Settings() { setNewPassword(""); setConfirmPassword(""); - const storedUser = localStorage.getItem("dtp_user"); - if (storedUser) { - const parsed = JSON.parse(storedUser); - Object.assign(parsed, updatedUser); - localStorage.setItem("dtp_user", JSON.stringify(parsed)); - } + // Update React context + localStorage in one place + updateUser(updatedUser); } catch (err) { setError(err instanceof Error ? err.message : "Failed to save settings."); } finally { diff --git a/gadget-code/frontend/src/pages/SignIn.tsx b/gadget-code/frontend/src/pages/SignIn.tsx index 44d558c..c6a265b 100644 --- a/gadget-code/frontend/src/pages/SignIn.tsx +++ b/gadget-code/frontend/src/pages/SignIn.tsx @@ -18,7 +18,7 @@ export default function SignIn({ onSuccess }: SignInProps) { setError(""); setLoading(true); try { - const response = await api.post("/auth/sign-in", { + const response = await api.post("/api/v1/auth/sign-in", { email, password, }); diff --git a/gadget-code/src/controllers/api/v1/auth.ts b/gadget-code/src/controllers/api/v1/auth.ts index bf8fc48..a2ba629 100644 --- a/gadget-code/src/controllers/api/v1/auth.ts +++ b/gadget-code/src/controllers/api/v1/auth.ts @@ -8,7 +8,7 @@ import { Request, Response } from "express"; import { DtpController } from "../../../lib/controller.js"; import UserService from "../../../services/user.js"; -import SessionService, { SessionType } from "../../../services/session.js"; +import SessionService from "../../../services/session.js"; export class AuthApiControllerV1 extends DtpController { get name(): string { @@ -53,7 +53,6 @@ export class AuthApiControllerV1 extends DtpController { }); const token = await SessionService.createJsonWebToken(user); req.session.token = token; - req.session.type = SessionType.JWT; req.session.save((err: Error): void => { if (err) { diff --git a/gadget-code/src/controllers/api/v1/user.ts b/gadget-code/src/controllers/api/v1/user.ts index 49292b2..fcc4032 100644 --- a/gadget-code/src/controllers/api/v1/user.ts +++ b/gadget-code/src/controllers/api/v1/user.ts @@ -32,9 +32,7 @@ export class UserApiControllerV1 extends DtpController { async getUser(req: Request, res: Response): Promise { res.status(200).json({ success: true, - data: { - user: req.user, - }, + data: req.user, }); } @@ -47,9 +45,7 @@ export class UserApiControllerV1 extends DtpController { const updatedUser = await UserService.updateForUser(req.user!, req.body); res.status(200).json({ success: true, - data: { - user: updatedUser, - }, + data: updatedUser, }); } catch (error) { this.log.error("failed to update user settings", { error }); diff --git a/gadget-code/src/controllers/auth.ts b/gadget-code/src/controllers/auth.ts deleted file mode 100644 index 0ee1234..0000000 --- a/gadget-code/src/controllers/auth.ts +++ /dev/null @@ -1,138 +0,0 @@ -// src/controllers/auth.ts -// Copyright (C) 2026 Robert Colbert -// All Rights Reserved - -// import env from "../config/env.js"; -import { NextFunction, Request, Response } from "express"; - -import { DtpController } from "../lib/controller.js"; - -import UserService from "../services/user.js"; -import SessionService, { SessionType } from "../services/session.js"; - -export class AuthController extends DtpController { - get name(): string { - return "AuthController"; - } - get slug(): string { - return "auth"; - } - get route(): string { - return "/auth"; - } - - constructor() { - super(); - } - - async start(): Promise { - this.router.post("/sign-in", this.postSignIn.bind(this)); - this.router.post("/renew-token", this.postRenewToken.bind(this)); - - this.router.get("/welcome", this.getWelcomeView.bind(this)); - this.router.get("/sign-in", this.getSignInForm.bind(this)); - this.router.get("/sign-out", this.getSignOut.bind(this)); - } - - async postSignIn( - req: Request, - res: Response, - next: NextFunction - ): Promise { - try { - const user = await UserService.authenticate( - req.body.email, - req.body.password - ); - req.session.user = { - _id: user._id, - email: user.email, - displayName: user.displayName, - flags: user.flags, - }; - - const token = await SessionService.createJsonWebToken(user); - req.session.token = token; - req.session.type = SessionType.WEB; - - req.session.save((err: Error) => { - if (err) { - return next(err); - } - res.status(200).json({ - success: true, - user: { - _id: user._id.toString(), - email: user.email, - displayName: user.displayName, - flags: user.flags, - }, - token, - }); - }); - } catch (error) { - return next(error); - } - } - - async postRenewToken(req: Request, res: Response): Promise { - try { - /* - * Use req.user (set by restoreUserSession from the session cookie) - * instead of verifying the expired JWT in the request body. - * This eliminates the catch-22 where an expired token cannot be - * used to request its own renewal. - */ - if (!req.user) { - res.status(401).json({ success: false, message: "No valid session found" }); - return; - } - const token = await SessionService.createJsonWebToken(req.user); - req.session.token = token; - res.status(200).json({ success: true, token }); - } catch (error) { - this.log.error("failed to process token renewal", { error }); - res.status((error as Error).statusCode || 500).json({ - success: false, - message: (error as Error).message, - }); - } - } - - async getWelcomeView(_req: Request, res: Response): Promise { - res.status(200).json({ - success: true, - message: "Welcome to DTP Web Application", - }); - } - - async getSignInForm(_req: Request, res: Response): Promise { - res.status(200).json({ - success: true, - form: "sign-in", - }); - } - - async getSignOut( - req: Request, - res: Response, - next: NextFunction - ): Promise { - if (req.session.token) { - try { - await SessionService.revokeJsonWebToken(req.session.token); - } catch (error) { - this.log.error("failed to revoke JSON Web Token", { error }); - } - } - req.session.destroy((err: Error) => { - if (err) { - this.log.error("failed to destroy user session", { error: err }); - return next(err); - } - res.status(200).json({ success: true, message: "Signed out successfully" }); - }); - } -} - -export default AuthController; diff --git a/gadget-code/src/services/session.ts b/gadget-code/src/services/session.ts index d89dc27..e374a9e 100644 --- a/gadget-code/src/services/session.ts +++ b/gadget-code/src/services/session.ts @@ -18,11 +18,6 @@ import UserService from "../services/user.js"; import { DtpService } from "../lib/service.js"; import { PopulateOptions } from "mongoose"; -export enum SessionType { - WEB = "web", - JWT = "jwt", -} - interface UserWebToken { _id: string; email: string; diff --git a/gadget-code/src/web-app.ts b/gadget-code/src/web-app.ts index a07c23f..690de35 100644 --- a/gadget-code/src/web-app.ts +++ b/gadget-code/src/web-app.ts @@ -36,7 +36,6 @@ import { GadgetComponent, GadgetLog } from "@gadget/api"; import { User } from "./models/user.js"; import { ApiController } from "./controllers/api.js"; -import { AuthController } from "./controllers/auth.js"; import { HomeController } from "./controllers/home.js"; import { UserController } from "./controllers/user.js"; @@ -47,7 +46,6 @@ import { startServices, stopServices, } from "./services/index.js"; -import { SessionType } from "./services/session.js"; class DtpWebAppServer implements GadgetComponent { private log: GadgetLog; @@ -211,11 +209,6 @@ class DtpWebAppServer implements GadgetComponent { async createAppRouter(): Promise { const router: express.Router = express.Router(); - const authController = new AuthController(); - await authController.start(); - this.log.info("mounting AuthController", { route: authController.route }); - router.use(authController.route, authController.router); - const apiController = new ApiController(); await apiController.start(); this.log.info("mounting ApiController", { @@ -298,21 +291,13 @@ class DtpWebAppServer implements GadgetComponent { async defaultErrorHandler( err: Error, - req: Request, + _req: Request, res: Response, _next: NextFunction, ) { this.log.error("ExpressJS error", { error: err }); res.locals.errorCode = err.statusCode || 500; res.locals.error = err; - if (req.session && req.session.type == SessionType.JWT) { - res.status(res.locals.errorCode).json({ - success: false, - message: err.message, - }); - return; - } - res.status(res.locals.errorCode).json({ success: false, message: err.message,