Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
NEXT_PUBLIC_SITE_NAME = 开放黑客松
NEXT_PUBLIC_SITE_NAME = 黑客松开放平台
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

Remove spaces around = in env assignment.

Line 1 uses KEY = value; prefer KEY=value to avoid parser/shell compatibility issues and satisfy dotenv linting.

Proposed fix
-NEXT_PUBLIC_SITE_NAME = 黑客松开放平台
+NEXT_PUBLIC_SITE_NAME=黑客松开放平台
📝 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
NEXT_PUBLIC_SITE_NAME = 黑客松开放平台
NEXT_PUBLIC_SITE_NAME=黑客松开放平台
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 1-1: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)

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

In @.env at line 1, The environment variable assignment for
NEXT_PUBLIC_SITE_NAME currently has spaces around the equals sign; update the
.env entry for NEXT_PUBLIC_SITE_NAME to use a direct KEY=value format (no spaces
around '=') so dotenv/shell parsers and linters accept it and the variable is
read correctly.

NEXT_PUBLIC_SITE_SUMMARY = 基于 Git 云开发环境的开放黑客马拉松平台
NEXT_PUBLIC_API_HOST = https://openhackathon-service.onrender.com
NEXT_PUBLIC_AUTHING_APP_ID = 60178760106d5f26cb267ac1
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,32 @@ Open-source [Hackathon][1] Platform with **Git-based Cloud Development Environme
- PWA framework: [Workbox v6][9]
- CI / CD: GitHub [Actions][10] + [Vercel][11]

## Environment Configuration

Configure the following required variables in your local environment:

```bash
# GitHub OAuth (required for login)
GITHUB_OAUTH_CLIENT_ID=your_client_id
GITHUB_OAUTH_CLIENT_SECRET=your_client_secret

# JWT Secret (required for session)
JWT_SECRET=your_jwt_secret

# API Host
NEXT_PUBLIC_API_HOST=https://openhackathon-service.onrender.com
Comment thread
dethan3 marked this conversation as resolved.
```

### Creating GitHub OAuth App

1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
2. Click "New OAuth App"
3. Fill in:
- **Application name**: HOP Local Dev
- **Homepage URL**: `http://localhost:3000`
- **Authorization callback URL**: `http://localhost:3000`
4. Copy the Client ID and generate a Client Secret

## Getting Started

