Skip to content

[refactor] add Sign In pages for multiple OAuth platforms#36

Open
TechQuery wants to merge 18 commits into
mainfrom
connect-new-API
Open

[refactor] add Sign In pages for multiple OAuth platforms#36
TechQuery wants to merge 18 commits into
mainfrom
connect-new-API

Conversation

@TechQuery
Copy link
Copy Markdown
Member

@TechQuery TechQuery commented May 3, 2026

PR-36 PR-36 PR-36 Powered by Pull Request Badge

Summary by CodeRabbit

  • Chores

    • Linting: unsafe HTML now emits warnings instead of errors.
  • Style

    • Homepage stats list styling refined.
  • New Features

    • "Fill with default questions" button in questionnaire editor (with translations).
    • Added "LinkedIn or CV" question option.
    • New OAuth sign-in flows: GitHub and CNB (personal access token page + server-side validation).
  • Bug Fixes / Improvements

    • Activity tabs and detail rendering reorganized for consistency.
    • Enrollment now uses POST.
    • User activity lists load per-tab.
    • Award labels/selection handle missing targets safely.

Co-authored-by: Copilot <copilot@github.com>
Copilot AI review requested due to automatic review settings May 3, 2026 23:09
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a UserActivityModel and optional ActivityList store prop, switches enrollment to POST, migrates many runtime types to type-only imports, adds "Fill with default questions" (UI + translations), introduces CNB/GitHub sign-in pages, rewrites sessionGuard to require JWT, and updates lint rules.

Changes

User Activity Model & ActivityList wiring

Layer / File(s) Summary
Data Shape / Imports
models/Activity/index.ts, models/User/index.ts
Adds buildURLData import; getUserTopList() uses 'activity-log/user-rank' literal.
Core Model
models/Activity/index.ts
Adds exported UserActivityModel extends ActivityModel with constructor(userId, role) and loadPage(pageIndex, pageSize, filter); changes ActivityModel.signOne to POST to ${baseURI}/${name}/enrollment.
Component Props / Wiring
components/Activity/ActivityList.tsx, pages/user/[id].tsx
ActivityListLayoutProps gains store?: ActivityModel; ActivityList.store initializes from `props.store
Page Markup / Minor UX
pages/activity/[name]/index.tsx, pages/index.tsx
Moves "award" and conditional "team-rank" tabs into <Tabs>; detail wrapped in <article dangerouslySetInnerHTML={...} />; homepage hero <ul> adds list-unstyled.
Config / Linting
biome.json
Changes style.useTemplate severity to warn; adds linter.rules.security.noDangerouslySetInnerHtml: "warn".

External types, awards, evaluation, questionnaire defaults

Layer / File(s) Summary
Types / Imports
models/Activity/Award.ts, models/Activity/Question.ts, models/Activity/Team.ts, components/...
Replaces several local runtime type declarations with type-only imports from @freecodecamp-chengdu/hop-service (Award, AwardAssignment, Question, QuestionType, Answer, and related types).
Model Cleanup
models/Activity/Award.ts, models/Activity/Team.ts
Removes local Award/AwardAssignment interfaces and redundant runtime imports; code relies on external types.
Award UI & Evaluation Flow
components/Activity/AwardList.tsx, components/Team/TeamAwardAssignment.tsx, pages/activity/[name]/manage/evaluation.tsx
Guards Award target rendering with fallback; simplifies TeamAwardAssignment mapping; evaluation submit uses teamStore and awardStore, renames form field awardIdaward.
Questionnaire defaults & wiring
models/Activity/Question.ts, pages/activity/[name]/manage/questionnaire.tsx, components/Activity/Questionnaire*
Adds linkein_or_cv question; imports questions helper; adds fillWithDefaults() on ActivityQuestionnaireEditor and a fill_default_questions button; switches several components to type-only Question imports and uses SubmitEvent typing.
Pages / Registration
pages/activity/[name]/register.tsx
Converts Answer/Question imports to type-only, adds QuestionnaireForm, PageHead, and activity store imports.
Translations
translation/en-US.ts, translation/zh-CN.ts, translation/zh-TW.ts
Adds fill_default_questions and related CN/TW keys (including personal_access_token, generate_token, and sign_in_with(platform)).

Sequence Diagram

sequenceDiagram
    participant Page as User Page
    participant UEM as UserActivityModel
    participant AL as ActivityList
    participant API as Activity Service

    Page->>UEM: instantiate UserActivityModel(userId, role)
    Page->>AL: render ActivityList with store={UEM}
    AL->>AL: initialize store = props.store || new ActivityModel()
    AL->>UEM: request loadPage(pageIndex, pageSize, filter)
    UEM->>API: GET /user/{userId}/hackathon/{role}?page...
    API-->>UEM: returns { list, count }
    UEM-->>AL: provides page data
    AL->>AL: render activities
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through models, left seeds to sow,
Role-based lists now fetch and grow,
Awards and questions tidied with care,
Lint whispers warnings for HTML there,
A little rabbit cheers the code's new glow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding Sign In pages for multiple OAuth platforms, which aligns with the introduction of new OAuth routes (GitHub and CNB), refactored sessionGuard, and related authentication infrastructure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch connect-new-API

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates profile activity lists to use a dedicated UserActivityModel, aiming to align the user page with role-based activity endpoints while also cleaning up a few UI and lint-related details elsewhere in the app.

Changes:

  • Add UserActivityModel and switch the user profile tabs to load activities through role-specific stores.
  • Update ActivityList so callers can inject a custom store instead of always using its internal ActivityModel.
  • Apply small markup/style cleanups on the home page, activity detail page, and Biome config.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pages/user/[id].tsx Rewires profile activity tabs to use UserActivityModel instances for enrollee/creator/staff roles.
pages/index.tsx Minor import reordering and list styling cleanup for the homepage stats section.
pages/activity/[name]/index.tsx Cleans up tab markup and keeps award/rank tabs inside the main Tabs container.
models/User/index.ts Formatting-only change in the top-user ranking request.
models/Activity/index.ts Adds UserActivityModel and changes the enrollment submission request method.
components/Activity/ActivityList.tsx Adds optional injected-store support for activity lists.
biome.json Enables a Biome warning for dangerouslySetInnerHTML.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +53 to 54
store = this.props.store || new ActivityModel();

Comment thread models/Activity/index.ts
@toggle('uploading')
async signOne(name: string, form: Enrollment['form'] = []) {
await this.client.put(`${this.baseURI}/${name}/enrollment`, { form });
await this.client.post(`${this.baseURI}/${name}/enrollment`, { form });
Comment thread pages/user/[id].tsx
Comment on lines 161 to +165
<Tab.Pane eventKey="created">
<ActivityList type="created" userId={id} />
<ActivityList store={new UserActivityModel(id, 'creator')} />
</Tab.Pane>
<Tab.Pane eventKey="admin">
<ActivityList type="admin" userId={id} />
<ActivityList store={new UserActivityModel(id, 'staff')} />
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
models/User/index.ts (1)

13-13: Add a clarifying comment for the cross-service endpoint.

The endpoint 'activity-log/user-rank' is a legitimate cross-service call to the activity-log service (not the user service). Consider adding an inline comment explaining why this endpoint doesn't follow the baseURI pattern:

// User rankings are managed by the activity-log service, not the user service
const { body } = await this.client.get<UserRankListChunk>('activity-log/user-rank');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/User/index.ts` at line 13, Add an inline clarifying comment
immediately above the call to
this.client.get<UserRankListChunk>('activity-log/user-rank') in
models/User/index.ts explaining that 'activity-log/user-rank' is a cross-service
endpoint owned by the activity-log service (not the user service) and therefore
intentionally does not use the local baseURI pattern; place the comment next to
the call that references the UserRankListChunk type so future readers understand
why this endpoint differs from other user service calls.
biome.json (1)

76-76: ⚡ Quick win

Prefer "error" + a targeted biome-ignore comment over a global "warn" downgrade.

Setting noDangerouslySetInnerHtml to "warn" globally means future unsafe usages anywhere in the codebase will silently pass CI. The idiomatic approach is to keep the rule at "error" and suppress it only at the one intentional callsite.

♻️ Proposed refactor
- "noDangerouslySetInnerHtml": "warn"
+ "noDangerouslySetInnerHtml": "error"

Then at the callsite in pages/activity/[name]/index.tsx:

