From 5ee2f91557ababb8886c1f8a9cf10ad616f1034a Mon Sep 17 00:00:00 2001 From: Princi Vershwal Date: Thu, 10 Oct 2024 16:21:12 +0100 Subject: [PATCH] Added support for fetching device details when creating session --- ghost/session-service/lib/session-service.js | 79 +++++++++++++++++++- ghost/session-service/package.json | 4 +- yarn.lock | 5 ++ 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/ghost/session-service/lib/session-service.js b/ghost/session-service/lib/session-service.js index 71ac6d369f..1b7057c8f5 100644 --- a/ghost/session-service/lib/session-service.js +++ b/ghost/session-service/lib/session-service.js @@ -2,6 +2,10 @@ const { BadRequestError } = require('@tryghost/errors'); const emailTemplate = require('../lib/emails/signin'); +const UAParser = require('ua-parser-js'); +const got = require('got'); +const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; +const IPV6_REGEX = /^(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}$/i; const {totp} = require('otplib'); totp.options = { @@ -143,6 +147,75 @@ module.exports = function createSessionService({ return isValid; } + const formatTime = new Intl.DateTimeFormat('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZone: 'UTC', + timeZoneName: 'short' + }).format; + + /** + * Get a readable location string from an IP address. + * @param {string} ip - The IP address to look up. + * @returns {Promise} - A readable location string or 'Unknown Location'. + */ + async function getGeolocationFromIP(ip) { + ip = '212.19.89.120'; + if (!ip || (!IPV4_REGEX.test(ip) && !IPV6_REGEX.test(ip))) { + return; + } + + const gotOpts = { + timeout: 500 + }; + + if (process.env.NODE_ENV?.startsWith('test')) { + gotOpts.retry = 0; + } + + const geojsUrl = `https://get.geojs.io/v1/ip/geo/${encodeURIComponent(ip)}.json`; + const response = await got(geojsUrl, gotOpts).json(); + + const { + city = '', + region = '', + country = '' + } = response || {}; + + const locationParts = []; + + if (city) { + locationParts.push(city); + } + if (region) { + locationParts.push(region); + } + if (country) { + locationParts.push(country); + } + + return locationParts.join(', ').trim() || 'Unknown Location'; + } + + async function getDeviceDetails(userAgent, ip) { + const parser = new UAParser(); + parser.setUA(userAgent); + const result = parser.getResult(); + const deviceParts = [ + result.browser?.name || '', + result.os?.name || '' + ].filter(Boolean); + + return { + device: deviceParts.join(', '), + location: await getGeolocationFromIP(ip), + time: formatTime(new Date()) + }; + } + /** * sendAuthCodeToUser * @@ -151,9 +224,8 @@ module.exports = function createSessionService({ * @returns {Promise} */ async function sendAuthCodeToUser(req, res) { - const token = await generateAuthCodeForUser(req, res); - const session = await getSession(req, res); + const token = await generateAuthCodeForUser(req, res); const user = await findUserById({id: session.user_id}); if (!user) { @@ -172,7 +244,8 @@ module.exports = function createSessionService({ email: recipient, siteDomain: siteDomain, siteUrl: siteUrl, - token + token: token, + deviceDetails: await getDeviceDetails(session.user_agent, session.ip) }); await mailer.send({ diff --git a/ghost/session-service/package.json b/ghost/session-service/package.json index 60c0d81bcc..87eb7023e5 100644 --- a/ghost/session-service/package.json +++ b/ghost/session-service/package.json @@ -26,6 +26,8 @@ }, "dependencies": { "@tryghost/errors": "1.3.5", - "otplib" : "12.0.1" + "otplib" : "12.0.1", + "ua-parser-js": "1.0.39", + "got": "11.8.6" } } diff --git a/yarn.lock b/yarn.lock index 3280a56c49..b982cd0c70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30491,6 +30491,11 @@ typescript@5.6.2, typescript@^5.0.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== +ua-parser-js@1.0.39: + version "1.0.39" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.39.tgz#bfc07f361549bf249bd8f4589a4cccec18fd2018" + integrity sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"