Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions pages/api/auth/checkOwner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ export async function handler(req: AuthenticatedRequest, res: NextApiResponse<Da
if (!user) {
return res.status(404).json({ success: false, error: "User not found" })
}



return res.status(200).json({ success: true, isOwner: user.isOwner || false })
} catch (error) {
console.error("Error checking workspace ownership:", error)
Expand Down
104 changes: 100 additions & 4 deletions utils/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,109 @@ import * as crypto from 'crypto'
import prisma from '@/utils/database'
import { UAParser } from 'ua-parser-js'
import axios from 'axios'
import * as net from 'net'

interface IpapiRes {
country_name: string,
region: string
}

const SESSION_SECRET = process.env.SESSION_SECRET!
interface IpapiRes {
country_name: string,
region: string
}

interface GeoResult {
country: string | null
region: string | null
}

const SESSION_SECRET = process.env.SESSION_SECRET!;

const geoCache = new Map<string, { data: GeoResult; expiresAt: number }>()
const GEO_CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24h

function normalizeIp(rawIp: string): string {
const trimmed = rawIp.trim()
const mappedV4Prefix = '::ffff:'
if (trimmed.toLowerCase().startsWith(mappedV4Prefix)) {
return trimmed.slice(mappedV4Prefix.length)
}
return trimmed
}

function isPrivateOrLocalIp(ip: string): boolean {
if (ip === '::1' || ip === '127.0.0.1') return true

if (net.isIP(ip) === 4) {
return (
ip.startsWith('10.') ||
ip.startsWith('192.168.') ||
/^172\.(1[6-9]|2\d|3[0-1])\./.test(ip) ||
ip.startsWith('127.') ||
ip.startsWith('169.254.')
)
}

if (net.isIP(ip) === 6) {
const lower = ip.toLowerCase()
return (
lower === '::1' ||
lower.startsWith('fc') ||
lower.startsWith('fd') ||
lower.startsWith('fe80:')
)
}

return true
}

function sanitizePublicIp(ipAddress?: string): string | null {
if (!ipAddress) return null
const normalized = normalizeIp(ipAddress)
if (!normalized || net.isIP(normalized) === 0) return null
if (isPrivateOrLocalIp(normalized)) return null
return normalized
}

async function lookupGeo(ipAddress?: string): Promise<GeoResult> {
const empty: GeoResult = { country: null, region: null }

const safeIp = sanitizePublicIp(ipAddress)
if (!safeIp) {
return empty
}

const cached = geoCache.get(safeIp)
if (cached && cached.expiresAt > Date.now()) {
return cached.data
}

try {
const geoUrl = new URL(`/${encodeURIComponent(safeIp)}/json/`, 'https://ipapi.co')
const res = await axios.get<IpapiRes>(
geoUrl.toString(),
{ timeout: 2000 }
)
Comment thread
Copilot marked this conversation as resolved.

if ((res.data as any).error) {
console.warn('ipapi.co returned an error payload:', (res.data as any).reason)
return empty
}

const geo: GeoResult = {
country: res.data.country_name ?? null,
region: res.data.region ?? null,
}

geoCache.set(safeIp, { data: geo, expiresAt: Date.now() + GEO_CACHE_TTL_MS })

return geo
} catch (err) {
console.error('ipapi.co lookup failed, continuing without geo data:', err)
return empty
}
}

function generateToken(): string {
const token = crypto.randomBytes(32).toString('hex')
Expand Down Expand Up @@ -118,7 +214,8 @@ async function createSession(

const info = await axios.get<IpapiRes>('https://ipapi.co/json');

const { browser, os, device } = parseUA(userAgent)
const { browser, os, device } = parseUA(userAgent);
const geo = await lookupGeo(ipAddress);

const session = await prisma.authSession.create({
data: {
Expand Down Expand Up @@ -259,7 +356,6 @@ async function deleteOtherSessions(
userId: bigint,
sid: string
) {

return prisma.authSession.deleteMany({
where: {
userId,
Expand Down Expand Up @@ -329,5 +425,5 @@ export {
deleteOtherSessions,
deleteAllUserSessions,
listActiveSessions,
purgeExpiredSessions,
purgeExpiredSessions
}
Loading