Fix User Settings persona display issue

- Switch frontend sign-in to /api/v1/auth/sign-in endpoint (includes persona)
- Add updateUser() to App context for proper state management
- Fix Settings.tsx save flow to use updateUser() instead of broken localStorage merge
- Remove unused web AuthController (gadget-code/src/controllers/auth.ts)
- Fix UserApiControllerV1 to return flat user object instead of double-wrapped
- Remove SessionType enum and references (dead code)
- Add proper server sign-out call before clearing local state

Resolves issue where User Settings view didn't display persona text even though it existed in the database.
This commit is contained in:
Rob Colbert 2026-05-12 01:15:00 -04:00
parent b3c9579890
commit 896aff1b02
8 changed files with 24 additions and 177 deletions

View File

@ -46,6 +46,7 @@ export function setStoredProject(slug: string | null) {
interface AppContextType { interface AppContextType {
user: User | null; user: User | null;
updateUser: (user: User) => void;
currentProject: string | null; currentProject: string | null;
setCurrentProject: (slug: string | null) => void; setCurrentProject: (slug: string | null) => void;
onSignOut: () => void; onSignOut: () => void;
@ -97,7 +98,20 @@ export default function App() {
socketClient.connect(token); 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(TOKEN_KEY);
localStorage.removeItem(PROJECT_KEY); localStorage.removeItem(PROJECT_KEY);
setStoredUser(null); setStoredUser(null);
@ -121,7 +135,7 @@ export default function App() {
} }
return ( return (
<AppContext.Provider value={{ user, currentProject, setCurrentProject: handleSetCurrentProject, onSignOut: handleSignOut, statusMessage, setStatusMessage }}> <AppContext.Provider value={{ user, updateUser: handleUpdateUser, currentProject, setCurrentProject: handleSetCurrentProject, onSignOut: handleSignOut, statusMessage, setStatusMessage }}>
<div className="h-screen flex flex-col bg-bg-primary"> <div className="h-screen flex flex-col bg-bg-primary">
<Header user={user} onSignOut={handleSignOut} /> <Header user={user} onSignOut={handleSignOut} />
<main className="flex-1 flex overflow-hidden"> <main className="flex-1 flex overflow-hidden">

View File

@ -6,7 +6,7 @@ import { useAppContext } from "../App";
const PERSONA_MAX = 500; const PERSONA_MAX = 500;
export default function Settings() { export default function Settings() {
const { user, setStatusMessage } = useAppContext(); const { user, updateUser, setStatusMessage } = useAppContext();
const navigate = useNavigate(); const navigate = useNavigate();
const [displayName, setDisplayName] = useState(user?.displayName ?? ""); const [displayName, setDisplayName] = useState(user?.displayName ?? "");
@ -76,12 +76,8 @@ export default function Settings() {
setNewPassword(""); setNewPassword("");
setConfirmPassword(""); setConfirmPassword("");
const storedUser = localStorage.getItem("dtp_user"); // Update React context + localStorage in one place
if (storedUser) { updateUser(updatedUser);
const parsed = JSON.parse(storedUser);
Object.assign(parsed, updatedUser);
localStorage.setItem("dtp_user", JSON.stringify(parsed));
}
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to save settings."); setError(err instanceof Error ? err.message : "Failed to save settings.");
} finally { } finally {

View File

@ -18,7 +18,7 @@ export default function SignIn({ onSuccess }: SignInProps) {
setError(""); setError("");
setLoading(true); setLoading(true);
try { try {
const response = await api.post<AuthResponse>("/auth/sign-in", { const response = await api.post<AuthResponse>("/api/v1/auth/sign-in", {
email, email,
password, password,
}); });

View File

@ -8,7 +8,7 @@ import { Request, Response } from "express";
import { DtpController } from "../../../lib/controller.js"; import { DtpController } from "../../../lib/controller.js";
import UserService from "../../../services/user.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 { export class AuthApiControllerV1 extends DtpController {
get name(): string { get name(): string {
@ -53,7 +53,6 @@ export class AuthApiControllerV1 extends DtpController {
}); });
const token = await SessionService.createJsonWebToken(user); const token = await SessionService.createJsonWebToken(user);
req.session.token = token; req.session.token = token;
req.session.type = SessionType.JWT;
req.session.save((err: Error): void => { req.session.save((err: Error): void => {
if (err) { if (err) {

View File

@ -32,9 +32,7 @@ export class UserApiControllerV1 extends DtpController {
async getUser(req: Request, res: Response): Promise<void> { async getUser(req: Request, res: Response): Promise<void> {
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: { data: req.user,
user: req.user,
},
}); });
} }
@ -47,9 +45,7 @@ export class UserApiControllerV1 extends DtpController {
const updatedUser = await UserService.updateForUser(req.user!, req.body); const updatedUser = await UserService.updateForUser(req.user!, req.body);
res.status(200).json({ res.status(200).json({
success: true, success: true,
data: { data: updatedUser,
user: updatedUser,
},
}); });
} catch (error) { } catch (error) {
this.log.error("failed to update user settings", { error }); this.log.error("failed to update user settings", { error });

View File

@ -1,138 +0,0 @@
// src/controllers/auth.ts
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// 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<void> {
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<void> {
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<void> {
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<void> {
res.status(200).json({
success: true,
message: "Welcome to DTP Web Application",
});
}
async getSignInForm(_req: Request, res: Response): Promise<void> {
res.status(200).json({
success: true,
form: "sign-in",
});
}
async getSignOut(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
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;

View File

@ -18,11 +18,6 @@ import UserService from "../services/user.js";
import { DtpService } from "../lib/service.js"; import { DtpService } from "../lib/service.js";
import { PopulateOptions } from "mongoose"; import { PopulateOptions } from "mongoose";
export enum SessionType {
WEB = "web",
JWT = "jwt",
}
interface UserWebToken { interface UserWebToken {
_id: string; _id: string;
email: string; email: string;

View File

@ -36,7 +36,6 @@ import { GadgetComponent, GadgetLog } from "@gadget/api";
import { User } from "./models/user.js"; import { User } from "./models/user.js";
import { ApiController } from "./controllers/api.js"; import { ApiController } from "./controllers/api.js";
import { AuthController } from "./controllers/auth.js";
import { HomeController } from "./controllers/home.js"; import { HomeController } from "./controllers/home.js";
import { UserController } from "./controllers/user.js"; import { UserController } from "./controllers/user.js";
@ -47,7 +46,6 @@ import {
startServices, startServices,
stopServices, stopServices,
} from "./services/index.js"; } from "./services/index.js";
import { SessionType } from "./services/session.js";
class DtpWebAppServer implements GadgetComponent { class DtpWebAppServer implements GadgetComponent {
private log: GadgetLog; private log: GadgetLog;
@ -211,11 +209,6 @@ class DtpWebAppServer implements GadgetComponent {
async createAppRouter(): Promise<express.Router> { async createAppRouter(): Promise<express.Router> {
const router: express.Router = express.Router(); 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(); const apiController = new ApiController();
await apiController.start(); await apiController.start();
this.log.info("mounting ApiController", { this.log.info("mounting ApiController", {
@ -298,21 +291,13 @@ class DtpWebAppServer implements GadgetComponent {
async defaultErrorHandler( async defaultErrorHandler(
err: Error, err: Error,
req: Request, _req: Request,
res: Response, res: Response,
_next: NextFunction, _next: NextFunction,
) { ) {
this.log.error("ExpressJS error", { error: err }); this.log.error("ExpressJS error", { error: err });
res.locals.errorCode = err.statusCode || 500; res.locals.errorCode = err.statusCode || 500;
res.locals.error = err; 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({ res.status(res.locals.errorCode).json({
success: false, success: false,
message: err.message, message: err.message,