First, run the development server:
Expand Down
9 changes: 7 additions & 2 deletions components/User/UserBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const UserBar = observer(() => {
{t('create_hackathons')}
</Button>

{user && (
{user ? (
<Dropdown>
<Dropdown.Toggle>{showName}</Dropdown.Toggle>
<Dropdown.Menu>
Expand All @@ -27,15 +27,20 @@ const UserBar = observer(() => {
title={t('edit_profile_tips')}
target="_blank"
href="https://github.com/settings/profile"
onClick={() => sessionStore.signOut(true)}
onClick={() => sessionStore.signOut()}
>
{t('edit_profile')}
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item onClick={() => sessionStore.signOut(true)}>
{t('sign_out')}
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
) : (
<Button variant="outline-light" href="/user/me">
{t('sign_in')}
</Button>
)}
<LanguageMenu />
</>
Expand Down
3 changes: 2 additions & 1 deletion configuration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ export const isServer = () => typeof window === 'undefined';
export const Name = process.env.NEXT_PUBLIC_SITE_NAME,
Summary = process.env.NEXT_PUBLIC_SITE_SUMMARY;

export const { JWT_SECRET, GITHUB_PAT, VERCEL } = process.env;
export const { NODE_ENV, JWT_SECRET, GITHUB_PAT, VERCEL } = process.env;

export const isProduction = NODE_ENV === 'production';
export const API_HOST = process.env.NEXT_PUBLIC_API_HOST;

export const { token, JWT } = (globalThis.document ? parseCookie() : {}) as Record<
Expand Down
9 changes: 5 additions & 4 deletions models/Activity/Award.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { User, Base, Media, Team } from '@freecodecamp-chengdu/hop-service';
import { ListModel, Stream, toggle } from 'mobx-restful';

import { createListStream, InputData } from '../Base';
import { createListStream, InputData, TableModel } from '../Base';
import sessionStore from '../User/Session';

export interface Award extends Record<'hackathonName' | 'name' | 'description', string>, Base {
Expand All @@ -11,19 +11,20 @@ export interface Award extends Record<'hackathonName' | 'name' | 'description',
}

export interface AwardAssignment
extends Omit<Base, 'id'>,
extends
Omit<Base, 'id'>,
Omit<Award, 'name' | 'quantity' | 'target' | 'pictures'>,
Record<'assignmentId' | 'assigneeId' | 'awardId', number> {
user?: User;
team?: Team;
award: Award;
}

export class AwardModel extends Stream<Award>(ListModel) {
export class AwardModel extends TableModel<Award> {
client = sessionStore.client;
currentAssignment?: AwardAssignmentModel;

constructor(baseURI: string) {
constructor(public baseURI: string) {
super();
this.baseURI = `${baseURI}/award`;
}
Expand Down
15 changes: 8 additions & 7 deletions models/User/Session.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Base, User } from '@freecodecamp-chengdu/hop-service';
import { clear } from 'idb-keyval';
import { HTTPClient } from 'koajax';
import { computed, observable } from 'mobx';
import { BaseModel, persist, restore, toggle } from 'mobx-restful';
import { buildURLData, setCookie } from 'web-utility';
import { buildURLData, setCookie, sleep } from 'web-utility';

import { API_HOST, isServer, JWT, token } from '../../configuration';

Expand All @@ -15,22 +16,20 @@ export const ownClient = new HTTPClient({ baseURI: API_HOST, responseType: 'json
);

export interface SessionUser
extends Base,
Record<'username' | 'email', string>,
Record<'confirmed' | 'blocked', boolean> {
extends Base, Record<'username' | 'email', string>, Record<'confirmed' | 'blocked', boolean> {
provider: 'local' | 'github';
gender?: 'Female' | 'Male' | 'Other';
}

export class SessionModel extends BaseModel {
client = ownClient;

restored = !isServer() && restore(this, 'Session');

@persist()
@observable
accessor user: User | undefined;

restored = !isServer() && restore(this, 'Session');

@computed
get metaOAuth() {
return { github: { accessToken: token } };
Expand Down Expand Up @@ -59,11 +58,13 @@ export class SessionModel extends BaseModel {
return body!;
}

signOut(reload = false) {
async signOut(reload = false) {
setCookie('token', '', { path: '/', expires: new Date() });
setCookie('JWT', '', { path: '/', expires: new Date() });

this.user = undefined;
await sleep();
await clear();

if (reload) location.reload();
Comment on lines +61 to 69
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

🌐 Web query:

In idb-keyval v6.x, can clear() reject when IndexedDB is unavailable or blocked (e.g., private mode / storage restrictions)?

💡 Result:

Yes. In idb-keyval v6.x, clear returns a Promise that rejects if the underlying IndexedDB request/transaction fails (which includes cases where IndexedDB can’t be opened/used due to privacy/storage restrictions).

Citations:


🏁 Script executed:

# First, locate and read the Session.ts file around the target lines
fd -t f "Session.ts" -x wc -l {}

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 96


🏁 Script executed:

# Read the file content around lines 61-69
fd -t f "Session.ts" -x head -75 {} | tail -20

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 518


🏁 Script executed:

# Also check the imports and structure
fd -t f "Session.ts" -x head -10 {}

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 542


🏁 Script executed:

# Search for signOut call sites
rg "signOut\(" --type ts --type tsx -B 2 -A 2

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 96


🏁 Script executed:

# Search for signOut call sites (without tsx type filter)
rg "signOut\(" --type ts -B 2 -A 2

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 1326


🏁 Script executed:

# Also try broader search for .tsx files
rg "signOut\(" -B 2 -A 2 | head -50

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 1326


Harden sign-out against IndexedDB cleanup failures.

signOut() is async and awaits clear() at line 67, but all call sites invoke it without error handling (pages/user/[id].tsx and components/User/UserBar.tsx click handlers). When IndexedDB is unavailable or blocked (private mode, storage restrictions), clear() rejects, causing an unhandled promise rejection and preventing line 69's reload from executing.

🛡️ Suggested resilient implementation
 async signOut(reload = false) {
   setCookie('token', '', { path: '/', expires: new Date() });
   setCookie('JWT', '', { path: '/', expires: new Date() });

   this.user = undefined;
   await sleep();
-  await clear();
+  if (!isServer()) {
+    try {
+      await clear();
+    } catch {
+      // Keep sign-out successful even if client storage cleanup fails
+    }
+  }

-  if (reload) location.reload();
+  if (reload && !isServer()) location.reload();
 }
📝 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
async signOut(reload = false) {
setCookie('token', '', { path: '/', expires: new Date() });
setCookie('JWT', '', { path: '/', expires: new Date() });
this.user = undefined;
await sleep();
await clear();
if (reload) location.reload();
async signOut(reload = false) {
setCookie('token', '', { path: '/', expires: new Date() });
setCookie('JWT', '', { path: '/', expires: new Date() });
this.user = undefined;
await sleep();
if (!isServer()) {
try {
await clear();
} catch {
// Keep sign-out successful even if client storage cleanup fails
}
}
if (reload && !isServer()) location.reload();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/User/Session.ts` around lines 61 - 69, The signOut function can throw
if clear() (IndexedDB cleanup) rejects, causing unhandled rejections and
preventing the subsequent reload; update async signOut(reload = false) to catch
and swallow errors from clear() (e.g., try { await clear(); } catch (err) { /*
log/warn but continue */ }) so failures in clear() do not block this.user reset
or the optional location.reload(), and add a minimal debug log inside the catch
to aid diagnosis; ensure callers in pages/user/[id].tsx and
components/User/UserBar.tsx can continue to call signOut() without surrounding
error handling.

}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"array-unique-proposal": "^0.3.4",
"classnames": "^2.5.1",
"echarts-jsx": "^0.6.0",
"idb-keyval": "^6.2.2",
"idea-react": "^2.2.2",
"jsonwebtoken": "^9.0.3",
"koa": "^3.2.0",
Expand Down
3 changes: 3 additions & 0 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
loadSSRLanguage,
} from '../models/Base/Translation';
import styles from './_app.module.less';
import sessionStore from '../models/User/Session';

configure({ enforceActions: 'never' });

Expand All @@ -45,6 +46,8 @@ export default class CustomApp extends App<I18nProps> {

if (tips) alert(tips);
});

sessionStore.getProfile().catch(console.debug);
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

Replace console.debug with an allowed logger (or remove it).

Line 50 violates no-console (only warn, error, info are allowed in this config).

✅ Minimal lint-safe patch
-    sessionStore.getProfile().catch(console.debug);
+    sessionStore.getProfile().catch(console.info);
📝 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
sessionStore.getProfile().catch(console.debug);
sessionStore.getProfile().catch(console.info);
🧰 Tools
🪛 ESLint

[error] 50-50: Unexpected console statement. Only these console methods are allowed: warn, error, info.

(no-console)

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

In `@pages/_app.tsx` at line 50, The call
sessionStore.getProfile().catch(console.debug) uses console.debug which violates
the no-console rule; replace it with an allowed logger or remove the catch
handler. Update the catch to use an approved method (e.g.,
sessionStore.getProfile().catch(err => logger.warn('getProfile failed', err)) or
.catch(err => console.info(err)) if a shared logger isn't available), or remove
the .catch entirely if upstream handles errors; locate the occurrence by
searching for sessionStore.getProfile() in the file and change the catch
argument from console.debug to an allowed logger method (warn/info/error) or a
no-op.

}

render() {
Expand Down
5 changes: 0 additions & 5 deletions pages/activity/create.module.less
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@
}

.form-card {
position: relative;
z-index: 10;
margin-top: -3rem;
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.1);
border-radius: 1.5rem;
background: white;
}

.icon-circle {
Expand Down
18 changes: 12 additions & 6 deletions pages/activity/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,18 @@ const ActivityCreatePage: FC<JWTProps> = observer(() => {
</hgroup>
</section>

<section className="pb-5 bg-body-tertiary">
<Card className={classNames(styles['form-card'], 'border-0 mx-auto')}>
<Card.Body className="p-4 p-md-5">
<ActivityEditor />
</Card.Body>
</Card>
<section className="py-5 bg-body-tertiary">
<Container>
<Row className="justify-content-center">
<Col lg={10} xl={8}>
<Card className={classNames(styles['form-card'], 'border-0 rounded-4')}>
<Card.Body className="p-4 p-md-5">
<ActivityEditor />
</Card.Body>
</Card>
</Col>
</Row>
</Container>
</section>
</>
);
Expand Down
22 changes: 17 additions & 5 deletions pages/api/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import {
withKoa,
} from 'next-ssr-middleware';

import { JWT_SECRET } from '../../configuration';
import { VERCEL } from '../../configuration';
import { isProduction, JWT_SECRET, VERCEL } from '../../configuration';
import { SessionModel } from '../../models/User/Session';

export type JWTContext = ParameterizedContext<
Expand Down Expand Up @@ -83,14 +82,27 @@ export const jwtSigner: SSRM<DataObject, JWTProps<User>> = async ({ req, res },

const user = await SessionModel.signInWithGitHub(token!);

res.setHeader('Set-Cookie', `JWT=${user.token}; Path=/`);
res.setHeader(
'Set-Cookie',
[`JWT=${user.token}`, 'Path=/', isProduction ? 'Secure' : '', 'SameSite=Lax']
.filter(Boolean)
.join('; '),
);

return { props: { jwtPayload: JSON.parse(JSON.stringify(user)) } };
}
};

const client_id = process.env.GITHUB_OAUTH_CLIENT_ID!,
client_secret = process.env.GITHUB_OAUTH_CLIENT_SECRET!;
const client_id = process.env.GITHUB_OAUTH_CLIENT_ID,
client_secret = process.env.GITHUB_OAUTH_CLIENT_SECRET;

if (!client_id || !client_secret)
throw new ReferenceError(
`[OAuth Config Error] Missing required environment variables:
- GITHUB_OAUTH_CLIENT_ID
- GITHUB_OAUTH_CLIENT_SECRET
Please configure them in .env.local or environment settings.`,
);

Comment thread
dethan3 marked this conversation as resolved.
export const ProxyBaseURL = 'https://test.hackathon.fcc-cd.dev/proxy';

Expand Down
52 changes: 52 additions & 0 deletions pages/user/[id].module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.hero-section {
background: radial-gradient(circle at top left, #312e81, #0f172a 55%, #020617);
padding: 3rem 0 6rem;
}

.avatar-placeholder {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
width: 7.5rem;
height: 7.5rem;
}

.social-btn {
transition: all 0.2s ease;
background: rgba(59, 130, 246, 0.08);
color: #64748b;

&:hover {
background: #2563eb;
color: white;
}

&.active {
background: #2563eb;
color: white;
}
}

.custom-tabs {
gap: 0.5rem;
margin-bottom: 1.5rem;
border: none;

:global(.nav-link) {
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

Stylelint is currently failing on CSS Modules :global selectors.

Line 33 and Line 47 trigger selector-pseudo-class-no-unknown, so this file won’t pass lint as configured.

💡 Local fix option in this file
 .custom-tabs {
   gap: 0.5rem;
   margin-bottom: 1.5rem;
   border: none;

+  /* stylelint-disable-next-line selector-pseudo-class-no-unknown */
   :global(.nav-link) {
@@
-    &:global(.active) {
+    /* stylelint-disable-next-line selector-pseudo-class-no-unknown */
+    &:global(.active) {
       background: `#2563eb`;
       color: white;
     }
   }
 }

Also applies to: 47-47

🧰 Tools
🪛 Stylelint (17.9.0)

[error] 33-33: Unknown pseudo-class selector ":global" (selector-pseudo-class-no-unknown)

(selector-pseudo-class-no-unknown)

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

In `@pages/user/`[id].module.less at line 33, Stylelint flags the CSS Modules
:global(...) pseudo-class as unknown for selectors like :global(.nav-link) (and
the other :global(...) at the later selector). Add an inline stylelint disable
comment immediately above each offending selector, e.g. /*
stylelint-disable-next-line selector-pseudo-class-no-unknown */ so stylelint
ignores the :global pseudo-class for those lines, or alternatively update
project stylelint config to allow the :global pseudo-class if you prefer a
global fix; target the specific selectors :global(.nav-link) and the other
:global(...) occurrences when applying the change.

transition: all 0.2s ease;
border: none;
border-radius: 0.75rem;
background: rgba(59, 130, 246, 0.05);
padding: 0.75rem 1.5rem;
color: #64748b;
font-weight: 500;

&:hover {
background: rgba(59, 130, 246, 0.1);
color: #2563eb;
}

&:global(.active) {
background: #2563eb;
color: white;
}
}
}
Loading
Loading