ping reporting + service unit
This commit is contained in:
+85
-1
@@ -91,6 +91,23 @@ function readBearer(req: Request): string | null {
|
||||
return m ? m[1]!.trim() : null;
|
||||
}
|
||||
|
||||
function readClientIp(req: Request): string | null {
|
||||
const forwarded = req.headers['x-forwarded-for'];
|
||||
const headerValue = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
||||
if (typeof headerValue === 'string') {
|
||||
const first = headerValue
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.find((part) => part.length > 0);
|
||||
if (first) return first;
|
||||
}
|
||||
if (typeof req.ip === 'string' && req.ip.trim().length > 0) return req.ip.trim();
|
||||
if (typeof req.socket?.remoteAddress === 'string' && req.socket.remoteAddress.trim().length > 0) {
|
||||
return req.socket.remoteAddress.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function coerceDbUserId(id: unknown): number | null {
|
||||
if (id == null) return null;
|
||||
if (typeof id === 'bigint') return Number(id);
|
||||
@@ -224,6 +241,7 @@ export function registerAuthRoutes(app: Express, settings: Settings): void {
|
||||
}
|
||||
|
||||
const hash = await bcrypt.hash(password, BCRYPT_ROUNDS);
|
||||
const clientIp = readClientIp(req);
|
||||
const { data: row, error } = await supabase
|
||||
.from('users')
|
||||
.insert({
|
||||
@@ -232,6 +250,7 @@ export function registerAuthRoutes(app: Express, settings: Settings): void {
|
||||
password: hash,
|
||||
cc: 0,
|
||||
rc: 0,
|
||||
ip_address: clientIp,
|
||||
})
|
||||
.select('id, username, email, cc, rc, created_at, last_logged_at')
|
||||
.single();
|
||||
@@ -288,7 +307,8 @@ export function registerAuthRoutes(app: Express, settings: Settings): void {
|
||||
}
|
||||
|
||||
const nowIso = new Date().toISOString();
|
||||
await supabase.from('users').update({ last_logged_at: nowIso }).eq('id', row.id);
|
||||
const clientIp = readClientIp(req);
|
||||
await supabase.from('users').update({ last_logged_at: nowIso, ip_address: clientIp }).eq('id', row.id);
|
||||
|
||||
const user: AuthUserPublic = {
|
||||
...rowToAuthUserPublic(row as Record<string, unknown>),
|
||||
@@ -340,6 +360,70 @@ export function registerAuthRoutes(app: Express, settings: Settings): void {
|
||||
res.json({ ok: true, rc_per_usd: RC_PER_USD, packs });
|
||||
});
|
||||
|
||||
/** Client reports observed ping for a match. User id comes from login token. */
|
||||
app.post('/auth/ping-report', async (req: Request, res: Response) => {
|
||||
if (!authConfigured(settings)) {
|
||||
res.status(503).json({ ok: false, error: 'Auth is not configured on the server' });
|
||||
return;
|
||||
}
|
||||
const token = readBearer(req);
|
||||
if (!token) {
|
||||
res.status(401).json({ ok: false, error: 'Missing Authorization: Bearer <token>' });
|
||||
return;
|
||||
}
|
||||
const v = verifyAuthToken(token, settings.auth_jwt_secret!.trim());
|
||||
if (!v) {
|
||||
res.status(401).json({ ok: false, error: 'Invalid or expired token' });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = req.body as Record<string, unknown>;
|
||||
const matchId = coerceDbUserId(body?.match_id);
|
||||
const ping =
|
||||
typeof body?.ping === 'number' && Number.isFinite(body.ping)
|
||||
? Math.trunc(body.ping)
|
||||
: typeof body?.ping === 'string' && /^-?\d+$/.test(body.ping.trim())
|
||||
? parseInt(body.ping.trim(), 10)
|
||||
: null;
|
||||
if (matchId == null || matchId < 1) {
|
||||
res.status(400).json({ ok: false, error: 'match_id must be a positive integer' });
|
||||
return;
|
||||
}
|
||||
if (ping == null || ping < 0) {
|
||||
res.status(400).json({ ok: false, error: 'ping must be a non-negative integer' });
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = coerceDbUserId(v.userId);
|
||||
if (userId == null || userId < 1) {
|
||||
res.status(400).json({ ok: false, error: 'Invalid token user id' });
|
||||
return;
|
||||
}
|
||||
|
||||
const supabase = createSupabase(settings);
|
||||
const ipAddress = readClientIp(req);
|
||||
const { data: inserted, error } = await supabase
|
||||
.from('ping_reports')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
match_id: matchId,
|
||||
ping,
|
||||
ip_address: ipAddress,
|
||||
})
|
||||
.select('id, user_id, match_id, ping, ip_address, created_at')
|
||||
.single();
|
||||
if (error || !inserted) {
|
||||
if (error?.code === '23503') {
|
||||
res.status(404).json({ ok: false, error: 'Referenced user or match not found' });
|
||||
return;
|
||||
}
|
||||
res.status(500).json({ ok: false, error: error?.message ?? 'Failed to report ping' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(201).json({ ok: true, report: inserted });
|
||||
});
|
||||
|
||||
/**
|
||||
* Dummy IAP: adds RC for a named pack when `iap_dummy_mode` is true in settings.
|
||||
* Later: same route can require a verified store receipt when dummy mode is off.
|
||||
|
||||
Reference in New Issue
Block a user