- <article dangerouslySetInnerHTML={{ __html: detail }} />
+ {/* biome-ignore lint/security/noDangerouslySetInnerHtml: detail is admin-authored HTML, sanitized server-side */}
+ <article dangerouslySetInnerHTML={{ __html: detail ?? '' }} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@biome.json` at line 76, The rule "noDangerouslySetInnerHtml" was globally
downgraded to "warn"; revert it to "error" in biome.json and instead add a
targeted suppression at the exact intentional callsite (e.g., the usage in
pages/activity/[name]/index.tsx) using a biome-ignore comment for that rule;
locate the unsafe usage (search for dangerouslySetInnerHTML in
pages/activity/[name]/index.tsx) and add a single-line biome-ignore for
noDangerouslySetInnerHtml immediately above that line so the rest of the
codebase remains enforced as an error.
components/Activity/ActivityList.tsx (1)

52-53: ⚡ Quick win

store is captured once at construction — prop updates after mount are silently ignored

store = this.props.store || new ActivityModel() is a class field initializer: it runs exactly once when ActivityList is mounted. If UserDetailPage (an observer component) re-renders and passes a fresh UserActivityModel instance, this.store retains the original value. PureComponent's shallow diff sees a new store prop reference and calls render(), but render() reads this.store, not this.props.store.

This is not a bug when callers memoize the store (see the useMemo suggestion in pages/user/[id].tsx), but a comment or a componentDidUpdate guard would prevent accidental misuse in the future:

♻️ Option A — guard against unintentional prop drift (minimal change)
 export default class ActivityList extends PureComponent<ActivityListProps> {
   store = this.props.store || new ActivityModel();
+
+  // `store` is initialised once at mount; pass a stable reference (useMemo) from the parent.
+  // Replacing the store after mount is not supported without remounting (use a `key` prop).
♻️ Option B — honour prop changes reactively
+  componentDidUpdate(prevProps: ActivityListProps) {
+    if (prevProps.store !== this.props.store && this.props.store) {
+      this.store = this.props.store;
+      this.forceUpdate();
+    }
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Activity/ActivityList.tsx` around lines 52 - 53, ActivityList
currently captures store once via the class field "store = this.props.store ||
new ActivityModel()", so later updates to props.store are ignored; update the
component to either (A) guard against prop drift by implementing
componentDidUpdate(prevProps) to detect a changed props.store and replace
this.store (and optionally warn) or (B) honor prop changes by removing the
cached this.store and always read from this.props.store (falling back to
creating a new ActivityModel when undefined) in render; reference ActivityList,
the store field, ActivityModel, props.store, and componentDidUpdate when making
the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@models/Activity/index.ts`:
- Around line 169-175: The subclass UserActivityModel is inheriting
ActivityModel's restored field (which runs restore(this, 'Activity') during
super()), causing all instances to share the singleton's localStorage key; to
fix, override the restored field in UserActivityModel to opt out of persistence
by adding a class field like restored = false (so after super() runs the
subclass resets it), ensuring UserActivityModel instances do not call
restore(this, 'Activity') and therefore don't overwrite the singleton
cache—update UserActivityModel (constructor/class body) to define restored =
false and leave ActivityModel.restore logic unchanged.

In `@pages/activity/`[name]/index.tsx:
- Around line 280-282: The Tab rendering currently injects unsanitized HTML via
dangerouslySetInnerHTML using the variable detail; sanitize detail before
rendering and guard against undefined. Install and import DOMPurify (or use
server-side sanitize in getServerSideProps), then when rendering in the
component that contains <Tab ...> and <article dangerouslySetInnerHTML={{
__html: detail }} />, replace detail with a sanitized value (e.g.,
sanitizedDetail = DOMPurify.sanitize(detail || '') ), ensuring DOMPurify is only
invoked in the browser (use the existing isServer() guard) or perform
sanitization inside getServerSideProps and pass the safe string down to the
component.

In `@pages/user/`[id].tsx:
- Around line 157-166: UserDetailPage currently constructs three new
UserActivityModel instances inline in JSX (new UserActivityModel(id, 'enrollee')
/ 'creator' / 'staff'), causing recreation on every observer-triggered re-render
(and redundant restore(...) calls) and also being ignored by mounted
ActivityList components; change to memoize or reuse one model instance per (id,
role) — e.g., create/get models from a stable cache or use React.useMemo keyed
on id and role and pass those cached instances into ActivityList instead of
constructing inline; update references around UserDetailPage, UserActivityModel,
and ActivityList (and remove inline new ... in Tab.Pane) and ensure the
PublicUser/User.id type matches the UserActivityModel constructor parameter
userId:number.

---

Nitpick comments:
In `@biome.json`:
- Line 76: The rule "noDangerouslySetInnerHtml" was globally downgraded to
"warn"; revert it to "error" in biome.json and instead add a targeted
suppression at the exact intentional callsite (e.g., the usage in
pages/activity/[name]/index.tsx) using a biome-ignore comment for that rule;
locate the unsafe usage (search for dangerouslySetInnerHTML in
pages/activity/[name]/index.tsx) and add a single-line biome-ignore for
noDangerouslySetInnerHtml immediately above that line so the rest of the
codebase remains enforced as an error.

In `@components/Activity/ActivityList.tsx`:
- Around line 52-53: ActivityList currently captures store once via the class
field "store = this.props.store || new ActivityModel()", so later updates to
props.store are ignored; update the component to either (A) guard against prop
drift by implementing componentDidUpdate(prevProps) to detect a changed
props.store and replace this.store (and optionally warn) or (B) honor prop
changes by removing the cached this.store and always read from this.props.store
(falling back to creating a new ActivityModel when undefined) in render;
reference ActivityList, the store field, ActivityModel, props.store, and
componentDidUpdate when making the change.

In `@models/User/index.ts`:
- Line 13: Add an inline clarifying comment immediately above the call to
this.client.get<UserRankListChunk>('activity-log/user-rank') in
models/User/index.ts explaining that 'activity-log/user-rank' is a cross-service
endpoint owned by the activity-log service (not the user service) and therefore
intentionally does not use the local baseURI pattern; place the comment next to
the call that references the UserRankListChunk type so future readers understand
why this endpoint differs from other user service calls.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e85857b7-d29d-4360-ae02-e3138514c743

📥 Commits

Reviewing files that changed from the base of the PR and between 742d0dc and 28c643c.

📒 Files selected for processing (7)
  • biome.json
  • components/Activity/ActivityList.tsx
  • models/Activity/index.ts
  • models/User/index.ts
  • pages/activity/[name]/index.tsx
  • pages/index.tsx
  • pages/user/[id].tsx

Comment thread models/Activity/index.ts
Comment on lines +169 to +175
export class UserActivityModel extends ActivityModel {
constructor(
public userId: number,
public role: 'creator' | 'staff' | 'enrollee',
) {
super();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

UserActivityModel inherits restore(this, 'Activity') — all instances share the same localStorage key as the singleton

ActivityModel line 37 defines restored = !isServer() && restore(this, 'Activity') as a class field initializer. Because UserActivityModel calls super(), that initializer runs for every instance, binding each one to the same 'Activity' localStorage key used by the export default new ActivityModel() singleton (line 185).

Consequences:

  • When UserActivityModel.loadPage stores its paginated result, mobx-restful's restore reaction writes to 'Activity', overwriting the singleton's cache.
  • Any singleton read that later triggers a restore reload (e.g., navigation back to the main activity list) may re-hydrate with the stale user-filtered list.
  • Multiple UserActivityModel instances (enrollee / creator / staff) overwrite each other on the same key.

Override restored in the subclass to opt out of persistence:

🛡️ Proposed fix
 export class UserActivityModel extends ActivityModel {
+  override restored = false as const;
+
   constructor(
     public userId: number,
     public role: 'creator' | 'staff' | 'enrollee',
   ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/Activity/index.ts` around lines 169 - 175, The subclass
UserActivityModel is inheriting ActivityModel's restored field (which runs
restore(this, 'Activity') during super()), causing all instances to share the
singleton's localStorage key; to fix, override the restored field in
UserActivityModel to opt out of persistence by adding a class field like
restored = false (so after super() runs the subclass resets it), ensuring
UserActivityModel instances do not call restore(this, 'Activity') and therefore
don't overwrite the singleton cache—update UserActivityModel (constructor/class
body) to define restored = false and leave ActivityModel.restore logic
unchanged.

Comment on lines +280 to 282
<Tab className="pt-2" eventKey="detail" title={t('hackathon_detail')}>
<article dangerouslySetInnerHTML={{ __html: detail }} />
</Tab>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sanitize detail HTML before rendering to prevent XSS.

detail is injected verbatim into the DOM via dangerouslySetInnerHTML. If this field is admin- or user-authored (rich-text editor output from the API), any embedded <script>, inline event handlers, or javascript: URIs will execute in the viewer's browser. The static analysis tool (CWE-79) flagged this as an unsanitized injection.

Additionally, detail may be undefined for hackathons that haven't populated this field, which should be guarded against.

The recommended fix is to sanitize on the client with DOMPurify:

🛡️ Proposed fix

Install DOMPurify:

pnpm add dompurify
pnpm add -D `@types/dompurify`

Apply at the callsite:

+ import DOMPurify from 'dompurify';

- <article dangerouslySetInnerHTML={{ __html: detail }} />
+ <article dangerouslySetInnerHTML={{ __html: isServer() ? '' : DOMPurify.sanitize(detail ?? '') }} />

Note: DOMPurify requires a DOM environment, so you must guard with isServer() (already imported) or use the SSR build (dompurify/dist/purify.cjs with a JSDOM window shim). Alternatively, sanitize the HTML on the server before returning it from getServerSideProps using sanitize-html or DOMPurify with JSDOM.

🧰 Tools
🪛 ast-grep (0.42.1)

