feat(mobile): show real orgs, servers, detections and scans; register Expo push - #241
Merged
Merged
Conversation
… Expo push Replace the placeholder entry and demo stores with an expo-router app that signs in against /api/auth/login and reads the user's orgs, servers, detections, remediations and scan runs. Add GET /api/orgs/:id/runs, POST /api/auth/refresh and POST/DELETE /api/orgs/:id/push-subscriptions; remove the stub GET /api/events. Run mobile typecheck and tests in PR Checks.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
| if (!body || typeof body !== "object" || Array.isArray(body)) { | ||
| return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); | ||
| } | ||
| refreshToken = "refresh_token" in body ? body.refresh_token : undefined; |
ThreatCrush Security Scan13 finding(s) HIGH/CRITICAL: 1 | MEDIUM: 6 | LOW: 6
Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The mobile app shipped a placeholder:
index.jsrendered a "bundle isolation" splash screen, the real screens sat inapp-disabled/, andsrc/stores/events.tsserved hardcoded threats, modules and stats. This PR replaces all of it with an app that signs in against the real auth API and shows the user's actual orgs, servers, detections, remediations and scan results. The demo path is deleted outright, with no fallback to fake data.Mobile (
apps/mobile)expo-router/entry. Screens:login, then four tabs (Threats, Servers, Scans, Settings) plus an org switcher.src/lib/api.tsis a thin client over existing routes. ItsApiErrorkeeps the status and the server's own error message.src/stores/auth.tsholds the Supabase session from/api/auth/loginin SecureStore. It refreshes before expiry and on a 401. Refreshes are single-flight because refresh tokens rotate: three parallel 401s must not burn the token three times and log the user out. A rejected refresh signs the user out.src/stores/workspace.tsloads orgs, picks the profile'scurrent_org_id(the same one the web dashboard uses), then fetches servers, detections (paged), remediations and runs. Switching orgs drops the old org's data and ignores late replies from the previous org.src/stores/push.tshandles Expo push (see P1 below).crypto.ts+ tweetnacl, never wired to anything), the "Skip → use demo mode" login,App.tsx/index.js/app-disabled/.vitest.config.tsand store tests added. The mobile typecheck and tests now run in PR Checks. Typecheck previously failed onprocessinapp.config.ts;@types/nodefixes that.Web API (
apps/web) — three small additions and one removal. They follow the existing pattern: bearer token throughlib/api-auth.ts, anorganization_memberscheck,parsePaginationParam, and middleware rate limiting.GET /api/orgs/:id/runs: recent scan/pentest runs across every property in the org, with the property name/kind/target. It leaves out the largeoutput/findingscolumns. Limit is 1..100 (default 25), plus offset and an optionalstatusfilter. Without it, a feed would need one request per property.POST /api/auth/refresh:{refresh_token}→ a rotated session. Without it, every bearer client (mobile, and the CLI, which already storesrefresh_tokenbut never uses it) had to ask for the password every hour. It has its own rate-limit rule (30/min/IP).POST/DELETE /api/orgs/:id/push-subscriptions: registers or forgets an Expo push token in the existingpush_subscriptionstable. No migration needed:endpointholds the token;keys = {provider: "expo", platform}.(user_id, endpoint).Expo(nent)PushToken[...]values are accepted, so the table can't be used to park arbitrary URLs for a future sender.GET /api/events. Its only consumer was the mobile demo client, and it returnedevents: []with a "threats" count that was really the number ofphone_verification_codesrows withattempts > 0.New dependency:
expo-notifications@~0.29.14(the SDK 52 version). It is needed to get an Expo push token; there is no alternative in the tree. The lockfile change beyond it is pnpm re-resolvingvite's optionaljitipeer from 1.21.7 to 2.6.1 forelectron-vite.apps/desktopstill builds with it (pnpm build✓).Data map (what exists server-side today)
organizations,organization_members(20260412)threatcrush orgs create→POST /api/orgs(trigger adds owner)GET /api/orgsservers(20260412)POST /api/orgs/:id/serversGET /api/orgs/:id/serversstatus/last_seen/threatcrushd_versiononly change via an/api/ingestheartbeat, which nothing sends, so every server reads offline / never seenproperties,property_runs(20260419*)runs-workerclaims (POST /runs/pending) and PATCHes resultsGET /api/orgs/:id/properties,…/properties/:pid/runs, newGET /api/orgs/:id/runsdetections(20260601)POST /api/ingestonlyGET /api/orgs/:id/detections(limit/offset, total),…/servers/:sid/detections/api/ingest: not the daemon, CLI, desktop or extensionremediation_actions(20260601)POST /api/orgs/:id/remediationsGET /api/orgs/:id/remediationspendinghardening_findings(20260601)GET …/servers/:sid/findingsalert_destinations,alert_rules(20260601)push_subscriptions(20260601)/api/servers/:id/events[]The real gap: no daemon → cloud detection pipeline
threatcrushddetects locally and alerts through the TUI/IPC, SMTP, Discord and PagerDuty (apps/cli/src/daemon/alerts). It never uploads detections./api/ingestexists and works (the smoke test below drives it), but has no producer. This blocks mobile, the extension and the web dashboard from showing live threats. It also blocks sending pushes, because there is nothing to push about. I did not build it here. Minimal design:~/.threatcrush/config.json;runs-workeruses it. Add aserver_idto that config:threatcrush servers link [name]matches or creates the org's server row by hostname. Longer term, use a per-server enrollment key (PRD-00 §9) instead of a user JWT, and refresh the token with/api/auth/refresh(added here) because access tokens last an hour.runs-worker. It subscribes to the event bus for detections at or above a configurable minimum severity (defaultmedium). It batches up to 100 detections or 10 s intoPOST /api/ingest {events:[{type:"detection", server_id, rule_id, severity, title, description, source_ip, detected_at, raw_metadata}]}and sends aheartbeatevery 60 s. Failed batches go to a bounded on-disk spool (drop oldest) so an outage doesn't grow memory. Uploading is independent of auto-defend, so no ban behaviour changes.(server_id, rule_id, source_ip, 1-minute bucket)so a flood becomes one row with a count.alert_rules.push_subscriptionswhereorganization_id = …andkeys->>'provider' = 'expo',POST https://exp.host/--/api/v2/push/sendin chunks of 100, and delete tokens whose receipts sayDeviceNotRegistered.P1 push
Registration is done. After sign-in, if the OS permission is still undecided, the Threats tab shows a card before the system prompt. It says what alerts are for and that alerts aren't flowing yet because servers don't upload detections. Nothing prompts on launch. If permission is already granted, the token is registered silently for the current org, re-registered when the org changes, and unregistered on sign-out. Settings shows the real state (on / off / blocked in system settings / not available / error) with a link to system settings.
Server-side sending is intentionally not implemented: there is no real detection source to trigger it (see above). It is step 4 of the design.
How verified
orgs/[id]/runs7,orgs/[id]/push-subscriptions14,auth/refresh7. They cover:fetchis stubbed at the HTTP boundary. Mutation-checked: removing the refresh single-flight fails "shares one refresh between concurrent 401s", and removing the org-switch guard fails the late-reply test.pnpm --filter @profullstack/threatcrush-mobile typecheck✓.npx expo export --platform android --platform ios✓ (Android 1250 modules, iOS 1245, 3.54 MB Hermes bundle each;expo config --type prebuildresolves theexpo-notificationsplugin and the library manifest addsPOST_NOTIFICATIONS).next build✓ (the new routes are in the route table,/api/eventsis gone).supabase start, every repo migration applied) andnext devfrom this branch.POST /api/orgs, then a server added through the servers route.POST /api/ingest.POST …/remediations.POST …/runs/pending, and completed through the PATCH route, the same wayruns-workerdoes it.current_org_idhad been saved throughPATCH /api/auth/me.refresh_token.keys = {provider: "expo", platform: "android"}. The app-side flow is covered by the push store tests; the web build has no push, and Settings correctly shows "Not available in this build"./runsreturns{total: 1, runs: [{status: succeeded, findings_count: 3, severity_summary: {high:1, medium:2}, property: {...}}]}with nooutputfield./runsand on push register./api/events→ 404.threatcrush properties add <name> <target>,install.sh), as do the web pages the links open (/org/:slug/properties/new,/org/:slug/servers/new).apps/desktoptest ✓ andpnpm build✓ with the lockfile change.docs/SURFACES.mdanddocs/MOBILE_RELEASE_TODO.mdnow say the app shows real data and that push is registration-only.Blocked on the account owner
com.threatcrush.mobile. It is needed for any iOS build and for APNs. Then runeas credentials → iOS(distribution certificate and push key).eas submit(eas.json → submit.production.android.serviceAccountKeyPath).com.threatcrush.mobile. Uploadgoogle-services.jsonas the EAS file variableGOOGLE_SERVICES_JSON(app.config.tspicks it up automatically when present) and the FCM V1 service-account key ineas credentials → Android → Push Notifications. Without it, the app builds and "Turn on alerts" shows Firebase's error instead of registering.eas credentials.Residual risks
UNIQUE(user_id, endpoint)in the existing table). That is fine for alerts on the org you're viewing; multi-org alerts would need a schema change.