// src/services/session.ts // Copyright (C) 2026 Robert Colbert // 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 {} async stop(): Promise {} async createJsonWebToken(user: IUser): Promise { 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 { 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 { 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 { 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();