[warning] 280-280: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/activity/`[name]/index.tsx around lines 280 - 282, The Tab rendering
currently injects unsanitized HTML via dangerouslySetInnerHTML using the
variable detail; sanitize detail before rendering and guard against undefined.
Install and import DOMPurify (or use server-side sanitize in
getServerSideProps), then when rendering in the component that contains <Tab
...> and <article dangerouslySetInnerHTML={{ __html: detail }} />, replace
detail with a sanitized value (e.g., sanitizedDetail = DOMPurify.sanitize(detail
|| '') ), ensuring DOMPurify is only invoked in the browser (use the existing
isServer() guard) or perform sanitization inside getServerSideProps and pass the
safe string down to the component.

Comment thread pages/user/[id].tsx
Comment on lines 157 to 166
<Tab.Content>
<Tab.Pane eventKey="enrolled">
<ActivityList type="enrolled" userId={id} />
<ActivityList store={new UserActivityModel(id, 'enrollee')} />
</Tab.Pane>
<Tab.Pane eventKey="created">
<ActivityList type="created" userId={id} />
<ActivityList store={new UserActivityModel(id, 'creator')} />
</Tab.Pane>
<Tab.Pane eventKey="admin">
<ActivityList type="admin" userId={id} />
<ActivityList store={new UserActivityModel(id, 'staff')} />
</Tab.Pane>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the type of User.id from the hop-service package
echo "=== User interface from hop-service ==="
fd -i "index.d.ts" node_modules/@freecodecamp-chengdu/hop-service --exec grep -n "id" {}

echo ""
echo "=== User type in codebase ==="
rg -n --type ts "interface User|type User" -C 5

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 804


🏁 Script executed:

# Find the pages directory and the specific file
fd "user.*\[id\]" pages/
# Also look for pages/user directory
ls -la pages/user/ 2>/dev/null || echo "pages/user not found"
# Check if it's TypeScript or JavaScript
fd "\[id\]" pages/

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 408


🏁 Script executed:

# Find UserActivityModel definition
rg -n "class UserActivityModel|export.*UserActivityModel" --type ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 152


🏁 Script executed:

# Find ActivityList component and check if it's PureComponent
rg -n "class ActivityList|const ActivityList|export.*ActivityList" --type ts --type tsx

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 96


🏁 Script executed:

# Check the User type definition in codebase
rg -n "^export.*interface User|^export.*type User" --type ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 142


🏁 Script executed:

# Get the full pages/user/[id].tsx file or at least lines around 157-166
fd -e tsx -e ts "user" pages/ | head -10

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 50


🏁 Script executed:

# Read pages/user/[id].tsx to verify the problematic code
cat -n pages/user/[id].tsx

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 7780


🏁 Script executed:

# Find ActivityList definition
rg -n "ActivityList" --type ts -A 3

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 5468


🏁 Script executed:

# Find UserActivityModel definition and constructor
cat -n models/Activity/index.ts | head -200

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 6694


🏁 Script executed:

# Check for PureComponent in ActivityList
rg -n "PureComponent|extends.*Component" models/Activity/ --type ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 50


🏁 Script executed:

# Check User import and type
rg -n "import.*User.*from.*hop-service" --type ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 904


new UserActivityModel(...) inline in JSX creates new instances on every re-render

UserDetailPage is wrapped with observer(), so it re-renders whenever any MobX observable it reads changes (e.g., sessionStore.user loading after login). Each render creates three new UserActivityModel instances. Because each instance inherits restore(this, 'Activity'), every re-render triggers three redundant localStorage operations.

Additionally, ActivityList (a PureComponent) stores the store prop in a class field at construction time, so any ActivityList that has already mounted will silently ignore replacement instances passed on subsequent renders.

Memoize the models keyed on id:

🛠️ Proposed fix
-import { FC, useContext, useEffect, useState } from 'react';
+import { FC, useMemo, useContext, useEffect, useState } from 'react';

 const UserDetailPage: FC<PublicUser> = observer(({ id, name, avatar }) => {
   const { t } = useContext(I18nContext);
   const { user } = sessionStore;
   const [isOwner, setIsOwner] = useState(false);
+
+  const enrolleeStore = useMemo(() => new UserActivityModel(id, 'enrollee'), [id]);
+  const creatorStore = useMemo(() => new UserActivityModel(id, 'creator'), [id]);
+  const staffStore = useMemo(() => new UserActivityModel(id, 'staff'), [id]);
 
   // ...
 
                     <Tab.Pane eventKey="enrolled">
-                      <ActivityList store={new UserActivityModel(id, 'enrollee')} />
+                      <ActivityList store={enrolleeStore} />
                     </Tab.Pane>
                     <Tab.Pane eventKey="created">
-                      <ActivityList store={new UserActivityModel(id, 'creator')} />
+                      <ActivityList store={creatorStore} />
                     </Tab.Pane>
                     <Tab.Pane eventKey="admin">
-                      <ActivityList store={new UserActivityModel(id, 'staff')} />
+                      <ActivityList store={staffStore} />
                     </Tab.Pane>

Also verify that User.id (the type of id in PublicUser) matches the userId: number constructor parameter of UserActivityModel.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Tab.Content>
<Tab.Pane eventKey="enrolled">
<ActivityList type="enrolled" userId={id} />
<ActivityList store={new UserActivityModel(id, 'enrollee')} />
</Tab.Pane>
<Tab.Pane eventKey="created">
<ActivityList type="created" userId={id} />
<ActivityList store={new UserActivityModel(id, 'creator')} />
</Tab.Pane>
<Tab.Pane eventKey="admin">
<ActivityList type="admin" userId={id} />
<ActivityList store={new UserActivityModel(id, 'staff')} />
</Tab.Pane>
<Tab.Content>
<Tab.Pane eventKey="enrolled">
<ActivityList store={enrolleeStore} />
</Tab.Pane>
<Tab.Pane eventKey="created">
<ActivityList store={creatorStore} />
</Tab.Pane>
<Tab.Pane eventKey="admin">
<ActivityList store={staffStore} />
</Tab.Pane>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/user/`[id].tsx around lines 157 - 166, UserDetailPage currently
constructs three new UserActivityModel instances inline in JSX (new
UserActivityModel(id, 'enrollee') / 'creator' / 'staff'), causing recreation on
every observer-triggered re-render (and redundant restore(...) calls) and also
being ignored by mounted ActivityList components; change to memoize or reuse one
model instance per (id, role) — e.g., create/get models from a stable cache or
use React.useMemo keyed on id and role and pass those cached instances into
ActivityList instead of constructing inline; update references around
UserDetailPage, UserActivityModel, and ActivityList (and remove inline new ...
in Tab.Pane) and ensure the PublicUser/User.id type matches the
UserActivityModel constructor parameter userId:number.

@TechQuery
Copy link
Copy Markdown
Member Author

@copilotAward 替换为 hop-service 包里的同名类型,再将整个项目中引用它的地方修改为适配新类型的代码:

import { User, Base, Media, Team } from '@freecodecamp-chengdu/hop-service';
import { ListModel, Stream, toggle } from 'mobx-restful';
import { createListStream, InputData, TableModel } from '../Base';
import sessionStore from '../User/Session';
export interface Award extends Record<'hackathonName' | 'name' | 'description', string>, Base {
quantity: number;
target: 'team' | 'individual';
pictures: Media[];
}

Copy link
Copy Markdown

Copilot AI commented May 5, 2026

@copilotAward 替换为 hop-service 包里的同名类型,再将整个项目中引用它的地方修改为适配新类型的代码:
https://github.com/FreeCodeCamp-Chengdu/HOP/blob/742d0d...

Done in commit d292915. Here's a summary of the changes made:

  • models/Activity/Award.ts: Removed the local Award and AwardAssignment interface definitions; now imports and re-exports Award, AwardAssignment, and AwardTarget directly from @freecodecamp-chengdu/hop-service.

  • components/Activity/AwardList.tsx: Updated AwardTargetName to use AwardTarget enum keys (AwardTarget.Individual, AwardTarget.Team); added safe fallback for optional target field (target ? (AwardTargetName(i18n)[target] ?? '') : '').

  • components/Team/TeamAwardAssignment.tsx: Removed description and updatedAt from destructuring — these are no longer part of the AwardAssignment type from hop-service.

  • pages/activity/[name]/manage/evaluation.tsx: Replaced custom awardId/assigneeId form fields with the AwardAssignment shape from hop-service ({ award } form field, { team: store.currentOne } assignment payload, and target === AwardTarget.Team comparison).

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
pages/activity/[name]/manage/evaluation.tsx (1)

49-66: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix misleading number type assertion on string form value.

In the submit handler, formToJSON<{ award: number }>(form) is a type assertion that lies to TypeScript. The formToJSON utility returns raw DOM input values, which are always strings—not coerced to numbers. At runtime, award will be a string, creating a type mismatch if Award['id'] is actually typed as number in the service.

Either coerce explicitly to the expected type or align the type annotation with the actual string:

Suggested fix
-    const { awardStore, store } = this,
-      form = event.currentTarget,
-      { award: awardId } = formToJSON<{ award: number }>(form);
+    const { awardStore, store } = this,
+      form = event.currentTarget,
+      { award } = formToJSON<{ award: string }>(form),
+      awardId = Number(award);
🤖 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 `@pages/activity/`[name]/manage/evaluation.tsx around lines 49 - 66, The
handler handleSubmit currently asserts formToJSON<{ award: number }>(form) but
form inputs are strings; change this to parse/coerce the award value before
using it: read award as a string from formToJSON (e.g., formToJSON<{ award:
string }>(form) or let raw = formToJSON(form)), then convert award to the
correct type (use Number(award) or parseInt(award, 10)) and store that result in
awardId before calling awardStore.getOne(awardId) and
awardStore.assignmentOf(awardId); ensure the converted awardId matches the
expected Award['id'] type used by awardStore.getOne and
assignmentStore.updateOne.
🤖 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.

Outside diff comments:
In `@pages/activity/`[name]/manage/evaluation.tsx:
- Around line 49-66: The handler handleSubmit currently asserts formToJSON<{
award: number }>(form) but form inputs are strings; change this to parse/coerce
the award value before using it: read award as a string from formToJSON (e.g.,
formToJSON<{ award: string }>(form) or let raw = formToJSON(form)), then convert
award to the correct type (use Number(award) or parseInt(award, 10)) and store
that result in awardId before calling awardStore.getOne(awardId) and
awardStore.assignmentOf(awardId); ensure the converted awardId matches the
expected Award['id'] type used by awardStore.getOne and
assignmentStore.updateOne.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 88439a3e-3381-4f4e-92ba-effd33c53426

📥 Commits

Reviewing files that changed from the base of the PR and between 28c643c and 6239405.

📒 Files selected for processing (4)
  • components/Activity/AwardList.tsx
  • components/Team/TeamAwardAssignment.tsx
  • models/Activity/Award.ts
  • pages/activity/[name]/manage/evaluation.tsx
✅ Files skipped from review due to trivial changes (1)
  • components/Team/TeamAwardAssignment.tsx

@TechQuery
Copy link
Copy Markdown
Member Author

@copilot 将以下后端接口及其对应类型引入:
https://github.com/FreeCodeCamp-Chengdu/HOP-service/blob/ea2249c62533d306e9aac0c0a0047f3c23717535/source/controller/Survey.ts#L21-L43

把原有问卷问题类型替换,再将原有静态数据作为新建问卷时的备选项,在编辑页面新增一个按钮一键填入:

export interface Question {
id?: string;
title: string;
options?: string[];
multiple?: boolean;
type?: 'text' | 'url';
required?: boolean;
}
export const questions = ({ t }: typeof i18n): Question[] => [
{
title: t('what_is_your_occupation'),
options: [
t('front_end_engineer'),
t('back_end_engineer'),
t('client_engineer'),
t('game_developerment_engineer'),
t('algorithm_engineer'),
t('blockchain_engineer'),
t('operations'),
t('quality_assurance_engineer'),
t('architect'),
t('project_manager'),
t('student'),
t('others'),
],
multiple: true,
},
{
title: t('what_are_your_commonly_used_programming_languages'),
options: [
'JavaScript/TypeScript',
'Java/Scala/Groovy/Kotlin',
'C#',
'Python',
'PHP',
'Ruby',
'Dart',
'Objective-C/Swift',
'Rust',
'Go',
'C/C++',
t('others'),
],
multiple: true,
},
{
title: t('linkein_or_cv'),
type: 'url',
},
{
title: t('social_media_account_or_twitter_or_weibo'),
type: 'url',
},
{
title: t('which_of_these_particular_areas_are_you_interested_in_going_into_this_event'),
type: 'text',
},
{
title: t('what_do_you_hope_to_learn_from_the_workshops'),
type: 'text',
},
{
title: t('do_you_plan__on_hacking_solo_or_with_a_team'),
options: [t('yes'), t('no_i_prefer_solo'), t('no_i_have_a_team')],
},
{
title: t('would_you_be_open_to_having_a_beginner_join_your_team'),
options: [t('yes'), t('no')],
},
{
title: t('anything_else_we_should_know_about_what_youre_looking_for_in_a_team_or_teammate'),
type: 'text',
},
];

