mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 23:21:30 -07:00
add duid endpoint
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
|
|
||||||
|
import { setDeviceId } from '@repo/domain'
|
||||||
|
|
||||||
|
import { authedId, unauthorized } from '../http'
|
||||||
|
|
||||||
import type { App } from '../context'
|
import type { App } from '../context'
|
||||||
|
|
||||||
// ---- Player reporting ------------------------------------------------------
|
// ---- Player reporting ------------------------------------------------------
|
||||||
@@ -23,3 +27,21 @@ export const moderationRoutes = new Hono<App>({ strict: false })
|
|||||||
)
|
)
|
||||||
.get('/api/PlayerReporting/v1/voteToKickReasons', (c) => c.json([])) // TODO: hydrate from JSON/vtkreasons.json
|
.get('/api/PlayerReporting/v1/voteToKickReasons', (c) => c.json([])) // TODO: hydrate from JSON/vtkreasons.json
|
||||||
.post('/api/PlayerReporting/v1/hile', (c) => c.json(false))
|
.post('/api/PlayerReporting/v1/hile', (c) => c.json(false))
|
||||||
|
|
||||||
|
// The client reporting its device id (form-encoded `oldDeviceId`, `newDeviceId`,
|
||||||
|
// `platform`), rotating from the id it thinks we hold to the current one. We don't
|
||||||
|
// reconcile the two: the client is the only source for either, so a mismatch tells
|
||||||
|
// us nothing and last write wins. `platform` is ignored — the account already
|
||||||
|
// records the platform its login is linked to. Auth-gated; the client ignores the
|
||||||
|
// response body, and the real service answers with an empty array.
|
||||||
|
.post('/api/PlayerReporting/v1/deviceId', async (c) => {
|
||||||
|
const id = await authedId(c)
|
||||||
|
if (id === null) return unauthorized(c)
|
||||||
|
const body = await c.req.parseBody().catch(() => ({}) as Record<string, unknown>)
|
||||||
|
const newDeviceId = body.newDeviceId
|
||||||
|
if (typeof newDeviceId !== 'string' || newDeviceId === '') {
|
||||||
|
return c.json({ error: 'newDeviceId is required' }, 400)
|
||||||
|
}
|
||||||
|
await setDeviceId(c.env.DB, id, newDeviceId)
|
||||||
|
return c.json([])
|
||||||
|
})
|
||||||
|
|||||||
@@ -222,6 +222,37 @@ describe('public endpoints', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('POST /api/PlayerReporting/v1/deviceId stores the new device id', async () => {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/api/PlayerReporting/v1/deviceId`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...(await bearer()),
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
oldDeviceId: '491e8b9',
|
||||||
|
newDeviceId: '491e8b9566cb1b593367c72860e978b3d5765326',
|
||||||
|
platform: '0',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toEqual([])
|
||||||
|
|
||||||
|
const row = await env.DB.prepare(
|
||||||
|
"SELECT json_extract(data, '$.deviceId') AS deviceId FROM account WHERE account_id = 42"
|
||||||
|
).first<{ deviceId: string | null }>()
|
||||||
|
expect(row?.deviceId).toBe('491e8b9566cb1b593367c72860e978b3d5765326')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('POST /api/PlayerReporting/v1/deviceId 401s without a bearer token', async () => {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/api/PlayerReporting/v1/deviceId`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({ newDeviceId: 'abc' }),
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(401)
|
||||||
|
})
|
||||||
|
|
||||||
test('POST /api/playerReputation/v2/bulk returns a reputation per id', async () => {
|
test('POST /api/playerReputation/v2/bulk returns a reputation per id', async () => {
|
||||||
const res = await exports.default.fetch(`${ORIGIN}/api/playerReputation/v2/bulk`, {
|
const res = await exports.default.fetch(`${ORIGIN}/api/playerReputation/v2/bulk`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ export interface Account {
|
|||||||
phone?: string
|
phone?: string
|
||||||
/** Set via PUT /account/me/bio; read back via GET /account/:id/bio. */
|
/** Set via PUT /account/me/bio; read back via GET /account/:id/bio. */
|
||||||
bio?: string
|
bio?: string
|
||||||
|
/**
|
||||||
|
* Hardware/install id the client last reported (POST /api/PlayerReporting/v1/deviceId).
|
||||||
|
* The client rotates it — it posts the id it believes we hold plus the new one —
|
||||||
|
* so this is simply the most recent value it told us about, not a proven identity.
|
||||||
|
*/
|
||||||
|
deviceId?: string
|
||||||
/** Remaining username changes; decremented by PUT /account/me/username. */
|
/** Remaining username changes; decremented by PUT /account/me/username. */
|
||||||
availableUsernameChanges?: number
|
availableUsernameChanges?: number
|
||||||
/**
|
/**
|
||||||
@@ -211,6 +217,15 @@ export async function setLastLoginTime(db: D1Database, id: number, time: string)
|
|||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Record the device id the client last reported. False when no such account exists. */
|
||||||
|
export async function setDeviceId(db: D1Database, id: number, deviceId: string): Promise<boolean> {
|
||||||
|
const { meta } = await db
|
||||||
|
.prepare("UPDATE account SET data = json_set(data, '$.deviceId', ?2) WHERE account_id = ?1")
|
||||||
|
.bind(id, deviceId)
|
||||||
|
.run()
|
||||||
|
return meta.changes > 0
|
||||||
|
}
|
||||||
|
|
||||||
/** Look up multiple accounts by AccountId (order not guaranteed). */
|
/** Look up multiple accounts by AccountId (order not guaranteed). */
|
||||||
export async function getAccountsByIds(db: D1Database, ids: number[]): Promise<Account[]> {
|
export async function getAccountsByIds(db: D1Database, ids: number[]): Promise<Account[]> {
|
||||||
if (ids.length === 0) return []
|
if (ids.length === 0) return []
|
||||||
|
|||||||
Reference in New Issue
Block a user