sakibkx
Start a project

Bilingual site and CMS for an education association

An association connecting Chinese and Bangladeshi universities needed its whole site in English and Chinese, edited by staff with no developer. I built it end to end, then ran a full security hardening pass on the live system.

RoleBuilt the site and CMS end to end. Ran the security hardening pass.
Written by me88.7% of the code
Year2026
StatusLive
Visit cbecea.com
cbecea.com
The CBECEA homepage and its programmes directory
Measured
Audit register68severity-graded findings in design/ISSUES.md with per-file line references, across security, accessibility, SEO, bugs and design
Security findings closed11 of 11section E of design/ISSUES.md, closed in commits b12c5c5, 375832c and 7ba1313
Write routes guarded18 of 18grep across every API route file with a write handler, cross-checked against auth calls
HTTP handlers45exported GET/POST/PATCH/DELETE across 22 route files, plus 24 server actions
Translation sync174 / 174recursive leaf-key walk over both locale catalogues, key sets identical in both directions

How authorship was measured. 31,666 of 35,711 lines, measured with per-file git blame across all tracked text files, excluding lockfiles and binaries.

The problem

A non-profit association connecting Chinese and Bangladeshi universities needed its entire website published in English and Chinese, edited by staff with no developer involved: programmes, services, events, news, leadership, membership tiers. It also had to stand up to the attention a public association site attracts, from bots as much as from people.

What I built

Bilingual content is stored as pairs. Every item exists as an English row and a Chinese row sharing a slug, written together in one operation. The update path refuses to run if the pair count is not exactly two. Bilingual sites almost always rot into English-only rows with orphaned translations; this makes that structurally hard. A script walks both locale catalogues and confirms that the 174 keys are identical in both directions.

Every write route is authenticated and authorised. Eighteen write handlers across the API, each checked against the session and the caller’s role, verified route by route. Members cannot approve their own applications, drafts stay private until they are published, and a failed file read never reveals the server’s layout.

Rate limiting is two layers. A signed state cookie with four rule tiers, and behind it a separate in-process counter that is checked first, because a cookie-only limiter can be reset by discarding the cookie.

The upload path rejects on content length before it buffers anything, then re-checks the bytes actually read, because a client-supplied length cannot be trusted. Everything accepted is transcoded to WebP under a random filename, which is what closes the stored-XSS vector rather than filtering extensions.

Image loading is locked to known hosts, so the site cannot be used as an open image proxy.

The hardening pass

Once the site was live I ran a full audit of it and kept a severity-graded register: 68 findings across security, accessibility, SEO, functional bugs and the design system, each with a file and line reference. Every security finding is closed in code, with the commits recorded against the register. The register itself is a document a client can read.

Stack
  • Next.js 16
  • TypeScript
  • MongoDB
  • Auth.js
  • sharp
  • nginx
  • PM2