155 lines
4.1 KiB
TypeScript
155 lines
4.1 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";
|
|
|
|
export enum SessionType {
|
|
WEB = "web",
|
|
JWT = "jwt",
|
|
}
|
|
|
|
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(1, "hour").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();
|