api: pair the mobile app with device tokens

MB1 (spec 13). Decision 0005: our own Android/iPhone app, Expo in the
mobile repo, user portal only, no push in v1.

- mobile_devices (migration 0013): one-time pairing codes from My settings
  (QR or server + code, 10 minutes), traded by the app for an spm_ device
  token that acts as its user and never as an admin
- POST /api/me/mobile-devices/pair (web session only), POST /api/mobile/pair,
  GET/DELETE /api/me/mobile-devices
- signing out everywhere (password change or reset, disabling) removes
  paired phones too
- My settings: Mobile app section with the QR code and paired phones
- mobile/ (its own repo) ignored here
This commit is contained in:
iamdoubz committed 2026-10-26 17:12:53 -05:00
1 parent adb6d52ddf
commit 0cdac0bcdc
26 files changed
+5340 -18

No files matched your search

+34
View File
@@ -74,3 +74,37 @@ export const ContrackOptionsSchema = z.object({
logSummaries: z.boolean().optional(),
});
export type ContrackOptions = z.input<typeof ContrackOptionsSchema>;
/** POST /api/mobile/pair (13 §2): the app trades a pairing code for its device token. */
export const MobilePairSchema = z.object({
// Typed by hand from a screen: case and spaces don't matter.
code: z
.string()
.transform((s) => s.toUpperCase().replace(/[^A-Z0-9]/g, ''))
.pipe(z.string().length(12)),
name: z.string().trim().min(1).max(100),
platform: z.enum(['android', 'ios']),
});
export type MobilePair = z.input<typeof MobilePairSchema>;
export type MobilePairResult = { token: string; user: { name: string; email: string } };
/** POST /api/me/mobile-devices/pair: shown on the web as a QR code and as server + code. */
export type MobilePairCode = {
code: string;
server: string;
link: string;
qr: string;
expiresAt: string;
};
export type MobileDeviceDto = {
id: string;
name: string;
platform: 'android' | 'ios';
/** "spm_AbC1", to recognize it. */
tokenPrefix: string;
createdAt: string;
lastUsedAt: string | null;
/** The device making this request. */
current: boolean;
};