mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
feat(schema): APNs device columns + dispatcher channel-type extension
Adds the per-device APNs identity pair (`apnsToken` + `apnsEnvironment`) to the Device model so the dispatcher can fan a notification out to a user's iPhone via Apple's push gateway. The iOS client decides whether the token belongs to the sandbox or production gateway at registration time — the server never auto-detects, mirroring the W1 stream-2 recommendation. Migration 0037 is strictly additive: both columns nullable, a CHECK constraint pairs them so neither can be set without the other, and a single-column index on `apns_token` backs the dispatcher's per-user lookup plus the cross-user-hijack guard already enforced in `POST /api/devices`. The `NotificationChannel.type` column stays a free-form string — adding "APNS" as the fourth channel-type is a TypeScript-side change in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bb76ea4f98
commit
70789170cf
2 files changed
+55
-13
No files matched your search
@@ -0,0 +1,30 @@
|
||||
-- v1.4.23 — APNs send-side scaffolding (Wave 3 / F4)
|
||||
--
|
||||
-- Adds the per-device APNs identity pair so the dispatcher can fan a
|
||||
-- notification out to a user's iPhone via Apple's push gateway. Strictly
|
||||
-- additive:
|
||||
-- * Both columns nullable — pre-iOS-app rows on disk stay valid.
|
||||
-- * `apnsToken` indexed for the dispatcher's per-user lookup +
|
||||
-- cross-user-hijack guard in `POST /api/devices`.
|
||||
-- * CHECK constraint enforces "if you store an APNs token, you must
|
||||
-- also tell us which gateway it belongs to". The iOS client decides
|
||||
-- `sandbox` vs `production` at registration time — the server never
|
||||
-- auto-detects (per W1 stream-2 recommendation).
|
||||
--
|
||||
-- The `NotificationChannel.type` column is a free-form String, not a
|
||||
-- Postgres enum, so adding the new "APNS" channel-type value is a
|
||||
-- TypeScript-side change only — no DDL needed for the channel cascade.
|
||||
|
||||
ALTER TABLE "devices"
|
||||
ADD COLUMN "apns_token" TEXT,
|
||||
ADD COLUMN "apns_environment" TEXT;
|
||||
|
||||
ALTER TABLE "devices"
|
||||
ADD CONSTRAINT "devices_apns_environment_required_with_token"
|
||||
CHECK (
|
||||
("apns_token" IS NULL AND "apns_environment" IS NULL)
|
||||
OR
|
||||
("apns_token" IS NOT NULL AND "apns_environment" IS NOT NULL)
|
||||
);
|
||||
|
||||
CREATE INDEX "devices_apns_token_idx" ON "devices" ("apns_token");
|
||||
+25
-13
@@ -678,7 +678,7 @@ model AuditLog {
|
||||
model NotificationChannel {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
type String // "TELEGRAM" | "NTFY" | "WEB_PUSH"
|
||||
type String // "TELEGRAM" | "NTFY" | "WEB_PUSH" | "APNS"
|
||||
enabled Boolean @default(true)
|
||||
config String // AES-256-GCM encrypted JSON (channel-specific config)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
@@ -790,23 +790,35 @@ model IdempotencyKey {
|
||||
// ─── Devices (APNs / mobile push targets) ────────────────
|
||||
|
||||
/// Registered native-client device for push delivery. Token is unique
|
||||
/// across users; re-registering on a different account moves ownership.
|
||||
/// across users; re-registering on a different account is rejected with
|
||||
/// 409 by `POST /api/devices` (cross-user-hijack guard).
|
||||
///
|
||||
/// `apnsToken` and `apnsEnvironment` are nullable because today's iOS
|
||||
/// builds register through a generic `token` first; a follow-up call
|
||||
/// from the iOS app populates the APNs-specific pair once the device
|
||||
/// receives its first push token from `application:didRegisterForRemoteNotificationsWithDeviceToken:`.
|
||||
/// Loose-typed `apnsEnvironment` (sandbox|production) leaves headroom
|
||||
/// for whatever new gateway labels Apple ships next without a forced
|
||||
/// migration.
|
||||
model Device {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
platform String // "ios"
|
||||
token String @unique
|
||||
bundleId String @map("bundle_id")
|
||||
locale String?
|
||||
appVersion String? @map("app_version")
|
||||
model String?
|
||||
lastSeen DateTime @default(now()) @map("last_seen")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
platform String // "ios"
|
||||
token String @unique
|
||||
bundleId String @map("bundle_id")
|
||||
locale String?
|
||||
appVersion String? @map("app_version")
|
||||
model String?
|
||||
apnsToken String? @map("apns_token")
|
||||
apnsEnvironment String? @map("apns_environment") // "sandbox" | "production"
|
||||
lastSeen DateTime @default(now()) @map("last_seen")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([apnsToken])
|
||||
@@map("devices")
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user