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:
parent
b3c9579890
commit
896aff1b02
@ -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 (
|
||||
<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">
|
||||
<Header user={user} onSignOut={handleSignOut} />
|
||||
<main className="flex-1 flex overflow-hidden">
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -18,7 +18,7 @@ export default function SignIn({ onSuccess }: SignInProps) {
|
||||
setError("");
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await api.post<AuthResponse>("/auth/sign-in", {
|
||||
const response = await api.post<AuthResponse>("/api/v1/auth/sign-in", {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -32,9 +32,7 @@ export class UserApiControllerV1 extends DtpController {
|
||||
async getUser(req: Request, res: Response): Promise<void> {
|
||||
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 });
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
@ -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<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();
|
||||
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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user