gadget/gadget-code/src/services/session.ts
Rob Colbert 896aff1b02 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.
2026-05-12 01:15:00 -04:00

150 lines
4.0 KiB
TypeScript

// src/services/session.ts
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// All Rights Reserved
import env from "../config/env.js";
import { Request } from "express";
import jwt from "jsonwebtoken";
import dayjs from "dayjs";
import geoip from "geoip-lite";
import { GadgetId, IUser } from "@gadget/api";
import { IWebToken, WebToken } from "../models/web-token.js";
import { WebVisit } from "../models/web-visit.js";
import UserService from "../services/user.js";
import { DtpService } from "../lib/service.js";
import { PopulateOptions } from "mongoose";
interface UserWebToken {
_id: string;
email: string;
displayName: string;
webToken: IWebToken | GadgetId;
}
class SessionService extends DtpService {
private populateWebToken: PopulateOptions[] = [
{
path: "user",
select: "-passwordSalt -password",
},
];
get name(): string {
return "SessionService";
}
get slug(): string {
return "session";
}
async start(): Promise<void> {}
async stop(): Promise<void> {}
async createJsonWebToken(user: IUser): Promise<string> {
const NOW = new Date();
const webToken = new WebToken();
webToken.created = NOW;
webToken.expires = dayjs(NOW).add(7, "day").toDate();
webToken.user = user._id;
const payload: UserWebToken = {
_id: user._id,
email: user.email,
displayName: user.displayName,
webToken: webToken._id,
};
const token = jwt.sign(payload, env.auth.jwtSecret, {
expiresIn: "24h",
});
webToken.token = token;
await webToken.save();
return token;
}
async verifyJsonWebToken(token: string): Promise<IUser> {
try {
const NOW = new Date();
const payload = jwt.verify(token, env.auth.jwtSecret) as UserWebToken;
const userId = payload._id;
const webToken = await WebToken.findOne({
_id: payload.webToken,
}).populate(this.populateWebToken);
if (!webToken) {
const error = new Error("Invalid JSON Web Token");
error.name = "InvalidToken";
error.statusCode = 401;
throw error;
}
if (webToken.expires < NOW) {
await WebToken.deleteOne({ _id: webToken._id });
const error = new Error("JSON Web Token has expired");
error.name = "ExpiredToken";
error.statusCode = 401;
throw error;
}
if (userId !== (webToken.user as IUser)._id) {
const error = new Error("JSON Web Token ownership mismatch");
error.name = "TokenOwnershipMismatch";
error.statusCode = 401;
throw error;
}
const user = await UserService.getById(userId);
return user;
} catch (cause) {
// this.log.error("failed to verify JSON Web Token", {
// token,
// error: cause,
// });
const error = new Error("Invalid JSON Web Token", { cause });
error.name = "TokenVerifyError";
error.statusCode = 401;
throw error;
}
}
async revokeJsonWebToken(token: string): Promise<void> {
const payload = jwt.verify(token, env.auth.jwtSecret) as UserWebToken;
this.log.info("revoking JSON Web Token", { tokenId: payload.webToken });
await WebToken.deleteOne({ _id: payload.webToken });
}
async recordWebVisit(req: Request): Promise<void> {
const NOW = new Date();
const visit = new WebVisit();
visit.created = NOW;
visit.url = req.url;
if (req.user) {
visit.user = req.user._id;
}
visit.userAgent = req.headers["user-agent"];
visit.referrer = req.headers["referer"];
visit.ipAddress = req.ip;
if (req.ip) {
const ipLookup = geoip.lookup(req.ip);
if (ipLookup) {
visit.country = ipLookup.country;
visit.region = ipLookup.region;
visit.eu = ipLookup.eu === "1";
visit.timezone = ipLookup.timezone;
visit.city = ipLookup.city;
visit.latitude = ipLookup.ll[0];
visit.longitude = ipLookup.ll[1];
visit.metroCode = ipLookup.metro;
}
}
await visit.save();
}
}
export default new SessionService();