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
5 changes: 2 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: '1.2.18' # or pin to whatever Bun version you need
bun-version: "1.2.18" # or pin to whatever Bun version you need

- name: Cache Bun dependencies
uses: actions/cache@v4
Expand All @@ -30,10 +30,9 @@ jobs:

- name: Install dependencies
run: bun install

- name: Generate Prisma client
run: bun run generate

- name: Run tests
run: bun jest

12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ This repository contains the common Express.js API for the backends of our Codin
├── tests/ # integration and unit tests (Jest or Mocha)
│ ├── members.test.ts
│ └── ...
│ └── ...
├── .env.example # template for environment variables
├── package.json
Expand All @@ -65,7 +65,7 @@ This repository contains the common Express.js API for the backends of our Codin

### Prerequisite

* Install [Bun](https://bun.sh/) on your machine.
- Install [Bun](https://bun.sh/) on your machine.

### 1. Clone the repo

Expand All @@ -82,8 +82,8 @@ bun install

### 3. Configure environment

* Copy `.env.example` to `.env`
* Update `.env` with your Supabase/PostgreSQL connection URL and any other variables:
- Copy `.env.example` to `.env`
- Update `.env` with your Supabase/PostgreSQL connection URL and any other variables:

### 4. Initialize Prisma & Database

Expand All @@ -98,8 +98,8 @@ bun prisma generate
bun run dev
```

* By default, the server listens on `http://localhost:3000`
* `app.ts` sets up your Express instance; `server.ts` starts the HTTP listener
- By default, the server listens on `http://localhost:3000`
- `app.ts` sets up your Express instance; `server.ts` starts the HTTP listener

### 6. Run tests

Expand Down
14 changes: 7 additions & 7 deletions apidoc.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"name": "COC API",
"version": "1.0.0",
"name": "COC API",
"version": "1.0.0",
"description": "REST API for Coding Club backend",
"title": "Coding Club API Docs",
"title": "Coding Club API Docs",
"url": "http://localhost:3000/api/v1",
"sampleUrl": false,
"sampleUrl": false,
"template": {
"withCompare": true,
"sort": true
},
"output": "docs/apidoc",
"input": "src/routes",
"includeFilters": ["\\.ts$"]
"output": "docs/apidoc",
"input": "src/routes",
"includeFilters": ["\\.ts$"]
}
12 changes: 9 additions & 3 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import globals from "globals";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";


export default defineConfig([
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"] },
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], languageOptions: { globals: globals.browser } },
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
plugins: { js },
extends: ["js/recommended"],
},
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
languageOptions: { globals: globals.browser },
},
tseslint.configs.recommended,
]);
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
console.log("Hello via Bun!");
console.log("Hello via Bun!");
11 changes: 5 additions & 6 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

export default {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'js', 'json'],
testMatch: ['**/tests/**/*.test.ts'],
}
preset: "ts-jest",
testEnvironment: "node",
moduleFileExtensions: ["ts", "js", "json"],
testMatch: ["**/tests/**/*.test.ts"],
};
23 changes: 11 additions & 12 deletions singleton.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'
import { PrismaClient } from '@prisma/client'
import { mockDeep, mockReset, DeepMockProxy } from "jest-mock-extended";
import { PrismaClient } from "@prisma/client";

let mock: DeepMockProxy<PrismaClient>
let mock: DeepMockProxy<PrismaClient>;

jest.mock('./src/db/client', () => {
mock = mockDeep<PrismaClient>()
jest.mock("./src/db/client", () => {
mock = mockDeep<PrismaClient>();
return {
__esModule: true,
prisma: mock,
}
})
};
});

import { prisma } from "./src/db/client";

import { prisma } from './src/db/client'

export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>
export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>;

beforeEach(() => {
mockReset(prismaMock)
})
mockReset(prismaMock);
});
55 changes: 28 additions & 27 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,53 @@
// src/app.ts
import express from 'express'
import cors from 'cors'
import multer from 'multer'
import { json, urlencoded } from 'body-parser'
import routes from './routes'
import { errorHandler } from './utils/apiError'
import { createClient } from '@supabase/supabase-js'
import config from './config'
import path from 'path'

import express from "express";
import cors from "cors";
import multer from "multer";
import { json, urlencoded } from "body-parser";
import routes from "./routes";
import { errorHandler } from "./utils/apiError";
import { createClient } from "@supabase/supabase-js";
import config from "./config";
import path from "path";

// Initialize Supabase client for storage operations
export const supabase = createClient(
config.SUPABASE_URL,
config.SUPABASE_SERVICE_ROLE_KEY
)
config.SUPABASE_SERVICE_ROLE_KEY,
);

const app = express()
const app = express();

// 1) Enable CORS for your domains
app.use(cors({
origin: config.ALLOWED_ORIGINS.split(','), // e.g. 'https://club.example.com'
methods: ['GET','POST','PATCH','DELETE','OPTIONS'],
credentials: true,
}))
app.use(
cors({
origin: config.ALLOWED_ORIGINS.split(","), // e.g. 'https://club.example.com'
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
credentials: true,
}),
);