Copy link
Copy Markdown

Copilot AI commented May 5, 2026

@copilot 将以下后端接口及其对应类型引入:
https://github.com/FreeCodeCamp-Chengdu/HOP-service/blob/ea2249c62533d306e9aac0c0a0047f3c23717535/source/controller/Survey.ts#L21-L43
...

Done in commit c82a700. Here's what was changed:

  • models/Activity/Question.ts: Removed the local Question interface; now imports and re-exports Question and QuestionType from @freecodecamp-chengdu/hop-service. The static questions preset function is kept and updated to use QuestionType.Text/QuestionType.URL enum values.

  • components/Activity/QuestionnaireTable.tsx: Updated import to use the QuestionType re-export and uses QuestionType.Text as the default for the type field.

  • pages/activity/[name]/manage/questionnaire.tsx: Imports the questions preset function. Added a fillWithDefaults method (with a confirmation prompt if existing questions would be overwritten) and a "Fill with default questions" secondary button on the edit page.

  • translation/{en-US,zh-CN,zh-TW}.ts: Added fill_default_questions translation key.

Copy link
Copy Markdown
Member Author

@TechQuery TechQuery left a comment

Choose a reason for hiding this comment

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

@copilot 根据我的以上意见来修改。

Comment thread models/Activity/Award.ts Outdated
team?: Team;
award: Award;
}
export { Award, AwardAssignment, AwardTarget };
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

  1. 所有的类型都直接引用,不要重复导出
  2. 注意这个包是纯类型包,枚举等运行时不可用,要直接写枚举的具体值,并可能需要断言

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Done in commit 0dca77b:

  • Removed export { Award, AwardAssignment, AwardTarget } from models/Activity/Award.ts — all consumers now import directly from @freecodecamp-chengdu/hop-service
  • Same done for models/Activity/Question.ts
  • All imports from hop-service changed to import type since the package is type-only
  • Runtime enum value AwardTarget.Team replaced with the concrete string 'team' in evaluation.tsx
  • Runtime enum values QuestionType.Text/QuestionType.URL replaced with 'text' as QuestionType / 'url' as QuestionType

)
return;

