From b70233395f2ce0d036d76a609338227a0e02ca0b Mon Sep 17 00:00:00 2001 From: React User Date: Tue, 2 Jun 2026 09:14:46 +0000 Subject: [PATCH] ping reporting + service unit --- auth_bridge.ts | 86 ++++++++++++++++++++++++++- games/soccar/soccar_Data/Log.txt | 4 +- games/soccar/soccar_Data/Logs/147.txt | 24 ++++++++ games/soccar/soccar_Data/Logs/148.txt | 26 ++++++++ kickkingsapi.service | 21 +++++++ schemas/alter_users_add_email.sql | 9 --- schemas/alter_users_rc_bigint.sql | 8 --- schemas/create_ping_reports.sql | 14 +++++ schemas/table_ping_reports.md | 11 ++++ schemas/table_users.md | 1 + 10 files changed, 184 insertions(+), 20 deletions(-) create mode 100644 games/soccar/soccar_Data/Logs/147.txt create mode 100644 games/soccar/soccar_Data/Logs/148.txt create mode 100644 kickkingsapi.service delete mode 100644 schemas/alter_users_add_email.sql delete mode 100644 schemas/alter_users_rc_bigint.sql create mode 100644 schemas/create_ping_reports.sql create mode 100644 schemas/table_ping_reports.md diff --git a/auth_bridge.ts b/auth_bridge.ts index 8dbfa74..ed67e5a 100644 --- a/auth_bridge.ts +++ b/auth_bridge.ts @@ -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), @@ -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 ' }); + 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; + 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. diff --git a/games/soccar/soccar_Data/Log.txt b/games/soccar/soccar_Data/Log.txt index b6305ea..698937a 100644 --- a/games/soccar/soccar_Data/Log.txt +++ b/games/soccar/soccar_Data/Log.txt @@ -1,3 +1,3 @@ -Logger initiated at 05/31/2026 23:39:08 +Logger initiated at 06/02/2026 08:53:05 -[05/31/2026 23:39:08] Configured dedicated match reporting for match id 146 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kickkingsapi.playpoolstudios.com +[06/02/2026 08:53:05] Configured dedicated match reporting for match id 148 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kickkingsapi.playpoolstudios.com diff --git a/games/soccar/soccar_Data/Logs/147.txt b/games/soccar/soccar_Data/Logs/147.txt new file mode 100644 index 0000000..1f3e146 --- /dev/null +++ b/games/soccar/soccar_Data/Logs/147.txt @@ -0,0 +1,24 @@ +[06/02/2026 08:36:50] Starting server at port 26393 +[06/02/2026 08:36:50] Mirror server exception logging enabled +[06/02/2026 08:36:50] Failed to fetch red and blue names, isServer? +[06/02/2026 08:36:50] Starting auto close watchdog +[06/02/2026 08:36:50] Active player connections: 0 +[06/02/2026 08:36:54] Active player connections: 1 +[06/02/2026 08:36:54] Client 15 set team to Blue +[06/02/2026 08:36:55] Dedicated match PATCH success: 200 {"ok":true,"id":147} +[06/02/2026 08:36:55] Active player connections: 2 +[06/02/2026 08:36:55] Game started On Server +[06/02/2026 08:36:56] Client 16 set team to Red +[06/02/2026 08:36:56] Dedicated match PATCH success: 200 {"ok":true,"id":147} +[06/02/2026 08:37:34] Mirror server: client disconnected (connId=1441821541) +[06/02/2026 08:37:34] Active player connections: 1 +[06/02/2026 08:37:36] Mirror server: client disconnected (connId=1441799581) +[06/02/2026 08:37:36] Active player connections: 0 +[06/02/2026 08:37:41] Server will be closed due to no players in, 60 +[06/02/2026 08:37:51] Server will be closed due to no players in, 50 +[06/02/2026 08:38:01] Server will be closed due to no players in, 40 +[06/02/2026 08:38:11] Server will be closed due to no players in, 30 +[06/02/2026 08:38:21] Server will be closed due to no players in, 20 +[06/02/2026 08:38:31] Server will be closed due to no players in, 10 +[06/02/2026 08:38:41] All players left, exiting +[06/02/2026 08:38:42] Dedicated match PATCH success: 200 {"ok":true,"id":147} diff --git a/games/soccar/soccar_Data/Logs/148.txt b/games/soccar/soccar_Data/Logs/148.txt new file mode 100644 index 0000000..c4e3a84 --- /dev/null +++ b/games/soccar/soccar_Data/Logs/148.txt @@ -0,0 +1,26 @@ +[06/02/2026 08:53:05] Starting server at port 26544 +[06/02/2026 08:53:05] Mirror server exception logging enabled +[06/02/2026 08:53:05] Failed to fetch red and blue names, isServer? +[06/02/2026 08:53:05] Starting auto close watchdog +[06/02/2026 08:53:06] Active player connections: 0 +[06/02/2026 08:53:08] Active player connections: 1 +[06/02/2026 08:53:08] Client 15 set team to Blue +[06/02/2026 08:53:09] Active player connections: 2 +[06/02/2026 08:53:09] Game started On Server +[06/02/2026 08:53:09] Dedicated match PATCH success: 200 {"ok":true,"id":148} +[06/02/2026 08:53:09] Client 16 set team to Red +[06/02/2026 08:53:10] Dedicated match PATCH success: 200 {"ok":true,"id":148} +[06/02/2026 08:53:14] force = 70 * 0.9955804 = 69.69063 +[06/02/2026 08:53:14] launching puck at (4.62, -1.91) with (-322.08, 132.97) force +[06/02/2026 09:02:40] Mirror server: client disconnected (connId=1441823526) +[06/02/2026 09:02:40] Active player connections: 1 +[06/02/2026 09:02:44] Mirror server: client disconnected (connId=1441823525) +[06/02/2026 09:02:44] Active player connections: 0 +[06/02/2026 09:02:49] Server will be closed due to no players in, 60 +[06/02/2026 09:02:59] Server will be closed due to no players in, 50 +[06/02/2026 09:03:09] Server will be closed due to no players in, 40 +[06/02/2026 09:03:19] Server will be closed due to no players in, 30 +[06/02/2026 09:03:29] Server will be closed due to no players in, 20 +[06/02/2026 09:03:39] Server will be closed due to no players in, 10 +[06/02/2026 09:03:49] All players left, exiting +[06/02/2026 09:03:50] Dedicated match PATCH success: 200 {"ok":true,"id":148} diff --git a/kickkingsapi.service b/kickkingsapi.service new file mode 100644 index 0000000..28317d5 --- /dev/null +++ b/kickkingsapi.service @@ -0,0 +1,21 @@ +[Unit] +Description=KickKings backend API +After=network.target +Wants=network-online.target + +[Service] +Type=simple +User=warlock +Group=warlock +WorkingDirectory=/home/react/KickKings +Environment=NODE_ENV=production +Environment=PATH=/home/warlock/.nvm/versions/node/v24.12.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +ExecStart=/home/warlock/.nvm/versions/node/v24.12.0/bin/node /home/react/KickKings/dist/app.js +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=kickkingsapi + +[Install] +WantedBy=multi-user.target diff --git a/schemas/alter_users_add_email.sql b/schemas/alter_users_add_email.sql deleted file mode 100644 index efa46be..0000000 --- a/schemas/alter_users_add_email.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Registration email. Run in Supabase Dashboard → SQL Editor. --- Application stores normalized (trimmed, lower-case) addresses. - -alter table public.users - add column if not exists email text null; - -create unique index if not exists users_email_unique - on public.users (email) - where email is not null; diff --git a/schemas/alter_users_rc_bigint.sql b/schemas/alter_users_rc_bigint.sql deleted file mode 100644 index 49572d2..0000000 --- a/schemas/alter_users_rc_bigint.sql +++ /dev/null @@ -1,8 +0,0 @@ --- `users.rc` as type `real` is IEEE float32: above ~1e7, consecutive values are spaced --- by more than 1 (and by hundreds at ~4e9), so debits like -20 do not change the stored value. --- Run this in Supabase SQL before relying on `purchase_rc` or large RC balances. --- --- After this, re-apply public.purchase_rc from schemas/rpc_purchase_rc.sql (no ::real casts). - -alter table public.users - alter column rc type bigint using round(coalesce(rc, 0))::bigint; diff --git a/schemas/create_ping_reports.sql b/schemas/create_ping_reports.sql new file mode 100644 index 0000000..ca3f732 --- /dev/null +++ b/schemas/create_ping_reports.sql @@ -0,0 +1,14 @@ +-- Per-match ping telemetry reported by game clients. +create table if not exists public.ping_reports ( + id bigint generated by default as identity not null, + created_at timestamp with time zone not null default now(), + user_id bigint not null references public.users(id), + match_id bigint not null references public.matches(id), + ip_address text null, + ping integer not null, + constraint ping_reports_pkey primary key (id) +); + +create index if not exists ping_reports_user_id_idx on public.ping_reports(user_id); +create index if not exists ping_reports_match_id_idx on public.ping_reports(match_id); +create index if not exists ping_reports_created_at_idx on public.ping_reports(created_at); diff --git a/schemas/table_ping_reports.md b/schemas/table_ping_reports.md new file mode 100644 index 0000000..e74547b --- /dev/null +++ b/schemas/table_ping_reports.md @@ -0,0 +1,11 @@ +create table public.ping_reports ( + id bigint generated by default as identity not null, + created_at timestamp with time zone not null default now(), + user_id bigint not null, + match_id bigint not null, + ip_address text null, + ping integer not null, + constraint ping_reports_pkey primary key (id), + constraint ping_reports_user_id_fkey foreign key (user_id) references users (id), + constraint ping_reports_match_id_fkey foreign key (match_id) references matches (id) +) TABLESPACE pg_default; diff --git a/schemas/table_users.md b/schemas/table_users.md index d1c1f3f..addce4f 100644 --- a/schemas/table_users.md +++ b/schemas/table_users.md @@ -4,6 +4,7 @@ create table public.users ( username text null, email text null, password text null, + ip_address text null, cc real null default '0'::real, rc real null default '0'::real, last_logged_at timestamp with time zone null default now(),