// 2) Parse JSON and form data
app.use(json())
app.use(urlencoded({ extended: true }))
app.use(json());
app.use(urlencoded({ extended: true }));

// 3) Handle file uploads (in-memory)
const upload = multer({ storage: multer.memoryStorage() })
const upload = multer({ storage: multer.memoryStorage() });

// 4) Mount your routes, injecting `upload` middleware where needed
// For endpoints that accept file uploads, you can do e.g.:
// router.post('/members/:memberId/photo', upload.single('photo'), ...)

app.use('/api/v1', routes(upload, supabase))
app.use("/api/v1", routes(upload, supabase));

// 5) 404 handler
app.use((req, res) => {
res.status(404).json({ message: 'Not Found' })
})
res.status(404).json({ message: "Not Found" });
});

// 6) Global error handler
app.use(errorHandler)
app.use(errorHandler);

// 7) do 'npm run apidoc to generate the documentation, I have added it in the scripts
// then you can go to localhost:3000/docs to see the docs
app.use('/docs', express.static(path.join(__dirname, '..', 'docs/apidoc')))
export default app
app.use("/docs", express.static(path.join(__dirname, "..", "docs/apidoc")));
export default app;
4 changes: 2 additions & 2 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export default {
DIRECT_URL: process.env.DIRECT_URL!,
SUPABASE_URL: process.env.SUPABASE_URL!,
SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY!,
ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS || '*'
}
ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS || "*",
};
Comment on lines +7 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Wildcard CORS fallback may be too permissive

ALLOWED_ORIGINS defaults to "*", allowing any origin when the env-var is absent. This is fine for local dev but risky in production. Consider failing fast or providing an explicit allow-list instead.

🤖 Prompt for AI Agents
In src/config/index.ts around lines 7 to 8, the ALLOWED_ORIGINS configuration
defaults to "*" which allows all origins and is too permissive for production.
Modify the code to either throw an error or require an explicit environment
variable value for ALLOWED_ORIGINS in production environments, ensuring no
wildcard fallback is used outside of local development. This will enforce a
safer CORS policy by preventing unintended open access.

49 changes: 24 additions & 25 deletions src/controllers/progress.controller.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
import { Request, Response } from "express";
import { ApiError } from "../utils/apiError";
import * as progressServices from "../services/progress.service"
import * as progressServices from "../services/progress.service";

export const getCompletedQuestion = async(req:Request,res:Response)=>{
const memberId = req.params.memberId;
if(!memberId){
throw new ApiError("required field is missing",400);
}
export const getCompletedQuestion = async (req: Request, res: Response) => {
const memberId = req.params.memberId;
if (!memberId) {
throw new ApiError("required field is missing", 400);
}

const completedQuestion = await progressServices.getCompletedQuestion(memberId);
res.status(200).json({
status:"SUCCESS",
completedQuestion
})
const completedQuestion =
await progressServices.getCompletedQuestion(memberId);
res.status(200).json({
status: "SUCCESS",
completedQuestion,
});
};

}
export const toggleQuestions = async (req: Request, res: Response) => {
const memberId = req.params.memberId;
const questionId = parseInt(req.params.questionId);
if (!memberId || !questionId) {
throw new ApiError("required field is missing", 400);
}
Comment on lines +20 to +24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Validation treats 0 the same as “missing” – use Number.isNaN

parseInt returns NaN for bad input. Your current guard if (!memberId || !questionId) will also fire when questionId === 0, a legitimate integer, yet won’t fire when questionId === "0abc" (because parseInt returns 0). Prefer an explicit NaN check:

-const questionId = parseInt(req.params.questionId);
-if (!memberId || !questionId) {
+const questionId = Number(req.params.questionId);
+if (!memberId || Number.isNaN(questionId)) {
   throw new ApiError("required field is missing", 400);
 }
📝 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
const memberId = req.params.memberId;
const questionId = parseInt(req.params.questionId);
if (!memberId || !questionId) {
throw new ApiError("required field is missing", 400);
}
const memberId = req.params.memberId;
const questionId = Number(req.params.questionId);
if (!memberId || Number.isNaN(questionId)) {
throw new ApiError("required field is missing", 400);
}
🤖 Prompt for AI Agents
In src/controllers/progress.controller.ts lines 20 to 24, the validation
incorrectly treats 0 as missing and fails to catch invalid questionId inputs.
Replace the current check with an explicit validation that uses Number.isNaN on
questionId to detect invalid numbers, and separately check if memberId is
missing. This ensures 0 is accepted as valid and invalid strings are properly
rejected.


export const toggleQuestions = async(req:Request,res:Response) =>{
const memberId = req.params.memberId;
const questionId = parseInt(req.params.questionId);
if(!memberId || !questionId){
throw new ApiError("required field is missing",400);
}

await progressServices.markQuestion(questionId,memberId);
res.status(200).json({
status:"SUCCESS",
})

}
await progressServices.markQuestion(questionId, memberId);
res.status(200).json({
status: "SUCCESS",
});
};
Loading