const defaultQuestions = questions(this.observedContext).map((q, i) => ({
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

变量名用英文单词全写。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Done in commit 0dca77b — renamed qquestion and iindex.

Copy link
Copy Markdown

Copilot AI commented May 5, 2026

@copilot 根据我的以上意见来修改。

Done in commit 0dca77b. Both points addressed — re-exports removed and runtime enum values replaced with string literals.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
models/Activity/index.ts (1)

169-183: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

restored field inherited from ActivityModel ties every UserActivityModel instance to the singleton's 'Activity' localStorage key.

ActivityModel.restored = !isServer() && restore(this, 'Activity') runs as a class field initializer for every subclass instance via super(), so every per-tab UserActivityModel (creator/staff/enrollee) hydrates from and persists into the same key as the default-export singleton, causing them to overwrite each other and the singleton cache.

🛡️ Suggested fix
 export class UserActivityModel extends ActivityModel {
+  override restored = false as const;
+
   constructor(
     public userId: number,
     public role: 'creator' | 'staff' | 'enrollee',
   ) {
🤖 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 `@models/Activity/index.ts` around lines 169 - 183, ActivityModel currently
initializes restored with a fixed key via ActivityModel.restored = !isServer()
&& restore(this, 'Activity'), causing every UserActivityModel (created via
super()) to hydrate/persist to the same 'Activity' localStorage key and
overwrite each other; change the persistence key to be instance-specific (e.g.,
include userId and role) by refactoring ActivityModel to accept a storageKey (or
compute it from this.constructor plus identifiers) and replace restore(this,
'Activity') with restore(this, storageKey), then update UserActivityModel to
pass a unique key like `Activity:${this.userId}:${this.role}` (or otherwise
compute the unique key before calling super) so each instance uses its own
localStorage entry.
🧹 Nitpick comments (4)
pages/activity/[name]/manage/questionnaire.tsx (1)

110-113: 💤 Low value

Sequential string IDs from index can collide with later user-added items.

String(index + 1) produces ids '1'..'9'. After filling defaults, if the user adds more questions via addQuestionnaireItem and another flow assigns ids based on length, or if defaults are filled twice, ids may collide and addQuestionnaireItem will short-circuit with the “question_id_repeat” alert. Consider a more unique scheme (e.g., crypto.randomUUID() or a monotonic counter prefixed like default-${i+1}) to avoid surprise collisions.

🤖 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 `@pages/activity/`[name]/manage/questionnaire.tsx around lines 110 - 113,
defaultQuestions currently assigns sequential string ids via String(index + 1)
which can collide with later user-added items and trigger the question_id_repeat
short-circuit in addQuestionnaireItem; replace the sequential id generation in
the defaultQuestions mapping (the expression that wraps
questions(this.observedContext)) with a unique scheme such as
crypto.randomUUID() or a monotonic prefixed id like `default-${index+1}` so each
default item has a globally-unique id and cannot collide with user-added ids or
repeated defaults.
models/Activity/index.ts (1)

169-183: 💤 Low value

UserActivityModel only specializes loadPage; other inherited operations still hit the global hackathon endpoint.

baseURI remains 'hackathon', so any inherited TableModel methods (getOne, updateOne, deleteOne, getList, etc.) invoked on a UserActivityModel instance will operate on the global hackathon resource, not on user/${userId}/hackathon/${role}. If only loadPage is intended to be used here, consider documenting that or narrowing the public surface; otherwise, override baseURI (or those methods) so the user-scoped URL is honored consistently.

🤖 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 `@models/Activity/index.ts` around lines 169 - 183, UserActivityModel currently
only overrides loadPage so inherited TableModel methods (getOne, updateOne,
deleteOne, getList, etc.) still use the default baseURI 'hackathon', causing
requests to the global resource; either restrict the public surface or override
baseURI (or those specific methods) to use the user-scoped path
`user/${this.userId}/hackathon/${this.role}` so all operations target the same
endpoint—update the class to set a per-instance baseURI or implement wrapper
methods for getOne/updateOne/deleteOne/getList that prepend
`user/${this.userId}/hackathon/${this.role}` to requests, and keep loadPage
as-is.
pages/activity/[name]/register.tsx (1)

26-28: 💤 Low value

Local activityStore shadows the imported singleton.

Line 12 imports activityStore (the singleton) and ActivityModel, then line 26 declares const activityStore = new ActivityModel(); inside getServerSideProps, shadowing the module-level binding. Functionally fine because the SSR-only block uses a fresh instance, but a different name (e.g., store) avoids the shadowing and makes intent clearer.

🤖 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 `@pages/activity/`[name]/register.tsx around lines 26 - 28, The local
declaration const activityStore = new ActivityModel() inside getServerSideProps
shadows the imported activityStore singleton; rename the local variable (e.g.,
to store or activityModel) so it doesn't shadow the module-level activityStore
and update subsequent references (status, questionnaire calls) to use the new
name; ensure you reference ActivityModel when creating the instance and keep
getServerSideProps logic unchanged otherwise.
components/Activity/QuestionnaireTable.tsx (1)

37-37: 💤 Low value

Consider the cast pattern consistency before removing as QuestionType.

While type = 'text' may appear to be valid without the cast, note that this as QuestionType pattern is consistently used throughout the codebase (see models/Activity/Question.ts lines 44, 48, 52, 56, 68). Since QuestionType is imported from the external @freecodecamp-chengdu/hop-service package, the cast may be intentional for type safety or narrowing. If removing it elsewhere, ensure consistency across the codebase and that no TypeScript compiler issues arise.

🤖 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 `@components/Activity/QuestionnaireTable.tsx` at line 37, The default parameter
in the QuestionnaireTable mapping currently uses type = 'text' without the cast,
which breaks the existing pattern of casting to QuestionType; restore the cast
so the parameter reads type = 'text' as QuestionType to match the rest of the
codebase (see QuestionType usage in models/Activity/Question.ts) and avoid
TypeScript narrowing/import mismatches; update the mapping callback signature ({
id, title, options, multiple, type = 'text' as QuestionType, required }, index,
{ length }) to reintroduce the cast so the component and other modules remain
consistent.
🤖 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 `@models/Activity/index.ts`:
- Around line 162-166: The signOne method currently discards the POST response
and returns currentEnrollment?.getSessionOne(), which can be undefined when
currentEnrollment is not set (see enrollmentOf usage) and causes a
redundant/racy GET via getSessionOne(); update signOne to capture and return the
POST response body (the newly created enrollment/session data) and also update
the model state (set currentEnrollment or sessionOne on the ActivityModel) from
that response so callers get the authoritative data without an extra GET; ensure
you check the enrollment POST response shape and map it into the model (or
fallback to calling getSessionOne only if the response lacks the expected data).

In `@models/Activity/Question.ts`:
- Around line 42-45: In models/Activity/Question.ts replace the typo'd
translation key t('linkein_or_cv') with t('linkedin_or_cv') and update all
corresponding translation entries (e.g., translation/en-US.ts, zh-CN.ts,
zh-TW.ts) to add/rename the key to "linkedin_or_cv" so the UI and stored
analytics use the corrected identifier; ensure you remove or alias the old
"linkein_or_cv" key to avoid duplicates.

In `@pages/activity/`[name]/manage/questionnaire.tsx:
- Around line 101-116: The confirmation message in fillWithDefaults currently
reuses t('confirm_to_delete_questionnaire') which is misleading; update
fillWithDefaults to call a new translation key (e.g.,
t('confirm_overwrite_with_defaults')) instead of
t('confirm_to_delete_questionnaire'), then add that key with the suggested copy
("Existing questions will be replaced with the default set. Continue?") to your
translation files (e.g., translation/en-US.ts, zh-CN.ts, zh-TW.ts) so the prompt
accurately reflects that existing questions will be overwritten with defaults.

---

Duplicate comments:
In `@models/Activity/index.ts`:
- Around line 169-183: ActivityModel currently initializes restored with a fixed
key via ActivityModel.restored = !isServer() && restore(this, 'Activity'),
causing every UserActivityModel (created via super()) to hydrate/persist to the
same 'Activity' localStorage key and overwrite each other; change the
persistence key to be instance-specific (e.g., include userId and role) by
refactoring ActivityModel to accept a storageKey (or compute it from
this.constructor plus identifiers) and replace restore(this, 'Activity') with
restore(this, storageKey), then update UserActivityModel to pass a unique key
like `Activity:${this.userId}:${this.role}` (or otherwise compute the unique key
before calling super) so each instance uses its own localStorage entry.

---

Nitpick comments:
In `@components/Activity/QuestionnaireTable.tsx`:
- Line 37: The default parameter in the QuestionnaireTable mapping currently
uses type = 'text' without the cast, which breaks the existing pattern of
casting to QuestionType; restore the cast so the parameter reads type = 'text'
as QuestionType to match the rest of the codebase (see QuestionType usage in
models/Activity/Question.ts) and avoid TypeScript narrowing/import mismatches;
update the mapping callback signature ({ id, title, options, multiple, type =
'text' as QuestionType, required }, index, { length }) to reintroduce the cast
so the component and other modules remain consistent.

In `@models/Activity/index.ts`:
- Around line 169-183: UserActivityModel currently only overrides loadPage so
inherited TableModel methods (getOne, updateOne, deleteOne, getList, etc.) still
use the default baseURI 'hackathon', causing requests to the global resource;
either restrict the public surface or override baseURI (or those specific
methods) to use the user-scoped path
`user/${this.userId}/hackathon/${this.role}` so all operations target the same
endpoint—update the class to set a per-instance baseURI or implement wrapper
methods for getOne/updateOne/deleteOne/getList that prepend
`user/${this.userId}/hackathon/${this.role}` to requests, and keep loadPage
as-is.

In `@pages/activity/`[name]/manage/questionnaire.tsx:
- Around line 110-113: defaultQuestions currently assigns sequential string ids
via String(index + 1) which can collide with later user-added items and trigger
the question_id_repeat short-circuit in addQuestionnaireItem; replace the
sequential id generation in the defaultQuestions mapping (the expression that
wraps questions(this.observedContext)) with a unique scheme such as
crypto.randomUUID() or a monotonic prefixed id like `default-${index+1}` so each
default item has a globally-unique id and cannot collide with user-added ids or
repeated defaults.

In `@pages/activity/`[name]/register.tsx:
- Around line 26-28: The local declaration const activityStore = new
ActivityModel() inside getServerSideProps shadows the imported activityStore
singleton; rename the local variable (e.g., to store or activityModel) so it
doesn't shadow the module-level activityStore and update subsequent references
(status, questionnaire calls) to use the new name; ensure you reference
ActivityModel when creating the instance and keep getServerSideProps logic
unchanged otherwise.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5077a851-ffdb-4e1e-9689-9189d3c1d27a

📥 Commits

Reviewing files that changed from the base of the PR and between 6239405 and 0dca77b.

📒 Files selected for processing (16)
  • components/Activity/AwardList.tsx
  • components/Activity/QuestionnaireCreate.tsx
  • components/Activity/QuestionnairePreview.tsx
  • components/Activity/QuestionnaireTable.tsx
  • components/Team/TeamAwardAssignment.tsx
  • models/Activity/Award.ts
  • models/Activity/Question.ts
  • models/Activity/Team.ts
  • models/Activity/index.ts
  • pages/activity/[name]/manage/award.tsx
  • pages/activity/[name]/manage/evaluation.tsx
  • pages/activity/[name]/manage/questionnaire.tsx
  • pages/activity/[name]/register.tsx
  • translation/en-US.ts
  • translation/zh-CN.ts
  • translation/zh-TW.ts
✅ Files skipped from review due to trivial changes (1)
  • components/Activity/QuestionnairePreview.tsx

Comment thread models/Activity/index.ts
Comment on lines 162 to 166
async signOne(name: string, form: Enrollment['form'] = []) {
await this.client.put(`${this.baseURI}/${name}/enrollment`, { form });
await this.client.post(`${this.baseURI}/${name}/enrollment`, { form });

return this.currentEnrollment?.getSessionOne();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all callers of signOne and whether currentEnrollment is guaranteed to be initialized first.
rg -nP -C3 '\bsignOne\s*\('
rg -nP -C3 '\benrollmentOf\s*\('

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 5175


🏁 Script executed:

# Check the register.tsx file to see if enrollmentOf is called before signOne
head -100 pages/activity/[name]/register.tsx

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 2698


🏁 Script executed:

# Check the full context of the index.tsx file where signOne is called
sed -n '60,130p' pages/activity/[name]/index.tsx

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 1936


🏁 Script executed:

# Check the getSessionOne implementation in EnrollmentModel
rg -nP -A5 'getSessionOne\s*\(' models/

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 1242


🏁 Script executed:

# Check the getOne method in Activity model to see the full initialization flow
sed -n '110,140p' models/Activity/index.ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 924


🏁 Script executed:

# Check EnrollmentModel constructor and class definition
sed -n '1,50p' models/Activity/Enrollment.ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 1622


🏁 Script executed:

# Check if there's any watch or auto-fetch in the model
rg -nP 'makeAutoObservable|makeObservable|reaction|watch' models/Activity/Enrollment.ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 50


The POST enrollment response is discarded and may return undefined in some call paths.

signOne discards the POST response and calls currentEnrollment?.getSessionOne() instead. Two issues:

  1. Silent failure in register.tsx: The page's server-side getOne() is called on a local ActivityModel instance, not the global activityStore. When the client calls signOne(), currentEnrollment is undefined on the global store, so the return silently fails. The enrollmentOf() method only creates a new model without fetching data.

  2. Wasteful redundant fetch: The POST response is discarded and a separate GET request is made via getSessionOne(). This is wasteful and potentially racy depending on backend semantics.

The index.tsx page is safer (calls enrollmentOf() before signOne()), but still depends on getSessionOne() being called elsewhere to populate sessionOne. Confirm the POST enrollment endpoint already returns the new enrollment data, and either return it directly or defer the GET until actually needed.

🤖 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 `@models/Activity/index.ts` around lines 162 - 166, The signOne method
currently discards the POST response and returns
currentEnrollment?.getSessionOne(), which can be undefined when
currentEnrollment is not set (see enrollmentOf usage) and causes a
redundant/racy GET via getSessionOne(); update signOne to capture and return the
POST response body (the newly created enrollment/session data) and also update
the model state (set currentEnrollment or sessionOne on the ActivityModel) from
that response so callers get the authoritative data without an extra GET; ensure
you check the enrollment POST response shape and map it into the model (or
fallback to calling getSessionOne only if the response lacks the expected data).

Comment on lines 42 to 45
{
title: t('linkein_or_cv'),
type: 'url',
type: 'url' as QuestionType,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Typo: linkein_or_cv should be linkedin_or_cv.

The translation key (and presumably the entries in translation/en-US.ts/zh-CN.ts/zh-TW.ts) use linkein instead of linkedin. Easy to fix now before it becomes a load-bearing string in stored data or analytics.

✏️ Proposed fix (apply across translation files too)
-    title: t('linkein_or_cv'),
+    title: t('linkedin_or_cv'),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
title: t('linkein_or_cv'),
type: 'url',
type: 'url' as QuestionType,
},
{
title: t('linkedin_or_cv'),
type: 'url' as QuestionType,
},
🤖 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 `@models/Activity/Question.ts` around lines 42 - 45, In
models/Activity/Question.ts replace the typo'd translation key
t('linkein_or_cv') with t('linkedin_or_cv') and update all corresponding
translation entries (e.g., translation/en-US.ts, zh-CN.ts, zh-TW.ts) to
add/rename the key to "linkedin_or_cv" so the UI and stored analytics use the
corrected identifier; ensure you remove or alias the old "linkein_or_cv" key to
avoid duplicates.

Comment on lines +101 to +116
fillWithDefaults = () => {
const { t } = this.observedContext;

if (
activityStore.questionnaire.length > 0 &&
!confirm(t('confirm_to_delete_questionnaire'))
)
return;

const defaultQuestions = questions(this.observedContext).map((question, index) => ({
...question,
id: String(index + 1),
}));

return activityStore.editQuestionnaireStatus(defaultQuestions);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Confirmation message is misleading for the “fill defaults” action.

fillWithDefaults reuses t('confirm_to_delete_questionnaire'), but this flow overwrites the existing questions with defaults rather than deleting the questionnaire. A user who is OK with replacement may cancel because the prompt says “delete”, and vice versa. Add a dedicated translation key (e.g., confirm_overwrite_with_defaults) for clarity.

🌐 Suggested message wording
     if (
       activityStore.questionnaire.length > 0 &&
-      !confirm(t('confirm_to_delete_questionnaire'))
+      !confirm(t('confirm_overwrite_with_defaults'))
     )
       return;

Add the new key to translation/en-US.ts, zh-CN.ts, zh-TW.ts (e.g., “Existing questions will be replaced with the default set. Continue?”).

🤖 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 `@pages/activity/`[name]/manage/questionnaire.tsx around lines 101 - 116, The
confirmation message in fillWithDefaults currently reuses
t('confirm_to_delete_questionnaire') which is misleading; update
fillWithDefaults to call a new translation key (e.g.,
t('confirm_overwrite_with_defaults')) instead of
t('confirm_to_delete_questionnaire'), then add that key with the suggested copy
("Existing questions will be replaced with the default set. Continue?") to your
translation files (e.g., translation/en-US.ts, zh-CN.ts, zh-TW.ts) so the prompt
accurately reflects that existing questions will be overwritten with defaults.

@TechQuery
Copy link
Copy Markdown
Member Author

@copilot 新增一个路由为 /user/signIn?callback=/path/to/some/private/page 的页面,页面里有 GitHub 等 OAuth 登录按钮,点击后直接跳转其官方登录页地址,且传入的重定向地址为本页网址。

第三方登录回跳后执行原 githubSigner()jwtSigner() 中间件以完成本系统登录。

同时将原 sessionGuard() 改造为未登录状态下跳转上述登录页,已登录时解析 JWT payload 后传入 props

Copy link
Copy Markdown

Copilot AI commented May 7, 2026

@copilot 新增一个路由为 /user/signIn?callback=/path/to/some/private/page 的页面,页面里有 GitHub 等 OAuth 登录按钮,点击后直接跳转其官方登录页地址,且传入的�...

Done in commit ab2b4a6. Here's what was implemented:

pages/user/signIn.tsx (new page):

  • Route /user/signIn?callback=/path/to/private/page
  • getServerSideProps chains jwtSigner + githubSigner to handle the OAuth callback
  • When GitHub redirects back with code, the middleware processes it, issues a JWT cookie, and redirects to the original callback URL
  • When the page is shown (not logged in, no OAuth code), a GitHub OAuth button is rendered with redirect_uri pointing back to this same page (preserving the callback param)

pages/api/core.ts:

  • sessionGuard now only checks the JWT cookie: if valid, parses the payload and passes it as jwtPayload in props; if invalid/missing, redirects to /user/signIn?callback=<current_url>
  • Extracted GITHUB_OAUTH_SCOPES as a shared constant used by both githubSigner and the sign-in page URL builder

components/User/UserBar.tsx:

  • Sign-in button now links to /user/signIn instead of /user/me

…rect/notFound check

Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP/sessions/d7957cea-ce4a-4e0d-b609-a5c5c7a19422

Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI commented May 7, 2026

@copilot 接着改

Done in commit 82aab62. Both points addressed: OAuth URL construction moved to the page component with buildURLData and OAuth=GitHub appended to the callback; compose(jwtSigner, githubSigner) called inside the specific branch.

@TechQuery
Copy link
Copy Markdown
Member Author

@copilot 新建一个路由为 /user/OAuth/CNB?callback=/path/to/private/page 的页面,内含一个名为 token 的输入框,旁边一个小按钮链接 https://cnb.cool/profile/token 以引导用户生成个人令牌,下面放一个提交按钮,让本 form 标签以 GET 方法提交至本页面 URL。

该页面服务端渲染的中间件在收到上述 token 参数后,先去 https://api.cnb.cool/#/operations/GetUserInfo 接口拿用户数据,再用 jwtSigner() 中间件登录本系统后端,在 JWT 已存在时跳转回调页面。

完成以上功能后,将其加入 signIn 页面,该平台 logo 从 https://cnb.cool/ 直接引用。

…on on sign-in page

Agent-Logs-Url: https://github.com/FreeCodeCamp-Chengdu/HOP/sessions/001771e3-772f-48e3-a571-fd6349a07e9b

Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI commented May 8, 2026

@copilot 新建一个路由为 /user/OAuth/CNB?callback=/path/to/private/page 的页面,内含一个名为 token 的输入框,旁边一个小按钮链接 https://cnb.cool/profile/token 以引�...

Done in commit 39f0061. Here's what was implemented:

pages/user/OAuth/CNB.tsx (new page at /user/OAuth/CNB?callback=...):

  • Form with token input, a "Generate" button linking to https://cnb.cool/profile/token (target="_blank" rel="noopener noreferrer"), and a submit button — form uses GET method
  • getServerSideProps: if user is already logged in → redirect to callback; if token param is present → run cnbSigner → redirect to callback on success

pages/api/core.tscnbSigner middleware:

  • Checks JWT first (already logged in → return jwtPayload)
  • If not, reads token from context.query, calls SessionModel.signInWithCNB(token), sets JWT cookie, returns jwtPayload

models/User/Session.ts:

  • static signInWithCNB(accessToken)POST user/OAuth/CNB { accessToken }

pages/user/signIn.tsx:

  • Added CNB button linking to /user/OAuth/CNB?${buildURLData({ callback })} with logo from https://cnb.cool/favicon.ico

Translations: added personal_access_token and generate_token keys in all 3 locales.

Copy link
Copy Markdown
Member Author

@TechQuery TechQuery left a comment

Choose a reason for hiding this comment

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

@copilot 接着改

Comment thread pages/api/core.ts Outdated
Comment thread pages/user/OAuth/CNB.tsx Outdated
Comment thread pages/user/signIn.tsx Outdated
Comment thread pages/user/signIn.tsx Outdated
Comment thread pages/user/signIn.tsx Outdated
Copy link
Copy Markdown

Copilot AI commented May 8, 2026

@copilot 接着改

Done in commit 9fb830f. All five review points addressed.

@TechQuery TechQuery added the feature New feature or request label May 8, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (1)
pages/user/OAuth/CNB.tsx (1)

49-51: ⚡ Quick win

Add a timeout to the CNB API fetch.

A slow/hung response from api.cnb.cool will block the SSR render until the platform-level timeout fires. Use AbortSignal.timeout(...) to bound the wait.

🛡️ Proposed fix
-      const response = await fetch(`${CNB_API_BASE}/user`, {
-        headers: { Authorization: `Bearer ${CNB_token}` },
-      });
+      const response = await fetch(`${CNB_API_BASE}/user`, {
+        headers: { Authorization: `Bearer ${CNB_token}` },
+        signal: AbortSignal.timeout(5000),
+      });
🤖 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 `@pages/user/OAuth/CNB.tsx` around lines 49 - 51, The fetch to
`${CNB_API_BASE}/user` using `CNB_token` must include an AbortSignal timeout so
a slow/hung `api.cnb.cool` response cannot block SSR; update the fetch call to
pass a `signal: AbortSignal.timeout(<ms>)` in the options (choose an appropriate
timeout, e.g. 3000–5000ms), and wrap the request in try/catch to handle an
aborted request (check for DOMException/AbortError) so you can return/handle a
timeout error path instead of letting the render hang; modify the code around
the existing fetch (the block referencing CNB_API_BASE and CNB_token) to add the
signal and error handling.
🤖 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 `@pages/api/core.ts`:
- Line 86: The console.error call currently logs the raw JWT (the JWT variable)
alongside the JsonWebTokenError message; remove the JWT from logs (or replace it
with a short non-sensitive fingerprint like a truncated hash) so sensitive token
payloads are not written to logs—locate the console.error((error as
JsonWebTokenError).message, JWT) usage and change it to log only the error
message or a hash/fingerprint of JWT instead.
- Around line 77-78: The cookie name is inconsistent: sessionGuard reads
req.cookies.JWT while parseJWT and verifyJWT expect cookie: 'token'; fix by
making the cookie name consistent across code — either change where the cookie
is set (in signIn.tsx) to use 'token' or update parseJWT and verifyJWT to use
cookie: 'JWT'. Locate the functions parseJWT and verifyJWT and adjust their
cookie option to 'JWT' (or update the setter in signIn.tsx to set 'token') so
all reads/writes use the same cookie name.

In `@pages/user/OAuth/CNB.tsx`:
- Around line 25-26: The callback query value (callback, destination) is
user-controlled and currently used directly in redirects; validate and sanitize
it before use by ensuring it is a relative same-origin path: parse the callback,
reject or strip any scheme/host, and only accept values that start with '/' and
do not contain full origin or "://" (otherwise fall back to '/'); update where
destination is computed (the const destination = callback + '' and any use in
redirect.destination) to use this sanitized path so external URLs cannot be used
for open redirects.
- Around line 30-35: The Set-Cookie header for CNB_token is missing the HttpOnly
attribute, exposing the server-only CNB_token to client-side scripts; update the
cookie construction in the Set-Cookie call that sets `CNB_token=${token}` (in
pages/user/OAuth/CNB.tsx) to include `HttpOnly` alongside Path, Secure, and
SameSite so the cookie cannot be accessed via document.cookie (keep the existing
filter(Boolean).join('; ') pattern and add 'HttpOnly' to the attributes array).
- Around line 80-97: The form currently uses the default GET (Form in CNB.tsx)
which leaks the personal access token via the URL; change the <Form> to submit
via POST (add method="post") and ensure the input with name="token" is read from
the request body in getServerSideProps instead of from query; update server-side
handling to parse POST body (or accept the cookie built client-side) and set the
auth cookie from req.body.token, keeping existing symbols Form, name="token",
and getServerSideProps as the touchpoints to locate and fix the code.
- Around line 78-79: The translation call t('sign_in_with', 'CNB') is using a
string instead of the options object required for function-valued templates;
update each usage (the PageHead title prop and the h1 in the CNB component and
the two occurrences in the signIn component) to call t('sign_in_with', {
platform: 'CNB' }) (or { platform: variableName } if using a variable) so the
function-style translation is invoked with the platform parameter.

In `@pages/user/OAuth/GitHub.tsx`:
- Around line 10-13: The redirect currently uses the unvalidated query variable
`callback` (assigned to `destination`) which allows open redirects; update the
logic around `callback`/`destination` to only allow same-origin relative paths
by validating that `callback` is a safe path (e.g., non-empty string starting
with a single '/' and not starting with '//' or containing a scheme like
'http:'), and fall back to '/' if invalid. Locate the code that reads `const {
callback = '/' } = query; const destination = callback + ''; return { redirect:
{ destination, permanent: false } };` and replace it with validation that
rejects or normalizes external URLs before returning the redirect. Ensure you
check for leading '//' and URI schemes and only permit paths like '/some/path'
(no host or protocol).

---

Nitpick comments:
In `@pages/user/OAuth/CNB.tsx`:
- Around line 49-51: The fetch to `${CNB_API_BASE}/user` using `CNB_token` must
include an AbortSignal timeout so a slow/hung `api.cnb.cool` response cannot
block SSR; update the fetch call to pass a `signal: AbortSignal.timeout(<ms>)`
in the options (choose an appropriate timeout, e.g. 3000–5000ms), and wrap the
request in try/catch to handle an aborted request (check for
DOMException/AbortError) so you can return/handle a timeout error path instead
of letting the render hang; modify the code around the existing fetch (the block
referencing CNB_API_BASE and CNB_token) to add the signal and error handling.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91f24cfe-0230-4c03-bd21-5cff378ff7bb

📥 Commits

Reviewing files that changed from the base of the PR and between 0dca77b and 9997ad8.

📒 Files selected for processing (15)
  • biome.json
  • components/Activity/QuestionnaireCreate.tsx
  • components/Team/TeamAwardAssignment.tsx
  • components/User/UserBar.tsx
  • models/Activity/index.ts
  • models/User/Session.ts
  • pages/activity/[name]/manage/evaluation.tsx
  • pages/api/core.ts
  • pages/user/OAuth/CNB.tsx
  • pages/user/OAuth/GitHub.tsx
  • pages/user/me.tsx
  • pages/user/signIn.tsx
  • translation/en-US.ts
  • translation/zh-CN.ts
  • translation/zh-TW.ts
💤 Files with no reviewable changes (1)
  • pages/user/me.tsx
✅ Files skipped from review due to trivial changes (5)
  • components/User/UserBar.tsx
  • pages/user/signIn.tsx
  • biome.json
  • translation/en-US.ts
  • translation/zh-TW.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • components/Team/TeamAwardAssignment.tsx
  • models/Activity/index.ts
  • pages/activity/[name]/manage/evaluation.tsx

Comment thread pages/api/core.ts
Comment on lines +77 to +78
export const sessionGuard = compose<DataObject, JWTProps<User>>(async ({ req }, next) => {
const { JWT = '' } = req.cookies;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find every place a JWT-style cookie is set or read so we can confirm naming consistency.
rg -nP -C2 "Set-Cookie|cookies?\.(JWT|token)\b|res\.cookie\s*\(|setHeader\(\s*['\"]Set-Cookie" --type=ts --type=tsx
rg -nP -C2 "cookie\s*:\s*['\"](JWT|token)['\"]" --type=ts --type=tsx

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 124


🏁 Script executed:

#!/bin/bash
# Find JWT/token cookie references in TypeScript files
rg -n "cookies?\.(JWT|token)\b" --type=ts
rg -n "cookie\s*:\s*['\"]?(JWT|token)['\"]?" --type=ts
rg -n "Set-Cookie|setHeader.*Set-Cookie" --type=ts
rg -n "res\.cookie" --type=ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 344


🏁 Script executed:

#!/bin/bash
# Check the specific file mentioned in the review
head -30 pages/api/core.ts
tail -n +70 pages/api/core.ts | head -20

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 1688


🏁 Script executed:

#!/bin/bash
# Check the Set-Cookie calls in signIn.tsx
sed -n '35,45p' pages/user/signIn.tsx
# Check the Set-Cookie calls in CNB.tsx
sed -n '25,35p' pages/user/OAuth/CNB.tsx
sed -n '58,68p' pages/user/OAuth/CNB.tsx

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 1175


🏁 Script executed:

#!/bin/bash
# Search for all places where cookies are being set with values
rg -n "Set-Cookie.*token|Set-Cookie.*JWT" --type=ts -B2 -A2
# Also search for cookie setting patterns with more context
rg -n "res\.cookie.*token|res\.cookie.*JWT" --type=ts -B2 -A2

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 50


Fix cookie name inconsistency: JWT vs token.

The server sets a cookie named JWT (signIn.tsx), but parseJWT and verifyJWT are configured to read from a cookie named token (lines 17, 21). This mismatch means parseJWT/verifyJWT will not find the JWT. While sessionGuard correctly reads from req.cookies.JWT, other parts of the code using parseJWT/verifyJWT will silently fail to locate the token. Either rename the cookie to token where it's set, or update parseJWT/verifyJWT to use cookie: 'JWT'.

🤖 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 `@pages/api/core.ts` around lines 77 - 78, The cookie name is inconsistent:
sessionGuard reads req.cookies.JWT while parseJWT and verifyJWT expect cookie:
'token'; fix by making the cookie name consistent across code — either change
where the cookie is set (in signIn.tsx) to use 'token' or update parseJWT and
verifyJWT to use cookie: 'JWT'. Locate the functions parseJWT and verifyJWT and
adjust their cookie option to 'JWT' (or update the setter in signIn.tsx to set
'token') so all reads/writes use the same cookie name.

Comment thread pages/api/core.ts

return 'props' in nextResult ? { props: { ...nextResult.props, jwtPayload } } : nextResult;
} catch (error) {
console.error((error as JsonWebTokenError).message, JWT);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't log the raw JWT cookie value.

console.error((error as JsonWebTokenError).message, JWT) writes the user's bearer token to logs. Even when verification fails, the token's payload is still decodable and frequently contains PII (email, user id). Log forwarders, error trackers, and CI logs will then carry sensitive data. If signature verification ever fails for a transient reason (e.g. clock skew, secret rotation), a valid token could end up in logs.

Drop the token from the log line, or at most log a short fingerprint.

🛡️ Proposed fix
-    console.error((error as JsonWebTokenError).message, JWT);
+    console.error('JWT verification failed:', (error as JsonWebTokenError).message);
🤖 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 `@pages/api/core.ts` at line 86, The console.error call currently logs the raw
JWT (the JWT variable) alongside the JsonWebTokenError message; remove the JWT
from logs (or replace it with a short non-sensitive fingerprint like a truncated
hash) so sensitive token payloads are not written to logs—locate the
console.error((error as JsonWebTokenError).message, JWT) usage and change it to
log only the error message or a hash/fingerprint of JWT instead.

Comment thread pages/user/OAuth/CNB.tsx Outdated
Comment thread pages/user/OAuth/CNB.tsx
Comment on lines +30 to +35
res.setHeader(
'Set-Cookie',
[`CNB_token=${token}`, 'Path=/', isProduction ? 'Secure' : '', 'SameSite=Lax']
.filter(Boolean)
.join('; '),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Token cookie must be HttpOnly.

CNB_token is only read on the server (line 44, 50). Without HttpOnly, any XSS on the site can read and exfiltrate the user's CNB personal access token via document.cookie. Add HttpOnly to the attributes.

🛡️ Proposed fix
     res.setHeader(
       'Set-Cookie',
-      [`CNB_token=${token}`, 'Path=/', isProduction ? 'Secure' : '', 'SameSite=Lax']
+      [
+        `CNB_token=${encodeURIComponent(token + '')}`,
+        'Path=/',
+        'HttpOnly',
+        isProduction ? 'Secure' : '',
+        'SameSite=Lax',
+      ]
         .filter(Boolean)
         .join('; '),
     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res.setHeader(
'Set-Cookie',
[`CNB_token=${token}`, 'Path=/', isProduction ? 'Secure' : '', 'SameSite=Lax']
.filter(Boolean)
.join('; '),
);
res.setHeader(
'Set-Cookie',
[
`CNB_token=${encodeURIComponent(token + '')}`,
'Path=/',
'HttpOnly',
isProduction ? 'Secure' : '',
'SameSite=Lax',
]
.filter(Boolean)
.join('; '),
);
🤖 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 `@pages/user/OAuth/CNB.tsx` around lines 30 - 35, The Set-Cookie header for
CNB_token is missing the HttpOnly attribute, exposing the server-only CNB_token
to client-side scripts; update the cookie construction in the Set-Cookie call
that sets `CNB_token=${token}` (in pages/user/OAuth/CNB.tsx) to include
`HttpOnly` alongside Path, Secure, and SameSite so the cookie cannot be accessed
via document.cookie (keep the existing filter(Boolean).join('; ') pattern and
add 'HttpOnly' to the attributes array).

Comment thread pages/user/OAuth/CNB.tsx Outdated
Comment thread pages/user/OAuth/CNB.tsx Outdated
Comment thread pages/user/OAuth/GitHub.tsx
@TechQuery TechQuery changed the title [fix] Activity lists with User Activity model [refactor] add Sign In pages for multiple OAuth platforms May 8, 2026
Copy link
Copy Markdown

Copilot AI commented May 9, 2026

@copilot 修复以下几条意见及全篇其它地方的同类问题:

  1. [refactor] add Sign In pages for multiple OAuth platforms #36 (comment)
  2. https://github.com/FreeCodeCamp-Chengdu/HOP/...

Done in commit e3d1590:

  • Issues 1 & 4 (open redirect): Added utils/url.ts with sanitizeCallback() that only allows paths starting with / (not //). Applied in pages/user/signIn.tsx, pages/user/OAuth/CNB.tsx, and pages/user/OAuth/GitHub.tsx.
  • Issue 3 (token in URL): Changed the CNB form to method="post" with the callback in the action URL. Added parseFormBody() with a 4 KB payload size limit to read the token from the POST body server-side.
  • Issue 2 (translation call syntax): Skipped — t('sign_in_with', 'CNB') is the correct mobx-i18n call pattern for function-valued translations in this codebase, consistent with the existing t('for_example', 'https://...') usage in components/Git/Modal.tsx.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
pages/user/OAuth/CNB.tsx (3)

55-60: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

[DUPLICATE] Token cookie must be HttpOnly.

This issue was flagged in a previous review and remains unfixed. The CNB_token cookie is only read server-side (lines 72, 77-78) but is accessible to client JavaScript. Any XSS vulnerability on the site can exfiltrate the user's CNB personal access token.

🛡️ Proposed fix
     res.setHeader(
       'Set-Cookie',
-      [`CNB_token=${token}`, 'Path=/', isProduction ? 'Secure' : '', 'SameSite=Lax']
+      [
+        `CNB_token=${encodeURIComponent(token)}`,
+        'Path=/',
+        'HttpOnly',
+        isProduction ? 'Secure' : '',
+        'SameSite=Lax',
+      ]
         .filter(Boolean)
         .join('; '),
     );
🤖 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 `@pages/user/OAuth/CNB.tsx` around lines 55 - 60, The CNB_token cookie is
missing the HttpOnly flag making it accessible to client JS; update the cookie
set in the res.setHeader call in CNB.tsx (the code building
'CNB_token=${token}') to include HttpOnly (in addition to existing Path, Secure,
SameSite flags) so the token is only sent by the browser and not exposed to
JavaScript.

106-107: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

[DUPLICATE] Fix translation function call syntax for parameterized templates.

This issue was flagged in a previous review and remains unfixed. The sign_in_with translation is a function-valued template that expects an options object, but you're passing a plain string. This will cause incorrect rendering or a runtime error.

🔧 Proposed fix
-      <PageHead title={t('sign_in_with', 'CNB')} />
-      <h1>{t('sign_in_with', 'CNB')}</h1>
+      <PageHead title={t('sign_in_with', { platform: 'CNB' })} />
+      <h1>{t('sign_in_with', { platform: 'CNB' })}</h1>

As noted in the previous review, this same fix applies to pages/user/signIn.tsx (lines 85, 95).

🤖 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 `@pages/user/OAuth/CNB.tsx` around lines 106 - 107, The translation key
sign_in_with is a parameterized (function-valued) template but the code calls
t('sign_in_with', 'CNB') as if passing a plain string; update the two
occurrences (the PageHead title and the h1 rendering where t('sign_in_with',
'CNB') is used) to pass an options object instead, e.g. t('sign_in_with', {
provider: 'CNB' }), so the template receives the expected parameters and renders
correctly; apply the same change for the other identical t('sign_in_with',
'CNB') usages in the codebase.

57-57: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

URL-encode the token value in the cookie.

If the CNB token contains special characters like =, ;, or spaces, the cookie will be malformed and parsing will fail. Use encodeURIComponent(token) to safely encode the value.

🔧 Proposed fix
-      [`CNB_token=${token}`, 'Path=/', isProduction ? 'Secure' : '', 'SameSite=Lax']
+      [`CNB_token=${encodeURIComponent(token)}`, 'Path=/', isProduction ? 'Secure' : '', 'SameSite=Lax']

Also remember to decode when reading: decodeURIComponent(req.cookies.CNB_token || '') at line 72.

🤖 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 `@pages/user/OAuth/CNB.tsx` at line 57, The CNB cookie value should be
URL-encoded to avoid breaking the cookie when token contains special chars:
update the code that builds the cookie string (the array entry using
`CNB_token=${token}`) to use `encodeURIComponent(token)` instead, and ensure the
corresponding read path decodes the cookie (use
`decodeURIComponent(req.cookies.CNB_token || '')`) where the token is read.
🧹 Nitpick comments (1)
pages/user/OAuth/CNB.tsx (1)

41-99: Consider rate limiting for CNB token validation.

The current implementation allows unlimited form submissions and CNB API calls per user/IP. This could enable brute-force attacks on CNB tokens or abuse of the CNB API quota.

Adding rate limiting (e.g., using a Redis-backed middleware or Next.js middleware with a memory store) would reduce the risk of abuse. For example:

  • Limit token submission attempts to 5 per IP per minute
  • Limit CNB API validation calls to prevent quota exhaustion

This is an infrastructure concern rather than a code fix, but worth considering before production deployment.

🤖 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 `@pages/user/OAuth/CNB.tsx` around lines 41 - 99, getServerSideProps allows
unlimited POST token submissions and CNB API calls; add a rate-limit check
(e.g., Redis or in-memory limiter) at the start of the POST branch (before
parseFormBody) to throttle token submissions (suggest 5 attempts per IP per
minute) and another check before calling fetch(${CNB_API_BASE}/user) when
CNB_token is present to protect API quota. If a limit is exceeded, immediately
return a 429 response or props with a clear error (include Retry-After header),
and log the event; ensure the limiter keys are derived from
req.socket.remoteAddress or similar and reference getServerSideProps, CNB_token,
parseFormBody, and CNB_API_BASE when implementing.
🤖 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 `@pages/user/OAuth/CNB.tsx`:
- Around line 29-35: The data handler continues appending chunks after calling
req.destroy(), so add an early-exit flag (e.g., let aborted = false) in the
scope above the req.on('data', ...) handler; inside that handler check if
(aborted) return; when size exceeds MAX_BODY_BYTES set aborted = true before
calling req.destroy(new Error('Request body too large')) to prevent further
processing and appending to data; update any related logic that relies on
size/data accordingly (references: the req.on('data', ...) callback, size,
MAX_BODY_BYTES, and req.destroy).

---

Duplicate comments:
In `@pages/user/OAuth/CNB.tsx`:
- Around line 55-60: The CNB_token cookie is missing the HttpOnly flag making it
accessible to client JS; update the cookie set in the res.setHeader call in
CNB.tsx (the code building 'CNB_token=${token}') to include HttpOnly (in
addition to existing Path, Secure, SameSite flags) so the token is only sent by
the browser and not exposed to JavaScript.
- Around line 106-107: The translation key sign_in_with is a parameterized
(function-valued) template but the code calls t('sign_in_with', 'CNB') as if
passing a plain string; update the two occurrences (the PageHead title and the
h1 rendering where t('sign_in_with', 'CNB') is used) to pass an options object
instead, e.g. t('sign_in_with', { provider: 'CNB' }), so the template receives
the expected parameters and renders correctly; apply the same change for the
other identical t('sign_in_with', 'CNB') usages in the codebase.
- Line 57: The CNB cookie value should be URL-encoded to avoid breaking the
cookie when token contains special chars: update the code that builds the cookie
string (the array entry using `CNB_token=${token}`) to use
`encodeURIComponent(token)` instead, and ensure the corresponding read path
decodes the cookie (use `decodeURIComponent(req.cookies.CNB_token || '')`) where
the token is read.

---

Nitpick comments:
In `@pages/user/OAuth/CNB.tsx`:
- Around line 41-99: getServerSideProps allows unlimited POST token submissions
and CNB API calls; add a rate-limit check (e.g., Redis or in-memory limiter) at
the start of the POST branch (before parseFormBody) to throttle token
submissions (suggest 5 attempts per IP per minute) and another check before
calling fetch(${CNB_API_BASE}/user) when CNB_token is present to protect API
quota. If a limit is exceeded, immediately return a 429 response or props with a
clear error (include Retry-After header), and log the event; ensure the limiter
keys are derived from req.socket.remoteAddress or similar and reference
getServerSideProps, CNB_token, parseFormBody, and CNB_API_BASE when
implementing.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b7d56344-13d0-4a6f-8ed0-2a227db3805f

📥 Commits

Reviewing files that changed from the base of the PR and between 9997ad8 and e3d1590.

📒 Files selected for processing (4)
  • pages/user/OAuth/CNB.tsx
  • pages/user/OAuth/GitHub.tsx
  • pages/user/signIn.tsx
  • utils/url.ts
✅ Files skipped from review due to trivial changes (1)
  • utils/url.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • pages/user/OAuth/GitHub.tsx
  • pages/user/signIn.tsx

Comment thread pages/user/OAuth/CNB.tsx Outdated
Comment on lines +29 to +35
req.on('data', (chunk: Buffer) => {
size += chunk.length;
if (size > MAX_BODY_BYTES) {
req.destroy(new Error('Request body too large'));
return;
}
data += chunk.toString();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add early-exit flag to prevent processing chunks after body size limit.

After calling req.destroy(), subsequent data events may still fire before the error event propagates. The handler continues appending chunks to data, which is unnecessary and could delay the rejection.

🛡️ Proposed fix
 const parseFormBody = (req: IncomingMessage) =>
   new Promise<URLSearchParams>((resolve, reject) => {
     let data = '';
     let size = 0;
+    let destroyed = false;
     req.on('data', (chunk: Buffer) => {
+      if (destroyed) return;
       size += chunk.length;
       if (size > MAX_BODY_BYTES) {
+        destroyed = true;
         req.destroy(new Error('Request body too large'));
         return;
       }
       data += chunk.toString();
     });
🤖 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 `@pages/user/OAuth/CNB.tsx` around lines 29 - 35, The data handler continues
appending chunks after calling req.destroy(), so add an early-exit flag (e.g.,
let aborted = false) in the scope above the req.on('data', ...) handler; inside
that handler check if (aborted) return; when size exceeds MAX_BODY_BYTES set
aborted = true before calling req.destroy(new Error('Request body too large'))
to prevent further processing and appending to data; update any related logic
that relies on size/data accordingly (references: the req.on('data', ...)
callback, size, MAX_BODY_BYTES, and req.destroy).

@TechQuery
Copy link
Copy Markdown
Member Author

@copilot 用 Playwright 阅读 https://api.cnb.cool/ ,给以下方法多加一个可选参数:
https://github.com/FreeCodeCamp-Chengdu/HOP/blob/main/models%2FGit.ts#L29-L34

用于调用一个新方法,其基本逻辑如下:

  1. 在 CNB 创建一个代码库
  2. 在库里写入 Dev Container 配置文件和 Dockerfile,其中容器初始化的脚本为:
git clone GitHub仓库地址 ./
git commit -m "HOP initializes this repository from GitHub仓库地址"
git push --no-verify
  1. 创建一个云开发环境

Copy link
Copy Markdown

Copilot AI commented May 9, 2026

@copilot 用 Playwright 阅读 https://api.cnb.cool/ ,给以下方法多加一个可选参数:
https://github.com/FreeCodeCamp-Chengdu/HOP/blob/main/models%2FGit.ts#L29-L34
...

Done in commit d3508e0.

models/Git.tscreateOneFrom now accepts an optional cnbToken?: string parameter. When provided, it calls the new createCNBWorkspace(githubRepoURL, name, cnbToken) method which:

  1. Verifies the token via GET https://api.cnb.cool/user to get the CNB username
  2. Creates a private CNB repo via POST https://api.cnb.cool/{username}/-/repos
  3. Calls /api/git/cnb-push (server-side) to write the devcontainer config to the repo
  4. Starts the cloud dev workspace via POST https://api.cnb.cool/{repoPath}/-/workspace/start

pages/api/git/cnb-push.ts (new) — Server-side Next.js API route that pushes .devcontainer/devcontainer.json and .devcontainer/Dockerfile to the new CNB repo via the git HTTP receive-pack protocol, using only Node.js built-ins (node:crypto for SHA-1, node:zlib for deflation). The devcontainer's onCreateCommand runs the init script you specified. Input is validated with strict regex patterns to prevent SSRF and shell injection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants