Skip to content

[#485] Move Bloom participant flows out of Blade#486

Merged
Adr1an04 merged 10 commits into
mainfrom
repo/hackathon-portals
Jul 4, 2026
Merged

[#485] Move Bloom participant flows out of Blade#486
Adr1an04 merged 10 commits into
mainfrom
repo/hackathon-portals

Conversation

@DVidal1205

@DVidal1205 DVidal1205 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • move Bloom participant auth, application, dashboard, profile, resume, QR, schedule, and issue-reporting flows onto bloom.knighthacks.org
  • add the typed participant-only API surface and headless @forge/hackathon toolkit for future event portals
  • keep Blade member/admin-only, remove the live-hackathon chooser, and convert legacy participant routes into validated portal redirects
  • add portal origin and confirmation-capacity fields with the Bloom backfill and archived-event null support
  • split auth and feature environment configuration so Bloom mounts only its own required surfaces

Madu dashboard and Bloom visual work is ported with co-author attribution. The reforge/main experiment is not included.

Verification

  • pnpm format
  • pnpm lint
  • pnpm lint:ws
  • pnpm typecheck
  • focused builds for DB, validators, utils, API, and @forge/hackathon
  • production builds for Bloom and Blade
  • populated local database verification: Bloom portal/capacity backfill, archived null portals, and preserved user/session/hacker/attendee records
  • runtime smoke tests for Bloom auth redirects, OAuth retry, exact callback origin, public/protected participant API behavior, and legacy Blade redirects/unavailable states

Deployment order

  1. Set BLOOMKNIGHTS_URL=https://bloom.knighthacks.org.
  2. Register https://bloom.knighthacks.org/api/auth/callback/discord and http://localhost:3006/api/auth/callback/discord in Discord.
  3. Apply migration 0010_wooden_supreme_intelligence.
  4. Deploy and smoke-test Bloom.
  5. Deploy Blade redirects only after Bloom is healthy.
  6. Complete staged production OAuth and responsive/reduced-motion QA before merge.

Closes #485

Summary by CodeRabbit

  • New Features
    • Added participant-portal support with external Dashboard, Application, and Profile flows.
    • Hackathon admin can configure participant portal origin and confirmation capacity, surfaced in hackathon management.
    • Enhanced BloomKnights portal UI with confirmation/withdrawal actions, schedule, QR check-in, issue reporting, and improved “portal unavailable” + Discord sign-in retry screens.
  • Bug Fixes
    • Legacy hackathon and application routes now consistently redirect to the correct portal destination or show “portal unavailable” when missing.
  • Refactor
    • Removed the Blade in-app hackathon/hacker dashboard UI in favor of external portal navigation.

Co-authored-by: Madu <madudiop1122@gmail.com>
@DVidal1205 DVidal1205 requested a review from a team as a code owner July 3, 2026 21:03
@DVidal1205 DVidal1205 added Feature New Feature or Request Major Big change - 2+ reviewers required Blade Change modifies code in Blade app Hack Sites Change modifies code in a Hackathon app (ex. 2025) Database Change modifies code in the DB package API Change modifies code in the global API/tRPC package Global Change modifies code for the entire repository labels Jul 3, 2026
@DVidal1205 DVidal1205 self-assigned this Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Blade now redirects participant traffic to BloomKnights, BloomKnights owns the application/dashboard/profile experience, and shared packages provide portal config, auth, and participant API support. Hackathon portal origin and confirmation capacity are added across schema, validation, and admin flows.

Changes

Blade Admin Config and Legacy Redirects

Layer / File(s) Summary
Hackathon portal config and admin form
packages/db/src/schemas/knight-hacks.ts, packages/db/drizzle/0010_*, packages/validators/src/hackathons.ts, packages/api/src/routers/hackathon.ts, apps/blade/src/app/_components/admin/hackathon/manage/hackathon-manager.tsx
Adds portalBaseUrl and confirmationCapacity to the hackathon schema, validation, router, and admin UI.
Portal URL helper and unavailable fallback
apps/blade/src/lib/hackathon-portal.ts, apps/blade/src/app/_components/hackathon/portal-unavailable.tsx
Adds portal URL construction and a fallback view when no portal is configured.
Legacy participant redirects
apps/blade/src/app/hackathon/..., apps/blade/src/app/hacker/application/[hackathon-id]/page.tsx, apps/blade/src/app/settings/hacker-profile/page.tsx, apps/blade/src/app/hackathon/README.md
Rewrites Blade participant routes to redirect to the external portal and updates the legacy guide.
Member-only dashboard and settings
apps/blade/src/app/dashboard/page.tsx, apps/blade/src/app/settings/page.tsx, apps/blade/src/consts/index.ts, apps/blade/src/app/_components/settings/sidebar-nav.tsx, apps/blade/src/app/_components/option-cards.tsx, apps/blade/src/app/_components/providers.tsx
Removes hacker-only dashboard behavior and navigation from Blade.
Blade environment wiring
.env.example, apps/blade/src/env.ts, apps/blade/src/env.client.ts, apps/blade/src/trpc/react.tsx, turbo.json
Adds BloomKnights/Blade URL env vars and uses them for client/server base URL resolution.

BloomKnights Portal Application

Layer / File(s) Summary
App config, env, and auth wiring
apps/bloomknights/next.config.js, apps/bloomknights/package.json, apps/bloomknights/src/env.ts, apps/bloomknights/src/auth/*, apps/bloomknights/src/lib/portal-config.ts, apps/bloomknights/src/app/api/**
Adds local auth, tRPC handlers, env config, and portal config for the standalone BloomKnights app.
Root layout and marketing site
apps/bloomknights/src/app/layout.tsx, apps/bloomknights/src/app/(marketing)/*
Simplifies the root layout and adds the marketing page/layout with auth-aware registration.
Portal layout and navigation
apps/bloomknights/src/app/(portal)/layout.tsx, apps/bloomknights/src/app/_components/navbar/*, apps/bloomknights/src/app/(portal)/_components/*
Adds the portal shell, header, retry UI, dashboard shell, and session-aware navbar behavior.
Portal pages
apps/bloomknights/src/app/(portal)/{apply,dashboard,dashboard/profile}/page.tsx, apps/bloomknights/src/app/(portal)/hacker/application/bloomknights/page.tsx
Adds the authenticated portal pages and the legacy /hacker/application/bloomknights redirect.
Dashboard and visual components
apps/bloomknights/src/app/(portal)/_components/bloom-dashboard.tsx, bloomknights-*.tsx, apps/bloomknights/src/app/globals.css
Implements dashboard state/actions and the portal-specific decorative styling and assets.
Application/profile forms
apps/bloomknights/src/app/(portal)/_components/application/hacker-application-form.tsx, apps/bloomknights/src/app/(portal)/_components/hacker-profile-form.tsx
Rewires the forms onto shared hackathon hooks and portal schemas.

@forge/hackathon Headless Toolkit

Layer / File(s) Summary
Package scaffolding
packages/hackathon/package.json, eslint.config.js, tsconfig.json, README.md
Creates the shared workspace package and its documentation.
Config, lifecycle, and types
packages/hackathon/src/config.ts, packages/hackathon/src/lifecycle.ts, packages/hackathon/src/types.ts, packages/hackathon/src/index.ts
Defines portal config, application steps, lifecycle state, and portal-facing types.
Application/profile schemas
packages/hackathon/src/application-schema.ts
Adds application/profile Zod schemas, defaults, and prefill helpers.
Provider, hooks, server caller
packages/hackathon/src/client.tsx, packages/hackathon/src/query-client.ts, packages/hackathon/src/server.ts
Implements the portal provider, hooks, query client, and server caller.

Participant tRPC Router and Contract

Layer / File(s) Summary
Participant contract types
packages/api/src/participant-contract.ts
Defines participant dashboard/context/schedule/contract types.
Participant router implementation
packages/api/src/routers/participant-portal.ts
Implements dashboard, application, resume, attendance, QR, schedule, and issue procedures.
Router assembly and export
packages/api/src/participant.ts, packages/api/package.json
Exposes the participant router via @forge/api/participant.
tRPC context
packages/api/src/trpc.ts
Uses the pre-validated session directly in context creation.
Hacker mutation authorization
packages/api/src/routers/hackers/mutations.ts, packages/validators/src/hacker.ts
Updates create/update/delete hacker flows to use the application wire schema and target-user cleanup.

@forge/auth Factory Refactor

Layer / File(s) Summary
Shared env and callback sanitization
packages/auth/src/shared-env.ts, packages/auth/src/callback-url.ts
Adds shared auth env validation and explicit callback URL sanitization inputs.
Server instance factory
packages/auth/src/factory.ts
Builds the reusable auth instance.
Next.js server factory
packages/auth/src/server-factory.ts
Provides server-side auth/session/sign-in helpers.
Browser client factory
packages/auth/src/client-factory.ts
Provides the browser auth client helpers.
Blade rewiring
packages/auth/src/index.ts, packages/auth/src/index.rsc.ts, packages/auth/package.json
Re-exports the new auth factories through Blade-facing entrypoints.

Service-specific Environment Modules

Layer / File(s) Summary
Dedicated env schemas
packages/utils/src/{discord,google,stripe}-env.ts, packages/api/src/storage-env.ts, and consumers
Moves MinIO, Discord, Google, and Stripe credentials into dedicated env modules.

Architecture and Permissions Documentation

Layer / File(s) Summary
Docs updates
docs/API-AND-PERMISSIONS.md, docs/ARCHITECTURE.md
Documents the participant router and the new app/auth split.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Hacker
  participant BloomKnightsPortal
  participant bloomAuth
  participant participantRouter
  participant Database
  Hacker->>BloomKnightsPortal: visit /apply
  BloomKnightsPortal->>bloomAuth: auth()
  alt no session
    bloomAuth-->>Hacker: signIn Discord redirect
  else session valid
    BloomKnightsPortal->>participantRouter: getHackathon("bloomknights")
    participantRouter->>Database: query hackathon
    Database-->>participantRouter: hackathon data
    Hacker->>BloomKnightsPortal: submit application
    BloomKnightsPortal->>participantRouter: submitApplication
    participantRouter->>Database: insert hacker record
    BloomKnightsPortal-->>Hacker: redirect to /dashboard
  end
Loading
sequenceDiagram
  participant BladeUser
  participant BladePage
  participant buildParticipantPortalUrl
  participant PortalUnavailable
  BladeUser->>BladePage: GET legacy hacker application route
  BladePage->>buildParticipantPortalUrl: portalBaseUrl + "/apply"
  alt portal configured
    buildParticipantPortalUrl-->>BladePage: portal URL
    BladePage-->>BladeUser: redirect to BloomKnights /apply
  else no portalBaseUrl
    BladePage->>PortalUnavailable: render fallback
    PortalUnavailable-->>BladeUser: no portal message
  end
Loading

Possibly related PRs

  • KnightHacks/forge#456: Introduces the BloomKnights app foundation and local auth pieces that this PR extends into a full participant portal.
  • KnightHacks/forge#475: Covers the older hackathon dashboard modules that this PR removes or replaces with portal redirects.
  • KnightHacks/forge#374: Touches the Blade hacker dashboard flow that this PR simplifies or removes.

Suggested reviewers: Adr1an04, DGoel1602

🚥 Pre-merge checks | ✅ 7 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Validated Env Access ⚠️ Warning Raw process.env still appears in apps/guild/src/trpc/react.tsx and packages/consts/src/util.ts (plus DB scripts), outside env.ts/config files. Move those reads behind validated env imports or into env/config files. For scripts, inject env via a shared helper instead of accessing process.env inline.
✅ Passed checks (7 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title starts with the required issue tag and clearly summarizes the main change.
Linked Issues check ✅ Passed The PR implements the participant-flow move, local auth, portal redirects, participant router, and portal URL/capacity support requested in #485.
Out of Scope Changes check ✅ Passed The changes stay focused on the participant-portal/auth split and related docs/config, with no clear unrelated scope creep.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
No Hardcoded Secrets ✅ Passed Scans found only a doc example, a non-secret sessionToken field name, and a CDN ref; no hardcoded API keys/passwords/tokens/secrets in source.
No Typescript Escape Hatches ✅ Passed Changed TS files contain no any, @ts-ignore, @ts-expect-error, or postfix ! assertions; remaining ! uses are ordinary boolean negation.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch repo/hackathon-portals

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/bloomknights/src/app/(portal)/_components/hacker-profile-form.tsx (1)

93-126: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize dob and gradDate before form.reset()

<input type="date"> only renders YYYY-MM-DD, but this reset path passes hacker.dob / hacker.gradDate through unchanged. If those values are Date objects or ISO timestamps, the edit form can show blank date fields. Match the application flow and format them first:

const toDateInputValue = (value: Date | string) => {
  const date = value instanceof Date ? value : new Date(value);
  return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(0, 10);
};

dob: toDateInputValue(hacker.dob),
gradDate: toDateInputValue(hacker.gradDate),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/bloomknights/src/app/`(portal)/_components/hacker-profile-form.tsx
around lines 93 - 126, Normalize the hacker profile date fields before calling
form.reset in hacker-profile-form.tsx: the useEffect that resets the form
currently passes hacker.dob and hacker.gradDate through unchanged, which can
break <input type="date"> display when they are Date objects or ISO strings. Add
a small formatter helper near the HackerProfileForm reset logic and use it for
dob and gradDate so the values are always YYYY-MM-DD before resetting the form.
🧹 Nitpick comments (15)
apps/blade/src/app/_components/settings/sidebar-nav.tsx (1)

27-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Loading state lacks screen-reader announcement.

The error branch below uses aria-live="polite" (Line 37) but the loading spinner doesn't announce itself to assistive tech.

♿ Suggested fix
   if (memberLoading) {
     return (
-      <div className="flex h-full w-full items-center justify-center">
+      <div
+        className="flex h-full w-full items-center justify-center"
+        role="status"
+        aria-live="polite"
+      >
         <Loader2 className="h-6 w-6 animate-spin" />
+        <span className="sr-only">Loading navigation…</span>
       </div>
     );
   }

As per path instructions, apps/blade/** requires checking "Accessibility (alt text, ARIA, semantic HTML)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blade/src/app/_components/settings/sidebar-nav.tsx` around lines 27 -
33, The loading branch in sidebar-nav.tsx does not announce its state to
assistive tech, unlike the error state that already uses aria-live. Update the
memberLoading return block in the SidebarNav component to add appropriate ARIA
live/status semantics (for example on the wrapper around the Loader2 spinner) so
screen readers are notified that loading is in progress.
packages/auth/src/factory.ts (1)

54-61: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hardcoded @blade.org email domain in a now-shared factory.

createForgeAuth is reused by BloomKnights too, so every synthesized user email lands on @blade.org regardless of the mounting app. Since these synthetic emails may be used downstream as identifiers, consider parameterizing the domain via ForgeAuthOptions (uniqueness is already preserved by profile.id).

 export interface ForgeAuthOptions {
   baseURL: string;
+  syntheticEmailDomain?: string;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/auth/src/factory.ts` around lines 54 - 61, The synthesized email in
createForgeAuth is hardcoded to `@blade.org`, which breaks reuse in other apps
like BloomKnights. Update ForgeAuthOptions to accept an email domain (or
equivalent configurable value) and use that inside mapProfileToUser instead of
the literal domain, while keeping the identifier based on profile.id so
uniqueness remains unchanged.
packages/validators/src/hackathons.ts (2)

26-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Deprecated .url() string method — consider z.url().

Zod v4 deprecated the chainable z.string().url() in favor of the top-level z.url(); the method form still works today but will be removed in a future major version.

♻️ Migration example
 export const hackathonPortalOriginSchema = z
-  .string()
+  .url("Enter a valid portal URL.")
   .trim()
-  .url("Enter a valid portal URL.")
   .superRefine((value, ctx) => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/validators/src/hackathons.ts` around lines 26 - 53, The hackathon
portal schema uses the deprecated chainable string URL validator in
hackathonPortalOriginSchema. Update the schema to use the top-level z.url()
validator instead of z.string().url(), while keeping the existing trim,
superRefine, and transform behavior intact so the validation logic and output
origin normalization remain the same.

61-69: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

LGTM, minor edge case: whitespace-only capacity input coerces to 0.

z.coerce.number() on a whitespace-only string (e.g. " ") yields 0 rather than NaN/null, since Number(" ") === 0. This produces the "must be greater than zero" error instead of treating it as blank/unlimited. Low risk in practice since the UI uses type="number", which normalizes such input to "" before submission.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/validators/src/hackathons.ts` around lines 61 - 69, The
whitespace-only input edge case in hackathonConfirmationCapacitySchema should be
treated as blank instead of coercing to 0. Update the preprocess step in
hackathonConfirmationCapacitySchema to normalize strings containing only
whitespace to null before z.coerce.number() runs, alongside the existing
empty/undefined/null handling. Use the hackathonConfirmationCapacitySchema
symbol to locate the validator and keep the rest of the number validation
unchanged.
apps/blade/src/app/_components/admin/hackathon/manage/hackathon-manager.tsx (1)

132-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Only the first validation issue per field is surfaced.

portalResult.error.issues[0]?.message drops any additional issues (e.g., hackathonPortalOriginSchema.superRefine can add both a protocol issue and a path/query/hash issue for the same invalid input). Minor UX nit — not blocking since the first message is still actionable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blade/src/app/_components/admin/hackathon/manage/hackathon-manager.tsx`
around lines 132 - 146, Only the first validation message is being forwarded for
each field in hackathon-manager.tsx, so multi-issue validations from
portalResult and capacityResult lose useful feedback. Update the portalBaseUrl
and confirmationCapacity handling in the validation block to collect and surface
all messages from each result’s issues array (for example by combining them into
one string or adding separate issues), instead of reading only
error.issues[0].message. Use the existing portalResult, capacityResult, and
ctx.addIssue locations to keep the fix local.
apps/blade/src/lib/hackathon-portal.ts (1)

1-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate origin-validation logic vs. packages/validators/src/hackathons.ts.

hackathonPortalOriginSchema already encodes the exact same "bare http(s) origin, no credentials/path/search/hash" rule used to validate portalBaseUrl on write. Re-implementing it here for the read/redirect path means the two checks can silently drift apart if one is updated and the other isn't.

Consider validating with the shared schema and just handling the parse failure:

♻️ Proposed refactor to reuse the shared validator
+import { hackathonPortalOriginSchema } from "`@forge/validators/hackathons`";
+
 export type ParticipantPortalPath =
   | "/apply"
   | "/dashboard"
   | "/dashboard/profile";

 export function buildParticipantPortalUrl(
   portalBaseUrl: string | null | undefined,
   path: ParticipantPortalPath,
 ) {
   if (!portalBaseUrl) return null;

-  try {
-    const baseUrl = new URL(portalBaseUrl);
-    if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") {
-      return null;
-    }
-    if (
-      baseUrl.username ||
-      baseUrl.password ||
-      baseUrl.pathname !== "/" ||
-      baseUrl.search ||
-      baseUrl.hash
-    ) {
-      return null;
-    }
-    return new URL(path, baseUrl).toString();
-  } catch {
-    return null;
-  }
+  const result = hackathonPortalOriginSchema.safeParse(portalBaseUrl);
+  if (!result.success) return null;
+  return new URL(path, result.data).toString();
 }

(Adjust the import path/export name to whatever packages/validators actually exposes.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blade/src/lib/hackathon-portal.ts` around lines 1 - 30, The URL
validation in buildParticipantPortalUrl duplicates the same bare-origin rule
already enforced by hackathonPortalOriginSchema, so refactor this helper to
reuse the shared validator instead of re-checking protocol, username/password,
pathname, search, and hash locally. Update buildParticipantPortalUrl in
hackathon-portal.ts to parse/validate portalBaseUrl through the shared schema
from packages/validators/src/hackathons.ts (or its exported equivalent), then
return null on parse failure and continue building the path URL only for valid
origins.
apps/bloomknights/src/app/(portal)/_components/auth-retry.tsx (1)

13-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider centralizing the BloomKnights color palette.

Hex literals like #FFFDF1, #42602A, #53634A, #f384d4 are hardcoded here and repeat verbatim across apply/page.tsx and dashboard/profile/page.tsx. Pulling these into a Tailwind theme (@theme in v4) or shared constants would avoid drift if the brand palette changes later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/bloomknights/src/app/`(portal)/_components/auth-retry.tsx around lines
13 - 26, The auth retry UI is using repeated hardcoded BloomKnights hex colors,
which should be centralized to avoid palette drift. Update the `AuthRetry`
component to reference shared brand color tokens instead of inline literals, and
reuse the same source used by `apply/page.tsx` and `dashboard/profile/page.tsx`
so the palette stays consistent. Prefer a Tailwind v4 `@theme` definition or a
shared constants module, then replace the existing color usages in `AuthRetry`
with those shared values.
apps/bloomknights/src/app/(portal)/_components/bloomknights-ambient-background.tsx (1)

7-97: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider consolidating inline <style> blocks used across portal components.

This component (and bloomknights-dashboard-shell.tsx, per the referenced snippet) each inject their own raw <style> tag with keyframes/pseudo-elements. Since Tailwind v4 supports CSS-first configuration (@theme, @utility), moving these decorative animations into a shared global stylesheet would reduce duplication and make it easier to reason about the full animation set for this portal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/bloomknights/src/app/`(portal)/_components/bloomknights-ambient-background.tsx
around lines 7 - 97, The portal components are duplicating raw inline animation
CSS in separate `<style>` tags, which makes the decorative styles harder to
maintain. Move the keyframes, pseudo-element rules, and reduced-motion handling
from `BloomknightsAmbientBackground` and the related portal shell component into
a shared global stylesheet or Tailwind v4 CSS-first definitions
(`@theme`/`@utility`), then keep the components only responsible for applying
the class names.
apps/bloomknights/src/app/(portal)/_components/portal-header.tsx (1)

21-28: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Sign-out failures fail silently.

void signOut(...) swallows any rejection with no feedback, unlike the toast-based error handling used consistently elsewhere in the portal (e.g., bloom-dashboard.tsx's confirm/withdraw/report-issue handlers). If sign-out fails, the user sees nothing and may believe they're logged out.

♻️ Proposed fix
       <Button
         type="button"
         variant="outline"
         className="rounded-full bg-white/85 text-[`#42602A`]"
-        onClick={() => void signOut({ redirectTo: "/" })}
+        onClick={() => {
+          void signOut({ redirectTo: "/" }).catch(() => {
+            toast.error("Could not sign out. Please try again.");
+          });
+        }}
       >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/bloomknights/src/app/`(portal)/_components/portal-header.tsx around
lines 21 - 28, The sign-out action in portal-header’s Button click handler
swallows failures by using a fire-and-forget signOut call with no user feedback.
Update the onClick handler around signOut to handle rejections explicitly, and
surface a toast-based error message consistent with the portal’s other handlers
in bloom-dashboard.tsx. Keep the existing Button and signOut usage, but wrap the
async sign-out flow so any failure is caught and reported to the user.
apps/bloomknights/src/app/(portal)/_components/bloom-dashboard.tsx (1)

220-233: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

"Confirmation closed" conflates two distinct reasons.

When atCapacity is true (capacity reached) vs confirmationClosed (deadline passed), the button shows the same "Confirmation closed" label. These are different situations for the user — a capacity cap is arguably actionable/explainable ("spots filled") while a deadline is not. Consider distinguishing the copy so waitlisted-by-capacity hackers understand why they can't confirm.

-              {confirmationClosed || atCapacity
-                ? "Confirmation closed"
-                : "Agree and confirm"}
+              {confirmationClosed
+                ? "Confirmation closed"
+                : atCapacity
+                  ? "Confirmation full"
+                  : "Agree and confirm"}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/bloomknights/src/app/`(portal)/_components/bloom-dashboard.tsx around
lines 220 - 233, The CTA label in bloom-dashboard’s Button currently uses the
same “Confirmation closed” copy for both confirmationClosed and atCapacity,
which hides the real reason. Update the conditional text in the Button render so
it distinguishes the two states separately, using the existing
confirmationClosed and atCapacity flags in bloom-dashboard’s confirm action
area. Keep the disabled behavior as-is, but change the displayed copy to reflect
whether the deadline passed or capacity was reached.
packages/hackathon/src/application-schema.ts (1)

71-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the Zod 4 email factory with pipe here

z.email() is the Zod 4 replacement, but keep the required check first so the empty-string message stays intact:

email: z.string().min(1, "Required").pipe(z.email({ error: "Invalid email" })),

Same change at line 159.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/hackathon/src/application-schema.ts` at line 71, The email
validation in application-schema should use the Zod 4 email factory with pipe so
the required-message behavior stays intact. Update the email field in the schema
definitions (including the one referenced by the ApplicationSchema and the
matching occurrence elsewhere) to keep the min(1, "Required") check first, then
pipe into z.email({ error: "Invalid email" }) instead of chaining
z.string().email(...).
packages/api/src/routers/participant-portal.ts (1)

530-546: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Double as unknown as cast bypasses the router's structural type checking.

createTRPCRouter(...) as unknown as TRPCBuiltRouter<...> hides any mismatch between the implementation object and ParticipantPortalRouterRecord. This is likely needed to give the exported const an explicit declared type (a common tRPC pattern), but per guidelines broad casts should be justified — a short comment explaining why the cast is required would help future maintainers trust it's intentional rather than a type-safety escape hatch.

The same pattern repeats in packages/api/src/participant.ts (lines 22-24).

As per coding guidelines, **/*.{ts,tsx,js,jsx}: "Do not bypass type or lint errors with any, broad casts, eslint-disable, or ignored promises unless justified".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/routers/participant-portal.ts` around lines 530 - 546, The
exported routers use a double `as unknown as` cast that bypasses structural type
checking and needs an explicit justification. In `participantPortalRouter` (and
the matching export in `participant.ts`), add a short inline comment explaining
why the cast to `TRPCBuiltRouter<...>` is required, so the intent is clear and
future readers know it is a deliberate tRPC typing pattern rather than an unsafe
escape hatch.

Source: Coding guidelines

packages/api/src/storage-env.ts (1)

1-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

no-restricted-properties disable doesn't match the "env.ts/.config." exclusion pattern.

This file directly reads process.env via createEnv, gated with /* eslint-disable no-restricted-properties */. As per coding guidelines, !(**/{env,*.config}.{js,ts,tsx}): "Flag any direct usage of process.env outside of env.ts config files and .config. files. The codebase uses validated env imports". storage-env.ts (and its siblings discord-env.ts, google-env.ts, stripe-env.ts from the same cohort) don't match either glob literal (env.ts or *.config.*), so per a strict reading, the lint rule's exception list should be updated to also cover this new *-env.ts naming convention rather than relying on a local eslint-disable in each new module. Also relevant per **/*.{ts,tsx,js,jsx}: "Do not bypass type or lint errors with ... eslint-disable ... unless justified" — this disable is reasonably justified (it is a validated env config module), but the underlying lint/config pattern should reflect that so future files don't need a per-file suppression.

♻️ Suggested ESLint config update (outside this file)
- restrictedProperties for files matching: env.{js,ts,tsx}, *.config.{js,ts,tsx}
+ restrictedProperties exception glob: **/{env,*-env,*.config}.{js,ts,tsx}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/storage-env.ts` around lines 1 - 14, The issue is that the
validated env module uses a local no-restricted-properties disable because the
lint exception pattern only covers env.ts and *.config.* files. Update the
ESLint rule/config exception to include the new *-env.ts naming convention used
by storageEnv and its sibling env modules (discordEnv, googleEnv, stripeEnv), so
these files can use process.env through createEnv without per-file suppressions.
Keep the existing storageEnv implementation unchanged and adjust the shared lint
rule instead of adding more eslint-disable comments.

Source: Coding guidelines

packages/utils/src/discord-env.ts (2)

4-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated skipValidation boilerplate into a shared helper.

The same createEnv({ ..., skipValidation: !!process.env.CI || process.env.npm_lifecycle_event === "lint" }) pattern is duplicated verbatim in google-env.ts and stripe-env.ts (and likely storage-env.ts). A shared helper keeps the CI/lint bypass logic in one place and reduces drift risk if the condition ever needs to change.

Also consider .min(1) on the token schema so an accidentally empty string doesn't silently pass validation.

♻️ Proposed shared helper
+// packages/utils/src/create-service-env.ts
+import { createEnv } from "`@t3-oss/env-core`";
+
+export const skipServiceEnvValidation =
+  !!process.env.CI || process.env.npm_lifecycle_event === "lint";
 import { createEnv } from "`@t3-oss/env-core`";
 import { z } from "zod";
+import { skipServiceEnvValidation } from "./create-service-env";

 export const discordEnv = createEnv({
-  server: { DISCORD_BOT_TOKEN: z.string() },
+  server: { DISCORD_BOT_TOKEN: z.string().min(1) },
   runtimeEnv: process.env,
-  skipValidation:
-    !!process.env.CI || process.env.npm_lifecycle_event === "lint",
+  skipValidation: skipServiceEnvValidation,
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utils/src/discord-env.ts` around lines 4 - 9, The `createEnv` setup
in `discordEnv` repeats the same `skipValidation` CI/lint condition used in
other env modules, so move that logic into a shared helper and reuse it from
`discordEnv` (and the matching `googleEnv`/`stripeEnv`/`storageEnv` callers) to
keep behavior centralized. Also tighten the `DISCORD_BOT_TOKEN` schema in
`createEnv` by using the token string validator with a non-empty constraint so
an empty value cannot pass validation.

4-9: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consolidate the Discord token env schema

packages/utils/src/env.ts still declares DISCORD_BOT_TOKEN, so packages/utils/src/env.ts and packages/utils/src/discord-env.ts both validate the same secret. Remove the duplicate or have packages/utils/src/discord.ts reuse the shared schema to keep boot-time validation from drifting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utils/src/discord-env.ts` around lines 4 - 9, The Discord bot token
is being validated twice, once in discordEnv and again in the shared env schema,
which can drift over time. Update packages/utils/src/discord-env.ts and
packages/utils/src/env.ts so DISCORD_BOT_TOKEN is declared in only one place,
then have packages/utils/src/discord.ts consume that shared source instead of
defining its own schema. Keep the runtime validation behavior the same while
consolidating the schema around the existing createEnv setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/bloomknights/src/app/_components/navbar/Navbar.tsx`:
- Around line 30-41: The signed-out auth links in Navbar use the same href for
both “Sign up” and “Log in”, which will collide with NavContent’s
key={link.href} rendering. Update the authLinks entries in Navbar so each item
has a unique href or otherwise provides distinct key material, and keep the
existing NavContent map keyed by link.href working without duplicate React keys.

In `@apps/bloomknights/src/app/`(portal)/_components/bloom-dashboard.tsx:
- Around line 271-306: The QR dialog flow in bloom-dashboard’s
Dialog/DialogContent path only handles qrCode vs loading, so a failed
loadQRCode() leaves the Loader2 spinner visible forever. Update the QR modal
state handling around qrMutation in bloom-dashboard to render an explicit error
state when qrMutation.isError is true, and provide a retry action that calls
loadQRCode() again; make sure the existing qrCode and loading branches still
work correctly.

In `@apps/bloomknights/src/app/api/trpc/`[trpc]/route.ts:
- Around line 9-16: The current MAX_REQUEST_SIZE check in handler only trusts
the Content-Length header, so requests without it or with a spoofed value can
bypass the limit. Update the route-level enforcement in handler to validate the
actual request body size before passing the request into the tRPC handler,
and/or add a real server-side body size cap in next.config.js so oversized
uploads are rejected even when Content-Length is missing or incorrect. Use the
existing handler and MAX_REQUEST_SIZE symbols to locate the fix.

In `@packages/api/src/routers/participant-portal.ts`:
- Around line 408-439: `withdrawAttendance` has a TOCTOU race because it checks
`participant.status` in a અલગ read but the `db.update(HackerAttendee)` call does
not re-assert that status in its `where` clause. Update the `withdrawAttendance`
mutation to make the write conditional on the current status (using the same
`HackerAttendee` row keyed by `hackerId` and `hackathonId`, plus a `status =
"confirmed"` predicate, or equivalent transactional locking/re-check) so a
concurrent change cannot still withdraw a non-confirmed attendee.
- Around line 88-90: The getHackathon query in participant-portal is returning
the full SelectHackathon record instead of a public-safe DTO. Update the
publicProcedure resolver to map the requireHackathon result to an explicit
public projection, excluding internal fields like applicationBackgroundKey,
emailTemplateKey, portalBaseUrl, and confirmationCapacity. Then adjust
ParticipantPortalContract.getHackathon to use the same narrowed shape so the API
contract matches the sanitized response.

In `@packages/api/src/trpc.ts`:
- Around line 41-42: Remove the unconditional debug logging in tRPC request
handling: the `console.log` in `createCaller`/request setup that prints `source`
and `session?.user` should not run in production. Delete the log entirely, or if
request tracing is still needed, replace it with a redacted log that does not
include the full user object or any PII and is gated behind an explicit debug
flag.

In `@packages/auth/src/factory.ts`:
- Line 9: Update the discord import in the auth factory to use the package
entrypoint from `@forge/utils` instead of reaching into ../../utils/src/discord,
and add `@forge/utils` as an explicit dependency in packages/auth/package.json.
Use the existing factory.ts import site and the discord symbol to locate the
change, and make sure the package boundary is respected everywhere this import
is used.

In `@packages/hackathon/src/application-schema.ts`:
- Around line 213-214: The two agreement refinements in application-schema’s
profile schema currently use z.boolean().refine(...) without user-facing
messages, so they fall back to a generic validation error. Update the
refinements on agreesToMLHCodeOfConduct and agreesToMLHDataSharing to include
explicit message text, matching the style used elsewhere in applicationSchema so
users see a clear explanation when either checkbox is unchecked.

In `@packages/hackathon/src/server.ts`:
- Around line 7-15: The createHackathonPortalServerCaller helper is passing a
thunk into createParticipantCaller instead of the resolved TRPC context, which
causes participantRouter.createCaller to receive a function rather than the
context object. Update createHackathonPortalServerCaller to construct the
context with createTRPCContext and pass that context value directly through
createParticipantCaller so participant procedures can access ctx.session and
ctx.token correctly.

In `@packages/validators/src/hacker.ts`:
- Around line 32-33: The Hacker schema currently treats agreesToMLHCodeOfConduct
and agreesToMLHDataSharing as plain booleans, so false is accepted as valid
input. Update the validation in the hacker schema to require explicit acceptance
for both fields, using the existing Zod schema for this object so that only true
passes and false is rejected.

---

Outside diff comments:
In `@apps/bloomknights/src/app/`(portal)/_components/hacker-profile-form.tsx:
- Around line 93-126: Normalize the hacker profile date fields before calling
form.reset in hacker-profile-form.tsx: the useEffect that resets the form
currently passes hacker.dob and hacker.gradDate through unchanged, which can
break <input type="date"> display when they are Date objects or ISO strings. Add
a small formatter helper near the HackerProfileForm reset logic and use it for
dob and gradDate so the values are always YYYY-MM-DD before resetting the form.

---

Nitpick comments:
In `@apps/blade/src/app/_components/admin/hackathon/manage/hackathon-manager.tsx`:
- Around line 132-146: Only the first validation message is being forwarded for
each field in hackathon-manager.tsx, so multi-issue validations from
portalResult and capacityResult lose useful feedback. Update the portalBaseUrl
and confirmationCapacity handling in the validation block to collect and surface
all messages from each result’s issues array (for example by combining them into
one string or adding separate issues), instead of reading only
error.issues[0].message. Use the existing portalResult, capacityResult, and
ctx.addIssue locations to keep the fix local.

In `@apps/blade/src/app/_components/settings/sidebar-nav.tsx`:
- Around line 27-33: The loading branch in sidebar-nav.tsx does not announce its
state to assistive tech, unlike the error state that already uses aria-live.
Update the memberLoading return block in the SidebarNav component to add
appropriate ARIA live/status semantics (for example on the wrapper around the
Loader2 spinner) so screen readers are notified that loading is in progress.

In `@apps/blade/src/lib/hackathon-portal.ts`:
- Around line 1-30: The URL validation in buildParticipantPortalUrl duplicates
the same bare-origin rule already enforced by hackathonPortalOriginSchema, so
refactor this helper to reuse the shared validator instead of re-checking
protocol, username/password, pathname, search, and hash locally. Update
buildParticipantPortalUrl in hackathon-portal.ts to parse/validate portalBaseUrl
through the shared schema from packages/validators/src/hackathons.ts (or its
exported equivalent), then return null on parse failure and continue building
the path URL only for valid origins.

In `@apps/bloomknights/src/app/`(portal)/_components/auth-retry.tsx:
- Around line 13-26: The auth retry UI is using repeated hardcoded BloomKnights
hex colors, which should be centralized to avoid palette drift. Update the
`AuthRetry` component to reference shared brand color tokens instead of inline
literals, and reuse the same source used by `apply/page.tsx` and
`dashboard/profile/page.tsx` so the palette stays consistent. Prefer a Tailwind
v4 `@theme` definition or a shared constants module, then replace the existing
color usages in `AuthRetry` with those shared values.

In `@apps/bloomknights/src/app/`(portal)/_components/bloom-dashboard.tsx:
- Around line 220-233: The CTA label in bloom-dashboard’s Button currently uses
the same “Confirmation closed” copy for both confirmationClosed and atCapacity,
which hides the real reason. Update the conditional text in the Button render so
it distinguishes the two states separately, using the existing
confirmationClosed and atCapacity flags in bloom-dashboard’s confirm action
area. Keep the disabled behavior as-is, but change the displayed copy to reflect
whether the deadline passed or capacity was reached.

In
`@apps/bloomknights/src/app/`(portal)/_components/bloomknights-ambient-background.tsx:
- Around line 7-97: The portal components are duplicating raw inline animation
CSS in separate `<style>` tags, which makes the decorative styles harder to
maintain. Move the keyframes, pseudo-element rules, and reduced-motion handling
from `BloomknightsAmbientBackground` and the related portal shell component into
a shared global stylesheet or Tailwind v4 CSS-first definitions
(`@theme`/`@utility`), then keep the components only responsible for applying
the class names.

In `@apps/bloomknights/src/app/`(portal)/_components/portal-header.tsx:
- Around line 21-28: The sign-out action in portal-header’s Button click handler
swallows failures by using a fire-and-forget signOut call with no user feedback.
Update the onClick handler around signOut to handle rejections explicitly, and
surface a toast-based error message consistent with the portal’s other handlers
in bloom-dashboard.tsx. Keep the existing Button and signOut usage, but wrap the
async sign-out flow so any failure is caught and reported to the user.

In `@packages/api/src/routers/participant-portal.ts`:
- Around line 530-546: The exported routers use a double `as unknown as` cast
that bypasses structural type checking and needs an explicit justification. In
`participantPortalRouter` (and the matching export in `participant.ts`), add a
short inline comment explaining why the cast to `TRPCBuiltRouter<...>` is
required, so the intent is clear and future readers know it is a deliberate tRPC
typing pattern rather than an unsafe escape hatch.

In `@packages/api/src/storage-env.ts`:
- Around line 1-14: The issue is that the validated env module uses a local
no-restricted-properties disable because the lint exception pattern only covers
env.ts and *.config.* files. Update the ESLint rule/config exception to include
the new *-env.ts naming convention used by storageEnv and its sibling env
modules (discordEnv, googleEnv, stripeEnv), so these files can use process.env
through createEnv without per-file suppressions. Keep the existing storageEnv
implementation unchanged and adjust the shared lint rule instead of adding more
eslint-disable comments.

In `@packages/auth/src/factory.ts`:
- Around line 54-61: The synthesized email in createForgeAuth is hardcoded to
`@blade.org`, which breaks reuse in other apps like BloomKnights. Update
ForgeAuthOptions to accept an email domain (or equivalent configurable value)
and use that inside mapProfileToUser instead of the literal domain, while
keeping the identifier based on profile.id so uniqueness remains unchanged.

In `@packages/hackathon/src/application-schema.ts`:
- Line 71: The email validation in application-schema should use the Zod 4 email
factory with pipe so the required-message behavior stays intact. Update the
email field in the schema definitions (including the one referenced by the
ApplicationSchema and the matching occurrence elsewhere) to keep the min(1,
"Required") check first, then pipe into z.email({ error: "Invalid email" })
instead of chaining z.string().email(...).

In `@packages/utils/src/discord-env.ts`:
- Around line 4-9: The `createEnv` setup in `discordEnv` repeats the same
`skipValidation` CI/lint condition used in other env modules, so move that logic
into a shared helper and reuse it from `discordEnv` (and the matching
`googleEnv`/`stripeEnv`/`storageEnv` callers) to keep behavior centralized. Also
tighten the `DISCORD_BOT_TOKEN` schema in `createEnv` by using the token string
validator with a non-empty constraint so an empty value cannot pass validation.
- Around line 4-9: The Discord bot token is being validated twice, once in
discordEnv and again in the shared env schema, which can drift over time. Update
packages/utils/src/discord-env.ts and packages/utils/src/env.ts so
DISCORD_BOT_TOKEN is declared in only one place, then have
packages/utils/src/discord.ts consume that shared source instead of defining its
own schema. Keep the runtime validation behavior the same while consolidating
the schema around the existing createEnv setup.

In `@packages/validators/src/hackathons.ts`:
- Around line 26-53: The hackathon portal schema uses the deprecated chainable
string URL validator in hackathonPortalOriginSchema. Update the schema to use
the top-level z.url() validator instead of z.string().url(), while keeping the
existing trim, superRefine, and transform behavior intact so the validation
logic and output origin normalization remain the same.
- Around line 61-69: The whitespace-only input edge case in
hackathonConfirmationCapacitySchema should be treated as blank instead of
coercing to 0. Update the preprocess step in hackathonConfirmationCapacitySchema
to normalize strings containing only whitespace to null before z.coerce.number()
runs, alongside the existing empty/undefined/null handling. Use the
hackathonConfirmationCapacitySchema symbol to locate the validator and keep the
rest of the number validation unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 6aa5b2f3-be81-4c10-a4f7-45a8b8b49903

📥 Commits

Reviewing files that changed from the base of the PR and between 59c629a and 412febe.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (114)
  • .env.example
  • apps/blade/src/app/_components/admin/hackathon/manage/hackathon-manager.tsx
  • apps/blade/src/app/_components/dashboard/dashboard-entry-dialogs.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/countdown.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx
  • apps/blade/src/app/_components/dashboard/hacker-dashboard/confirm-button.tsx
  • apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx
  • apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx
  • apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-qr-button.tsx
  • apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-resume-button.tsx
  • apps/blade/src/app/_components/dashboard/hacker-dashboard/past-hackathons.tsx
  • apps/blade/src/app/_components/dashboard/hacker/hackbackgrounds/khix.ts
  • apps/blade/src/app/_components/hackathon/portal-unavailable.tsx
  • apps/blade/src/app/_components/option-cards.tsx
  • apps/blade/src/app/_components/providers.tsx
  • apps/blade/src/app/_components/settings/delete-hacker-button.tsx
  • apps/blade/src/app/_components/settings/sidebar-nav.tsx
  • apps/blade/src/app/_components/theme-toggle-route-guard.tsx
  • apps/blade/src/app/dashboard/page.tsx
  • apps/blade/src/app/hackathon/README.md
  • apps/blade/src/app/hackathon/[slug]/page.tsx
  • apps/blade/src/app/hackathon/bloomknights/page.tsx
  • apps/blade/src/app/hackathon/page.tsx
  • apps/blade/src/app/hacker/application/[hackathon-id]/page.tsx
  • apps/blade/src/app/settings/hacker-profile/page.tsx
  • apps/blade/src/app/settings/page.tsx
  • apps/blade/src/consts/index.ts
  • apps/blade/src/lib/hackathon-portal.ts
  • apps/bloomknights/next.config.js
  • apps/bloomknights/package.json
  • apps/bloomknights/src/app/(marketing)/layout.tsx
  • apps/bloomknights/src/app/(marketing)/page.tsx
  • apps/bloomknights/src/app/(portal)/_components/application/hackbackgrounds/bloomknights.ts
  • apps/bloomknights/src/app/(portal)/_components/application/hackbackgrounds/index.ts
  • apps/bloomknights/src/app/(portal)/_components/application/hackbackgrounds/types.ts
  • apps/bloomknights/src/app/(portal)/_components/application/hacker-application-background.tsx
  • apps/bloomknights/src/app/(portal)/_components/application/hacker-application-form.tsx
  • apps/bloomknights/src/app/(portal)/_components/auth-retry.tsx
  • apps/bloomknights/src/app/(portal)/_components/bloom-dashboard.tsx
  • apps/bloomknights/src/app/(portal)/_components/bloomknights-action-blooms.tsx
  • apps/bloomknights/src/app/(portal)/_components/bloomknights-ambient-background.tsx
  • apps/bloomknights/src/app/(portal)/_components/bloomknights-dashboard-logo.tsx
  • apps/bloomknights/src/app/(portal)/_components/bloomknights-dashboard-shell.tsx
  • apps/bloomknights/src/app/(portal)/_components/bloomknights-flower-cursor.tsx
  • apps/bloomknights/src/app/(portal)/_components/hacker-profile-form.tsx
  • apps/bloomknights/src/app/(portal)/_components/portal-header.tsx
  • apps/bloomknights/src/app/(portal)/apply/page.tsx
  • apps/bloomknights/src/app/(portal)/dashboard/page.tsx
  • apps/bloomknights/src/app/(portal)/dashboard/profile/page.tsx
  • apps/bloomknights/src/app/(portal)/hacker/application/bloomknights/page.tsx
  • apps/bloomknights/src/app/(portal)/layout.tsx
  • apps/bloomknights/src/app/_components/navbar/NavContent.tsx
  • apps/bloomknights/src/app/_components/navbar/Navbar.tsx
  • apps/bloomknights/src/app/_components/register/registerButton.tsx
  • apps/bloomknights/src/app/api/auth/[...all]/route.ts
  • apps/bloomknights/src/app/api/auth/signin/route.ts
  • apps/bloomknights/src/app/api/trpc/[trpc]/route.ts
  • apps/bloomknights/src/app/layout.tsx
  • apps/bloomknights/src/auth/client.ts
  • apps/bloomknights/src/auth/server.ts
  • apps/bloomknights/src/env.ts
  • apps/bloomknights/src/lib/portal-config.ts
  • docs/API-AND-PERMISSIONS.md
  • docs/ARCHITECTURE.md
  • packages/api/package.json
  • packages/api/src/minio/minio-client.ts
  • packages/api/src/participant-contract.ts
  • packages/api/src/participant.ts
  • packages/api/src/resume-storage.ts
  • packages/api/src/routers/hackathon.ts
  • packages/api/src/routers/hackers/mutations.ts
  • packages/api/src/routers/participant-portal.ts
  • packages/api/src/storage-env.ts
  • packages/api/src/trpc.ts
  • packages/auth/package.json
  • packages/auth/src/callback-url.ts
  • packages/auth/src/client-factory.ts
  • packages/auth/src/config.ts
  • packages/auth/src/env.ts
  • packages/auth/src/factory.ts
  • packages/auth/src/index.rsc.ts
  • packages/auth/src/index.ts
  • packages/auth/src/server-factory.ts
  • packages/auth/src/shared-env.ts
  • packages/db/drizzle/0010_wooden_supreme_intelligence.sql
  • packages/db/drizzle/meta/0010_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/schemas/knight-hacks.ts
  • packages/hackathon/README.md
  • packages/hackathon/eslint.config.js
  • packages/hackathon/package.json
  • packages/hackathon/src/application-schema.ts
  • packages/hackathon/src/client.tsx
  • packages/hackathon/src/config.ts
  • packages/hackathon/src/index.ts
  • packages/hackathon/src/lifecycle.ts
  • packages/hackathon/src/query-client.ts
  • packages/hackathon/src/server.ts
  • packages/hackathon/src/types.ts
  • packages/hackathon/tsconfig.json
  • packages/utils/src/discord-env.ts
  • packages/utils/src/discord.ts
  • packages/utils/src/google-env.ts
  • packages/utils/src/google.ts
  • packages/utils/src/stripe-env.ts
  • packages/utils/src/stripe.ts
  • packages/validators/package.json
  • packages/validators/src/hackathons.ts
  • packages/validators/src/hacker.ts
  • packages/validators/src/index.ts
  • turbo.json
💤 Files with no reviewable changes (19)
  • apps/blade/src/app/_components/dashboard/hacker-dashboard/confirm-button.tsx
  • apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-qr-button.tsx
  • apps/blade/src/consts/index.ts
  • apps/blade/src/app/_components/dashboard/hacker-dashboard/past-hackathons.tsx
  • apps/blade/src/app/_components/theme-toggle-route-guard.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/issue-dialog.tsx
  • apps/blade/src/app/_components/dashboard/hacker/hackbackgrounds/khix.ts
  • apps/blade/src/app/settings/page.tsx
  • apps/blade/src/app/_components/settings/delete-hacker-button.tsx
  • apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-dashboard.tsx
  • apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-data.tsx
  • apps/blade/src/app/_components/dashboard/dashboard-entry-dialogs.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/countdown.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/hackathon-data.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/upcoming-events.tsx
  • apps/blade/src/app/_components/dashboard/hacker-dashboard/hacker-resume-button.tsx
  • apps/blade/src/app/_components/dashboard/hackathon-dashboard/components.tsx
  • apps/blade/src/app/hackathon/bloomknights/page.tsx
  • packages/auth/src/config.ts

Comment thread apps/bloomknights/src/app/_components/navbar/Navbar.tsx
Comment thread apps/bloomknights/src/app/(portal)/_components/bloom-dashboard.tsx Outdated
Comment thread apps/bloomknights/src/app/api/trpc/[trpc]/route.ts
Comment thread packages/api/src/routers/participant-portal.ts Outdated
Comment thread packages/api/src/routers/participant-portal.ts
Comment thread packages/api/src/trpc.ts Outdated
Comment thread packages/auth/src/factory.ts Outdated
Comment thread packages/hackathon/src/application-schema.ts Outdated
Comment thread packages/hackathon/src/server.ts
Comment thread packages/validators/src/hacker.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/blade/src/env.client.ts (1)

8-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate schema for NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY.

This key is now validated here AND still in apps/blade/src/env.ts's client section (with matching runtimeEnv mapping). Since checkout-form.tsx was updated to consume clientEnv instead of env, the copy in env.ts looks like leftover duplication — two sources of truth for the same variable increase drift risk if one gets updated without the other.

If nothing else still reads env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, remove it from apps/blade/src/env.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blade/src/env.client.ts` at line 8, The Stripe publishable key is being
validated in two places, creating duplicate sources of truth between env.client
and env.ts. Remove NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY from the client schema in
env.ts if nothing else still reads env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, and
keep the single validation in env.client so checkout-form.tsx continues using
clientEnv consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/blade/src/app/auth/bloom-return/route.ts`:
- Around line 6-16: The origin allowlist in getBloomReturnURL currently always
includes the localhost origin via ALLOWED_BLOOM_RETURN_ORIGINS, which creates an
open-redirect path in production. Update the allowlist construction so the
localhost entry is only added in local/development environments, while keeping
configuredBloomOrigin and the production Bloom origin allowed as-is. Use the
existing getBloomReturnURL and ALLOWED_BLOOM_RETURN_ORIGINS symbols to locate
the logic and gate the localhost origin behind an environment check before
building allowedOrigins.

In `@apps/blade/src/env.client.ts`:
- Line 7: Update the env schema in env.client.ts so NEXT_PUBLIC_BLADE_URL is not
silently defaulted to localhost in production; instead, make it required when
running in production and keep the localhost fallback only for local
development. Adjust the zod definition around NEXT_PUBLIC_BLADE_URL and any
related env validation helper so missing deployment configuration fails fast
before getBaseUrl() and tRPC self-fetch are used.

---

Nitpick comments:
In `@apps/blade/src/env.client.ts`:
- Line 8: The Stripe publishable key is being validated in two places, creating
duplicate sources of truth between env.client and env.ts. Remove
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY from the client schema in env.ts if nothing
else still reads env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, and keep the single
validation in env.client so checkout-form.tsx continues using clientEnv
consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: cedbe889-5b7f-4165-a502-953c3bad2da0

📥 Commits

Reviewing files that changed from the base of the PR and between 412febe and 8bea5a6.

⛔ Files ignored due to path filters (1)
  • apps/bloomknights/public/knighthacks.svg is excluded by !**/*.svg
📒 Files selected for processing (30)
  • .env.example
  • apps/blade/src/app/_components/dashboard/member/checkout-form.tsx
  • apps/blade/src/app/auth/bloom-return/route.ts
  • apps/blade/src/env.client.ts
  • apps/blade/src/env.ts
  • apps/blade/src/trpc/react.tsx
  • apps/bloomknights/src/app/(marketing)/home-page-client.tsx
  • apps/bloomknights/src/app/(marketing)/page.tsx
  • apps/bloomknights/src/app/(portal)/_components/application/hacker-application-form.tsx
  • apps/bloomknights/src/app/(portal)/_components/auth-retry.tsx
  • apps/bloomknights/src/app/(portal)/_components/bloom-dashboard.tsx
  • apps/bloomknights/src/app/(portal)/_components/bloomknights-dashboard-logo.tsx
  • apps/bloomknights/src/app/(portal)/_components/bloomknights-dashboard-shell.tsx
  • apps/bloomknights/src/app/(portal)/_components/hacker-profile-form.tsx
  • apps/bloomknights/src/app/(portal)/_components/portal-header.tsx
  • apps/bloomknights/src/app/(portal)/apply/page.tsx
  • apps/bloomknights/src/app/(portal)/dashboard/layout.tsx
  • apps/bloomknights/src/app/(portal)/dashboard/page.tsx
  • apps/bloomknights/src/app/(portal)/dashboard/profile/page.tsx
  • apps/bloomknights/src/app/_components/navbar/NavContent.tsx
  • apps/bloomknights/src/app/_components/navbar/Navbar.tsx
  • apps/bloomknights/src/app/_components/register/registerButton.tsx
  • apps/bloomknights/src/app/api/auth/signin/route.ts
  • apps/bloomknights/src/app/globals.css
  • apps/bloomknights/src/app/layout.tsx
  • apps/bloomknights/src/env.ts
  • packages/auth/package.json
  • packages/hackathon/README.md
  • packages/hackathon/src/application-schema.ts
  • turbo.json
💤 Files with no reviewable changes (1)
  • apps/bloomknights/src/app/_components/navbar/Navbar.tsx
✅ Files skipped from review due to trivial changes (2)
  • apps/bloomknights/src/app/(portal)/dashboard/layout.tsx
  • packages/hackathon/README.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/bloomknights/src/app/(portal)/_components/bloomknights-dashboard-logo.tsx
  • turbo.json
  • apps/bloomknights/src/app/(portal)/_components/auth-retry.tsx
  • apps/bloomknights/src/app/layout.tsx
  • apps/bloomknights/src/app/_components/navbar/NavContent.tsx
  • packages/hackathon/src/application-schema.ts
  • apps/bloomknights/src/app/(portal)/_components/hacker-profile-form.tsx

Comment thread apps/blade/src/app/auth/bloom-return/route.ts Outdated
Comment thread apps/blade/src/env.client.ts Outdated

@Adr1an04 Adr1an04 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

viewed all tested and verified safety

@Adr1an04 Adr1an04 enabled auto-merge July 4, 2026 01:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
apps/blade/src/env.ts (1)

8-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider migrating to Zod 4's top-level z.url().

z.string().url() is deprecated in Zod 4 in favor of the top-level z.url(); the chained form still works but will be removed in a future major version.

♻️ Optional migration
 const bloomKnightsUrlSchema =
   process.env.NODE_ENV === "production"
-    ? z.string().url()
-    : z.string().url().default("http://localhost:3006");
+    ? z.url()
+    : z.url().default("http://localhost:3006");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blade/src/env.ts` around lines 8 - 12, The URL schema in
bloomKnightsUrlSchema uses the deprecated chained form from Zod 4. Update the
schema to use the top-level z.url() while preserving the existing production and
non-production behavior, and keep the default localhost value on the
non-production branch. Locate and adjust the schema definition in env.ts so
future Zod upgrades don’t break it.
apps/bloomknights/src/app/api/trpc/[trpc]/route.ts (2)

29-55: 🧹 Nitpick | 🔵 Trivial

Defense-in-depth: consider a platform/proxy-level body-size cap too.

The in-handler streaming limit is a solid application-level fix, but it doesn't stop the underlying Node/edge runtime (or a fronting proxy/CDN) from buffering the same oversized payload before your code ever runs. Consider adding a platform-level max body size (reverse proxy, WAF, or hosting-provider setting) as defense-in-depth against large uploads consuming resources upstream of this handler.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/bloomknights/src/app/api/trpc/`[trpc]/route.ts around lines 29 - 55, The
streaming guard in readRequestWithLimit is good, but oversized bodies can still
be buffered before this handler runs. Keep the existing MAX_REQUEST_SIZE
enforcement in route.ts, and also add a platform/proxy-level request body cap
(for example at the reverse proxy, WAF, or hosting provider) so the
trpc/[trpc]/route handler is protected upstream as defense-in-depth.

57-66: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Past content-length bypass issue is resolved.

The new readRequestWithLimit streaming check enforces the real 8MB cap on actual bytes read, closing the gap where a missing/spoofed content-length header could bypass the earlier soft check. Good fix.

One minor point: validateToken() runs only after the full (up to 8MB) body has been buffered. Since it doesn't depend on the request body, calling it earlier (in parallel with, or before, readRequestWithLimit) would avoid wasting memory/CPU buffering large payloads from unauthenticated callers before rejecting them.

♻️ Optional reordering
 const handler = async (req: Request) => {
   const contentLength = Number(req.headers.get("content-length") ?? 0);
   if (contentLength > MAX_REQUEST_SIZE) {
     return requestTooLargeResponse();
   }
 
-  const limitedRequest = await readRequestWithLimit(req);
+  const [session, limitedRequest] = await Promise.all([
+    validateToken(),
+    readRequestWithLimit(req),
+  ]);
   if (!limitedRequest) return requestTooLargeResponse();
 
-  const session = await validateToken();
   return fetchRequestHandler({
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/bloomknights/src/app/api/trpc/`[trpc]/route.ts around lines 57 - 66,
Move the `validateToken()` call in the `handler` for the TRPC route to run
before, or in parallel with, `readRequestWithLimit` so unauthenticated requests
are rejected without first buffering up to 8MB of body data. Keep the existing
`requestTooLargeResponse` behavior and the streaming size cap logic intact, but
reorder the authentication check and body القراءة in `handler` to avoid
unnecessary memory/CPU work.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/blade/src/env.ts`:
- Around line 8-12: The URL schema in bloomKnightsUrlSchema uses the deprecated
chained form from Zod 4. Update the schema to use the top-level z.url() while
preserving the existing production and non-production behavior, and keep the
default localhost value on the non-production branch. Locate and adjust the
schema definition in env.ts so future Zod upgrades don’t break it.

In `@apps/bloomknights/src/app/api/trpc/`[trpc]/route.ts:
- Around line 29-55: The streaming guard in readRequestWithLimit is good, but
oversized bodies can still be buffered before this handler runs. Keep the
existing MAX_REQUEST_SIZE enforcement in route.ts, and also add a
platform/proxy-level request body cap (for example at the reverse proxy, WAF, or
hosting provider) so the trpc/[trpc]/route handler is protected upstream as
defense-in-depth.
- Around line 57-66: Move the `validateToken()` call in the `handler` for the
TRPC route to run before, or in parallel with, `readRequestWithLimit` so
unauthenticated requests are rejected without first buffering up to 8MB of body
data. Keep the existing `requestTooLargeResponse` behavior and the streaming
size cap logic intact, but reorder the authentication check and body القراءة in
`handler` to avoid unnecessary memory/CPU work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ca30f677-b456-41aa-895e-a788bfc96770

📥 Commits

Reviewing files that changed from the base of the PR and between 8bea5a6 and be55d03.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (15)
  • apps/blade/src/app/auth/bloom-return/route.ts
  • apps/blade/src/env.client.ts
  • apps/blade/src/env.ts
  • apps/bloomknights/src/app/(portal)/_components/bloom-dashboard.tsx
  • apps/bloomknights/src/app/api/trpc/[trpc]/route.ts
  • apps/bloomknights/src/env.ts
  • packages/api/src/participant-contract.ts
  • packages/api/src/routers/participant-portal.ts
  • packages/api/src/trpc.ts
  • packages/auth/package.json
  • packages/auth/src/factory.ts
  • packages/hackathon/src/application-schema.ts
  • packages/utils/package.json
  • packages/utils/src/discord.ts
  • packages/validators/src/hacker.ts
💤 Files with no reviewable changes (1)
  • packages/utils/package.json
🚧 Files skipped from review as they are similar to previous changes (11)
  • apps/blade/src/env.client.ts
  • apps/bloomknights/src/env.ts
  • packages/auth/src/factory.ts
  • packages/auth/package.json
  • apps/blade/src/app/auth/bloom-return/route.ts
  • packages/api/src/trpc.ts
  • packages/validators/src/hacker.ts
  • packages/api/src/participant-contract.ts
  • packages/api/src/routers/participant-portal.ts
  • packages/hackathon/src/application-schema.ts
  • apps/bloomknights/src/app/(portal)/_components/bloom-dashboard.tsx

@Adr1an04 Adr1an04 added this pull request to the merge queue Jul 4, 2026
Merged via the queue into main with commit afd2f89 Jul 4, 2026
11 checks passed
@Adr1an04 Adr1an04 deleted the repo/hackathon-portals branch July 4, 2026 01:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API Change modifies code in the global API/tRPC package Blade Change modifies code in Blade app Database Change modifies code in the DB package Feature New Feature or Request Global Change modifies code for the entire repository Hack Sites Change modifies code in a Hackathon app (ex. 2025) Major Big change - 2+ reviewers required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Move Bloom participant flows out of Blade

2 participants