refactor(geoip): reconcile geoip system (#31)

This commit is contained in:
hampus-fluxer
2026-01-05 23:19:05 +01:00
committed by GitHub
parent 5d047b2856
commit 2e007b5076
86 changed files with 982 additions and 2648 deletions

View File

@@ -331,8 +331,8 @@ export class AuthLoginService {
if (!isIpAuthorized) {
const ticket = createIpAuthorizationTicket(await this.generateSecureToken());
const authToken = createIpAuthorizationToken(await this.generateSecureToken());
const geoipResult = await IpUtils.getCountryCodeDetailed(clientIp);
const clientLocation = IpUtils.formatGeoipLocation(geoipResult);
const geoipResult = await IpUtils.lookupGeoip(clientIp);
const clientLocation = IpUtils.formatGeoipLocation(geoipResult) ?? IpUtils.UNKNOWN_LOCATION;
const userAgent = request.headers.get('user-agent') || '';
const platform = request.headers.get('x-fluxer-platform');

View File

@@ -83,8 +83,6 @@ const MINIMUM_AGE_BY_COUNTRY: Record<string, number> = {
const DEFAULT_MINIMUM_AGE = 13;
const USER_AGENT_TRUNCATE_LENGTH = 512;
type CountryResultDetailed = Awaited<ReturnType<typeof IpUtils.getCountryCodeDetailed>>;
interface RegistrationMetadataContext {
metadata: Map<string, string>;
clientIp: string;
@@ -120,11 +118,6 @@ function determineAgeGroup(age: number | null): string {
return '65+';
}
function sanitizeEmail(email: string | null | undefined): {raw: string | null; key: string | null} {
const key = email ? email.toLowerCase() : null;
return {raw: email ?? null, key};
}
function isIpv6(ip: string): boolean {
return ip.includes(':');
}
@@ -189,8 +182,8 @@ export class AuthRegistrationService {
const metrics = getMetricsService();
const clientIp = IpUtils.requireClientIp(request);
const countryCode = await IpUtils.getCountryCodeFromReq(request);
const countryResultDetailed = await IpUtils.getCountryCodeDetailed(clientIp);
const geoipResult = await IpUtils.lookupGeoip(clientIp);
const countryCode = geoipResult.countryCode;
const minAge = (countryCode && MINIMUM_AGE_BY_COUNTRY[countryCode]) || DEFAULT_MINIMUM_AGE;
if (!this.validateAge({dateOfBirth: data.date_of_birth, minAge})) {
@@ -204,7 +197,8 @@ export class AuthRegistrationService {
throw InputValidationError.create('password', 'Password is too common');
}
const {raw: rawEmail, key: emailKey} = sanitizeEmail(data.email);
const rawEmail = data.email ?? null;
const emailKey = rawEmail ? rawEmail.toLowerCase() : null;
const enforceRateLimits = !Config.dev.relaxRegistrationRateLimits;
await this.enforceRegistrationRateLimits({enforceRateLimits, clientIp, emailKey});
@@ -300,7 +294,7 @@ export class AuthRegistrationService {
name: 'user.registration',
dimensions: {
country: countryCode ?? 'unknown',
state: countryResultDetailed.region ?? 'unknown',
state: geoipResult.region ?? 'unknown',
ip_version: isIpv6(clientIp) ? 'v6' : 'v4',
},
});
@@ -310,7 +304,7 @@ export class AuthRegistrationService {
name: 'user.age',
dimensions: {
country: countryCode ?? 'unknown',
state: countryResultDetailed.region ?? 'unknown',
state: geoipResult.region ?? 'unknown',
age: age !== null ? age.toString() : 'unknown',
age_group: determineAgeGroup(age),
},
@@ -333,7 +327,7 @@ export class AuthRegistrationService {
user,
clientIp,
request,
countryResultDetailed,
geoipResult,
});
if (isPendingVerification)
@@ -542,9 +536,9 @@ export class AuthRegistrationService {
user: User;
clientIp: string;
request: Request;
countryResultDetailed: CountryResultDetailed;
geoipResult: IpUtils.GeoipResult;
}): Promise<RegistrationMetadataContext> {
const {user, clientIp, request, countryResultDetailed} = params;
const {user, clientIp, request, geoipResult} = params;
const userAgentHeader = (request.headers.get('user-agent') ?? '').trim();
const fluxerTag = `${user.username}#${user.discriminator.toString().padStart(4, '0')}`;
@@ -559,9 +553,9 @@ export class AuthRegistrationService {
? this.parseUserAgentSafe(userAgentHeader)
: {osInfo: 'Unknown', browserInfo: 'Unknown', deviceInfo: 'Desktop/Unknown'};
const normalizedIp = countryResultDetailed.normalizedIp ?? clientIp;
const geoipReason = countryResultDetailed.reason ?? 'none';
const locationLabel = IpUtils.formatGeoipLocation(countryResultDetailed);
const normalizedIp = geoipResult.normalizedIp ?? clientIp;
const locationLabel = IpUtils.formatGeoipLocation(geoipResult) ?? IpUtils.UNKNOWN_LOCATION;
const safeCountryCode = geoipResult.countryCode ?? 'unknown';
const ipAddressReverse = await IpUtils.getIpAddressReverse(normalizedIp, this.cacheService);
const metadataEntries: Array<[string, string]> = [
@@ -570,27 +564,26 @@ export class AuthRegistrationService {
['email', emailDisplay],
['ip_address', clientIp],
['normalized_ip', normalizedIp],
['country_code', countryResultDetailed.countryCode],
['country_code', safeCountryCode],
['location', locationLabel],
['geoip_reason', geoipReason],
['os', uaInfo.osInfo],
['browser', uaInfo.browserInfo],
['device', uaInfo.deviceInfo],
['user_agent', truncatedUserAgent],
];
if (countryResultDetailed.city) metadataEntries.push(['city', countryResultDetailed.city]);
if (countryResultDetailed.region) metadataEntries.push(['region', countryResultDetailed.region]);
if (countryResultDetailed.countryName) metadataEntries.push(['country_name', countryResultDetailed.countryName]);
if (geoipResult.city) metadataEntries.push(['city', geoipResult.city]);
if (geoipResult.region) metadataEntries.push(['region', geoipResult.region]);
if (geoipResult.countryName) metadataEntries.push(['country_name', geoipResult.countryName]);
if (ipAddressReverse) metadataEntries.push(['ip_address_reverse', ipAddressReverse]);
return {
metadata: new Map(metadataEntries),
clientIp,
countryCode: countryResultDetailed.countryCode,
countryCode: safeCountryCode,
location: locationLabel,
city: countryResultDetailed.city,
region: countryResultDetailed.region,
city: geoipResult.city,
region: geoipResult.region,
osInfo: uaInfo.osInfo,
browserInfo: uaInfo.browserInfo,
deviceInfo: uaInfo.deviceInfo,

View File

@@ -17,12 +17,10 @@
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import Bowser from 'bowser';
import {type AuthSessionResponse, mapAuthSessionsToResponse} from '~/auth/AuthModel';
import type {UserID} from '~/BrandedTypes';
import {AccessDeniedError} from '~/Errors';
import type {IGatewayService} from '~/infrastructure/IGatewayService';
import {Logger} from '~/Logger';
import type {AuthSession, User} from '~/Models';
import type {IUserRepository} from '~/user/IUserRepository';
import * as IpUtils from '~/utils/IpUtils';
@@ -41,45 +39,6 @@ interface UpdateUserActivityParams {
userId: UserID;
clientIp: string;
}
function formatNameVersion(name?: string | null, version?: string | null): string {
if (!name) return 'Unknown';
if (!version) return name;
return `${name} ${version}`;
}
function nullIfUnknown(value: string | null | undefined): string | null {
if (!value) return null;
return value === IpUtils.UNKNOWN_LOCATION ? null : value;
}
function parseUserAgent(userAgentRaw: string): {clientOs: string; detectedPlatform: string} {
const ua = userAgentRaw.trim();
if (!ua) return {clientOs: 'Unknown', detectedPlatform: 'Unknown'};
try {
const parser = Bowser.getParser(ua);
const osName = parser.getOSName() || 'Unknown';
const osVersion = parser.getOSVersion() || null;
const browserName = parser.getBrowserName() || 'Unknown';
const browserVersion = parser.getBrowserVersion() || null;
return {
clientOs: formatNameVersion(osName, osVersion),
detectedPlatform: formatNameVersion(browserName, browserVersion),
};
} catch (error) {
Logger.warn({error}, 'Failed to parse user agent');
return {clientOs: 'Unknown', detectedPlatform: 'Unknown'};
}
}
function resolveClientPlatform(platformHeader: string | null, detectedPlatform: string): string {
if (!platformHeader) return detectedPlatform;
if (platformHeader === 'desktop') return 'Fluxer Desktop';
return detectedPlatform;
}
export class AuthSessionService {
constructor(
private repository: IUserRepository,
@@ -97,11 +56,7 @@ export class AuthSessionService {
const platformHeader = request.headers.get('x-fluxer-platform')?.trim().toLowerCase() ?? null;
const uaRaw = request.headers.get('user-agent') ?? '';
const uaInfo = parseUserAgent(uaRaw);
const geoip = await IpUtils.getCountryCodeDetailed(ip);
const locationLabel = nullIfUnknown(IpUtils.formatGeoipLocation(geoip));
const countryLabel = nullIfUnknown(geoip.countryName ?? geoip.countryCode ?? null);
const isDesktopClient = platformHeader === 'desktop';
const authSession = await this.repository.createAuthSession({
user_id: user.id,
@@ -109,10 +64,8 @@ export class AuthSessionService {
created_at: now,
approx_last_used_at: now,
client_ip: ip,
client_os: uaInfo.clientOs,
client_platform: resolveClientPlatform(platformHeader, uaInfo.detectedPlatform),
client_country: countryLabel,
client_location: locationLabel,
client_user_agent: uaRaw || null,
client_is_desktop: isDesktopClient,
version: 1,
});
@@ -125,7 +78,7 @@ export class AuthSessionService {
async getAuthSessions(userId: UserID): Promise<Array<AuthSessionResponse>> {
const authSessions = await this.repository.listAuthSessions(userId);
return mapAuthSessionsToResponse({authSessions});
return await mapAuthSessionsToResponse({authSessions});
}
async updateAuthSessionLastUsed(tokenHash: Uint8Array): Promise<void> {