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 {
|
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">
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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 });
|
||||||
|
|||||||
@ -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 { 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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user