Add signed-in account area: submissions, moderation, and developer profiles#283
Conversation
Mirrors the FOSSBilling/api repo's own v2 rename (authors -> developers table, /authors/* -> /developers/* routes, SubmissionPayload.author -> .developer): "author" implied solo/literary authorship, which never fit an entity that can be an organization, gets moderated, and can be transferred or claimed. Required, not cosmetic — submissions and every developer-profile action would 404/422 against the live api otherwise. extensions.author_id (the v1-owned FK column) is deliberately left unrenamed, matching the api repo's own v1 boundary; database.ts aliases it to developer_id immediately so nothing downstream depends on the old name. Claude-Session: https://claude.ai/code/session_01GP7BWksJf2KBFc2RGirEyN
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
extensions | c3165c1 | Commit Preview URL Branch Preview URL |
Jul 26 2026, 08:41 PM |
There was a problem hiding this comment.
All reported issues were addressed
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Addresses the cubic-dev-ai review on #283 (20 findings): Security: - oauth.ts: isSafeRedirectPath rejected absolute/protocol-relative URLs and backslashes but not tab/CR/LF, which URL parsers strip — "/\t/evil.com" passed the check but resolved to "//evil.com" (protocol-relative) once a browser followed the redirect. Reject those characters too. - Developer-supplied URLs (Developer.URL, DeveloperHistoryEntry.URL) were rendered as <a href> unchecked; a javascript: URI would execute in whoever clicked it, including a moderator reviewing profile history. Added isSafeHttpUrl (src/lib/safe-url.ts) and applied it at all three render sites (public developer page, moderator profile list, history). - logout.ts: POST had no origin check, so a cross-site auto-submitted form could force a session to be cleared (Set-Cookie deletion applies regardless of whether the request carried the cookie). Reject POSTs with a mismatched Origin. - session.ts: a malformed session cookie made base64url/HMAC verification throw uncaught, 500ing every page (including public ones) for that visitor instead of treating them as signed out. Wrapped in try/catch, matching the JSON.parse handling already in the same function. Correctness: - users.ts: callback.ts intentionally doesn't fail sign-in if upsertUser fails, which can leave a signed-in user with no `users` row. updateUserProfile's UPDATE would then silently affect zero rows and the caller would redirect claiming success. Now ensures the row exists (INSERT ... ON CONFLICT DO NOTHING) before updating, and refreshes updated_at. - submission-form.ts: adding a release during an edit only required version_tag (the other fields are optional in edit mode); filling in just the tag sent empty date/download_url/min_fossbilling_version to the api. Now validates all four together and raises a SubmissionValidationError the calling pages surface as a normal form error. - account/developer/index.astro: a failed profile save (e.g. duplicate id) re-rendered the form from the pre-save `developer`, blanking every field on first-time creation. Now falls back to the just-submitted values. - login.ts: a stale OAUTH_REDIRECT_COOKIE from an abandoned login attempt could redirect a later, unrelated login attempt to the old target. Clear it explicitly when the new attempt has no (safe) redirect of its own. - Base.astro: the auth-error banner said "Sign-in with GitHub failed", but sign-in is delegated to auth.fossbilling.net's OIDC service, not GitHub directly. Generic message instead. - types/index.ts: DeveloperProfileInput only omitted `approved`, leaving `unclaimed` (local-only, api-repo-unaware) assignable into a PUT body. Now omits both. - developer-form.ts: the publisher `type` field was cast from form input with no validation against DEVELOPER_TYPES. Now validated, falls back to 'user' for anything else. Robustness (5 sites): FormData.get() can return a File for any field name in a multipart request; several handlers cast straight to `string | null` and called .trim(), which throws on a File and 500s the request instead of treating it as invalid input. Added a shared formString() helper (src/lib/form.ts) and applied it in submission-form.ts, developer-form.ts, account/profile/index.astro, developer/[id].astro, and both reject.ts moderation routes. Migration tooling (the one P1 with no code fix otherwise): db:migrate:local and db:migrate:remote only ever ran the base CREATE TABLE IF NOT EXISTS, never the incremental ALTER migrations — exactly the gap that made the display_name/bio columns missing on both local and remote earlier today. Adopted wrangler's native D1 migrations (migrations_dir in wrangler.jsonc), split the schema into 0001_create_users/0002_add_is_moderator/ 0003_add_profile_fields, and seeded the d1_migrations bookkeeping table on both databases to reflect the ALTERs already applied by hand — the shared D1 database already uses this same mechanism for the api repo's own migrations, so this just brings this repo's side in line with it. Not changed: auth/login.ts's redirect_uri uses url.origin, which is correct for the two actually-registered callback URIs (production, localhost) but means OAuth can't complete on ad-hoc preview deployments. Switching to a fixed canonical origin wouldn't actually fix that — the session would land on production instead of the preview being tested — so this is left as a known limitation rather than a fake fix. Claude-Session: https://claude.ai/code/session_01GP7BWksJf2KBFc2RGirEyN
The previous commit's git add included a stale pathspec that made the whole command abort, so only the migration file renames actually landed. This is the rest of that same change — see the previous commit message for the full description of what's being fixed. Claude-Session: https://claude.ai/code/session_01GP7BWksJf2KBFc2RGirEyN
Lets a developer delete just their publisher profile or their whole account, gated on having no published extensions to avoid orphaning them. Requires the api repo's new DELETE /developers/me endpoint. Claude-Session: https://claude.ai/code/session_01GP7BWksJf2KBFc2RGirEyN
There was a problem hiding this comment.
All reported issues were addressed across 49 files
Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.
Re-trigger cubic
- requireUser now verifies the users row still exists, closing the gap where a session on another device survives account deletion for up to 30 days - edit-mode submissions always use the verified extension id, never the form's own extension_id - extension type/source type are validated against known enums; a new (non-edit) submission with no release now fails validation instead of publishing empty version/download_url fields - empty developer id on create is rejected instead of reaching the API - public developer reads are typed without contact_email so a future change can't leak it without a type error - logout rejects a missing Origin header, not just a mismatched one - removed the unused offline_access OAuth scope - apiClient synthesizes a clean ApiRequestError on a non-JSON upstream response instead of throwing a raw SyntaxError - extension edit form preserves submitted values on a rejected edit Claude-Session: https://claude.ai/code/session_01GP7BWksJf2KBFc2RGirEyN
There was a problem hiding this comment.
0 issues found across 11 files (changes from recent commits).
Requires human review: PR adds database migrations, authentication/authorization logic, and a new user-facing account area — all changes that require human sign-off for security, data model, and architectural decisions.
Re-trigger cubic
Summary
/accountarea on top of it./developer/[id]page, an approval trust badge, an append-only edit-history log moderators can inspect, single-use ownership-transfer links, and a moderator-approved claim flow for linking unowned ("legacy") profiles to an account.developers/extensionstables go through theFOSSBilling/apirepo's/extensions/v2service (bearer-assertion authenticated); this app never writes to those tables directly, only reads them for public listings.author→developerthroughout to match the api repo's own v2 rename (routes, schemas,SubmissionPayload.author→.developer) — required, not cosmetic, since submissions and developer-profile actions would otherwise 404/422 against the live api.See the README's "Authentication" section for the full ownership/moderation model.
Test plan
npm run checkandnpm run format:check(both pass locally)/account/developer→ accept it from a second account → ownership moves, badge clears/account/moderate/developers/claims/account/moderate/developers/[id]/history