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;
|
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 {
|
function coerceDbUserId(id: unknown): number | null {
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
if (typeof id === 'bigint') return Number(id);
|
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 hash = await bcrypt.hash(password, BCRYPT_ROUNDS);
|
||||||
|
const clientIp = readClientIp(req);
|
||||||
const { data: row, error } = await supabase
|
const { data: row, error } = await supabase
|
||||||
.from('users')
|
.from('users')
|
||||||
.insert({
|
.insert({
|
||||||
@@ -232,6 +250,7 @@ export function registerAuthRoutes(app: Express, settings: Settings): void {
|
|||||||
password: hash,
|
password: hash,
|
||||||
cc: 0,
|
cc: 0,
|
||||||
rc: 0,
|
rc: 0,
|
||||||
|
ip_address: clientIp,
|
||||||
})
|
})
|
||||||
.select('id, username, email, cc, rc, created_at, last_logged_at')
|
.select('id, username, email, cc, rc, created_at, last_logged_at')
|
||||||
.single();
|
.single();
|
||||||
@@ -288,7 +307,8 @@ export function registerAuthRoutes(app: Express, settings: Settings): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nowIso = new Date().toISOString();
|
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 = {
|
const user: AuthUserPublic = {
|
||||||
...rowToAuthUserPublic(row as Record<string, unknown>),
|
...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 });
|
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.
|
* 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.
|
* Later: same route can require a verified store receipt when dummy mode is off.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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}
|
||||||
@@ -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
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
@@ -4,6 +4,7 @@ create table public.users (
|
|||||||
username text null,
|
username text null,
|
||||||
email text null,
|
email text null,
|
||||||
password text null,
|
password text null,
|
||||||
|
ip_address text null,
|
||||||
cc real null default '0'::real,
|
cc real null default '0'::real,
|
||||||
rc real null default '0'::real,
|
rc real null default '0'::real,
|
||||||
last_logged_at timestamp with time zone null default now(),
|
last_logged_at timestamp with time zone null default now(),
|
||||||
|
|||||||
Reference in New Issue
Block a user