A cyberpunk, Tokyo‑flavoured portfolio with a WebGL Sharingan, a VAPT request desk,
a bug bounty Hall of Fame and a hardened, puzzle‑gated admin console. Every word on it lives in MongoDB.
▶ Open the live site · Features · Tour · Architecture · Security · Quick start · Deploy
// 01 · 機能
|
👁 WebGL Sharingan |
🛰 VAPT desk |
🏆 Bug bounty Hall of Fame |
|
✉ Program invites |
🐙 GitHub sync |
🔗 LinkedIn import |
|
🔐 Hardened admin console |
📝 Markdown blog |
⚡ Static and fresh |
On a first visit an experience gate asks for Safe mode or Glitch mode, like the photosensitivity warnings in games. Safe mode, and prefers-reduced-motion, calm the 3D scene and switch off flicker.
// 02 · 画面
Admin console, Requests tab (demo data): status pipeline, private notes, reply by email
// 03 · 構成
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#12141c", "primaryTextColor": "#e9e2d0", "primaryBorderColor": "#ff1f1f", "lineColor": "#ff1f1f", "secondaryColor": "#1b1e29", "tertiaryColor": "#0d0f15", "clusterBkg": "#0d0f15", "clusterBorder": "#ff1f1f", "titleColor": "#e9e2d0", "edgeLabelBackground": "#12141c", "fontFamily": "ui-monospace, SFMono-Regular, Menlo, monospace"}}}%%
flowchart LR
subgraph C["Browser"]
V["Visitor"]
A["Admin"]
end
subgraph E["Vercel"]
P["Public pages<br/>Server Components · ISR 60 s"]
F["Public APIs<br/>contact · engagements"]
X["Admin APIs<br/>session + origin check"]
end
subgraph D["Data & services"]
M[("MongoDB Atlas<br/>via Prisma 6")]
G["GitHub REST API"]
B["Vercel Blob<br/>résumé PDF"]
end
V --> P
V -- "zod · honeypot · rate limit" --> F
A -- "PIN → session cookie" --> X
P -- "reads · private programs redacted" --> M
F --> M
X --> M
X -- "repo sync" --> G
X -- "upload" --> B
X -. "revalidate on save" .-> P
- One data layer.
src/lib/portfolio-data.tsloads everything a page needs in parallel, redacts private bug bounty programs and hides empty sections. Public pages never call the API routes. - One CRUD factory.
collectionHandlers()insrc/lib/api.tsgives every collection admin‑only writes, zod validation, clean 400/404/409 errors and instant revalidation. - Admin is a client app in
src/components/admin/*that talks to the same API routes.
// 04 · 防御
A security portfolio should survive being poked at, so the controls are written down here.
%%{init: {"theme": "base", "themeVariables": {"actorBkg": "#12141c", "actorBorder": "#ff1f1f", "actorTextColor": "#e9e2d0", "actorLineColor": "#8b949e", "signalColor": "#ff1f1f", "signalTextColor": "#ff1f1f", "labelBoxBkgColor": "#12141c", "labelBoxBorderColor": "#ff1f1f", "labelTextColor": "#e9e2d0", "loopTextColor": "#ff1f1f", "noteBkgColor": "#12141c", "noteBorderColor": "#1fd6c6", "noteTextColor": "#e9e2d0", "fontFamily": "ui-monospace, SFMono-Regular, Menlo, monospace"}}}%%
sequenceDiagram
autonumber
participant B as Browser
participant L as POST /api/admin/login
participant DB as MongoDB
B->>L: PIN (6–12 digits)
L->>DB: recent failures (this IP / all IPs)
alt locked out
L-->>B: 429 + Retry-After
else allowed
L->>L: scrypt verify (or constant-time compare with ADMIN_PIN)
L->>DB: log attempt · store SHA-256(session token)
L-->>B: Set-Cookie admin_session (HttpOnly · Secure · SameSite=Strict)
end
Note over B,DB: every admin call: Origin check → session lookup → 12 h absolute / 2 h idle expiry
| Surface | Control |
|---|---|
| Admin PIN | 6–12 digits. A new PIN is rejected if it is weak (repeats, sequences, common PINs). It is stored as scrypt (N=16384, r=8, p=1, 16‑byte salt). The first‑login ADMIN_PIN is compared in constant time. |
| Brute force | 5 wrong PINs from one IP lock that IP for 15 min. 40 failures across all IPs lock everyone for 5 min. Every attempt is logged and kept for 30 days. |
| Sessions | 32 random bytes. Only the SHA‑256 hash is stored. The cookie is HttpOnly, Secure and SameSite=Strict. Sessions end after 12 h, or 2 h without activity. Other devices can be signed out from Security. |
| CSRF | A state‑changing admin request with a foreign Origin gets a 403. |
| Public forms | zod validation, a honeypot (bots get a fake success) and hourly limits per IP and overall (4/40 for requests, 5/60 for contact). Submissions store only a truncated SHA‑256 of the sender's IP. |
| Private programs | Redacted on the server before rendering. The findings list API is admin‑only. |
| Content | Blog Markdown renders to React elements. No raw HTML is injected. |
| Headers | HSTS (2 years, preload), X-Frame-Options: DENY, nosniff, strict Referrer-Policy, a Permissions-Policy that turns off camera, mic and geolocation, and no X-Powered-By. |
| Obscurity | /admin is a decoy and the real console sits behind a puzzle. That's for fun only; none of the controls above depend on it. |
Found something? Please report it privately through the contact form instead of opening a public issue.
// 05 · 技術
| Layer | Tech |
|---|---|
| Framework | Next.js 15 (App Router, Server Components, ISR) · React 18 · TypeScript (strict) |
| 3D & motion | three.js with custom GLSL shaders · Motion (Framer Motion) |
| Styling | Tailwind CSS 3 · shadcn/ui on Radix primitives · lucide + react‑icons |
| Data | Prisma 6 · MongoDB Atlas · zod 4 |
| Files | Vercel Blob (résumé PDF) · fflate (LinkedIn ZIP, unzipped in the browser) |
| Quality | Jest 30 + Testing Library · ESLint · tsc --noEmit |
| Hosting | Vercel |
// 06 · 準備
You need Node.js 18.18+ (20 or 22 recommended) and a MongoDB Atlas cluster. The free tier works. Prisma needs a replica set, and Atlas always runs one.
git clone https://github.com/modhack2003/studio.git
cd studio
npm install # also runs prisma generate
cp .env.example .env # set DATABASE_URL and ADMIN_PIN
npm run db:push # create collections + indexes (safe, keeps data)
npm run prisma:seed # optional sample data; skipped if a profile exists
npm run dev # http://localhost:9002
The admin console, first login and day‑to‑day content workflow are covered in SETUP.md.
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
✅ | Atlas URI including the database name (…mongodb.net/portfolio?…). URL‑encode special characters in the password. |
ADMIN_PIN |
✅ first login | 6–12 digits. After you log in, set a new PIN in Security. It is stored hashed and this variable is then ignored. |
GITHUB_TOKEN |
– | Read‑only token that raises GitHub's rate limit for the sync. |
CRON_SECRET |
for scheduled sync | Random production secret authenticating the daily GitHub refresh. |
GITHUB_USERNAME |
– | Fallback when the profile has no GitHub URL (modhack2003). |
BLOB_READ_WRITE_TOKEN |
– | Only needed to upload a résumé PDF. Pasting a link works too. |
| Command | Does |
|---|---|
npm run dev |
Dev server with Turbopack on port 9002 |
npm run build / npm start |
Production build / serve |
npm run lint · npm run typecheck · npm test |
ESLint · tsc · Jest |
npm run db:push |
Sync collections and indexes with prisma/schema.prisma |
npm run prisma:seed |
Sample data (refuses to overwrite an existing profile unless SEED_FORCE=true) |
npm run github:sync |
Command‑line version of GitHub → Sync now |
// 07 · 展開
- Import the repo in Vercel. The Next.js preset is picked up automatically.
- Add the variables above under Settings → Environment Variables.
- From your machine, run
DATABASE_URL="<production uri>" npm run db:pushto create the collections and indexes, including content history. - For a new database, use the local admin console (
npm run dev) to save your real profile (name, title and bio) and at least one project or visible repository. Existing populated installations can skip this step. Do not seed example content into production. - Deploy.
postinstallgenerates Prisma;prebuildverifies the content/database before a release. A failed check keeps the previous successful deployment active. - Log in, set a new PIN in Security, then press GitHub → Sync now.
See availability, admin recovery and browser tests for Trash, revisions, backup limits and test commands.
Tip
Seeing Server selection timeout or bad auth? In Atlas, open Network Access to Vercel (for example 0.0.0.0/0), and check that the database name is part of DATABASE_URL.
// 08 · 管理
| Tab | What it does |
|---|---|
| Profile | Name, title, bio, avatar, social links, résumé (PDF upload or link) and bug bounty profile links |
| Experience · Projects · CTF · Education · Certificates · Skills | Add, edit and delete. The site updates right after you save. |
| GitHub | Sync now, then show/hide, pin or rename any repo |
| Bug bounty | Findings with severity, platform, bounty and currency, CVE, report link, and hall‑of‑fame / private‑program flags |
| Blog | Markdown posts, drafts, publish |
| Requests | VAPT requests and program invites: new → reviewing → accepted → completed / declined, private notes, reply by email |
| Inbox | Contact‑form messages |
| LinkedIn import | Drop the data‑export ZIP, preview, import |
| Security | Change the PIN, see active sessions, sign out other devices, view the login history |
API reference
| Route | Methods | Access |
|---|---|---|
/api/projects · /api/certificates · /api/education · /api/experience · /api/ctf |
GET · POST |
GET public, POST admin |
…/[id] for the collections above, plus /api/blog/[id] and /api/bounties/[id] |
PUT · DELETE |
admin |
/api/blog · /api/bounties |
GET · POST |
admin (drafts and private programs stay private) |
/api/personal-data · /api/skills |
GET · PUT |
GET public, PUT admin |
/api/contact |
POST |
public, rate‑limited + honeypot |
/api/engagements |
POST · GET |
POST public (rate‑limited + honeypot), GET admin |
/api/engagements/[id] · /api/messages/[id] |
PATCH · DELETE |
admin |
/api/messages |
GET |
admin |
/api/github/repos · /api/github/repos/[id] · /api/github/sync |
GET · PUT · DELETE · POST |
admin |
/api/import/linkedin · /api/resume/upload |
POST |
admin |
/api/admin/login · /api/admin/logout · /api/admin/session |
POST · POST · GET |
public |
/api/admin/security · /api/admin/security/pin · /api/admin/sessions |
GET · POST · DELETE |
admin |
/api/ctf/verify |
POST |
public, rate‑limited (puzzle check) |
Project structure
.
├── prisma/
│ ├── schema.prisma # 15 models: content, bounty findings, requests, sessions, login log
│ └── seed.ts # sample data; refuses to touch a database that already has a profile
├── scripts/sync-github.ts # npm run github:sync
├── src/
│ ├── app/
│ │ ├── page.tsx # the one-page site (ISR, revalidate 60 s)
│ │ ├── blog/ # /blog and /blog/[slug]
│ │ └── api/ # route handlers (public + admin)
│ ├── components/
│ │ ├── cyber/ # Sharingan (three.js), cursor, experience gate, form kit, primitives
│ │ ├── sections/ # hero, about, experience, projects, ctf, skills, vapt, bounty, invite, blog, contact
│ │ ├── admin/ # dashboard, collection editor, requests, inbox, GitHub, import, security
│ │ └── ui/ # shadcn/ui building blocks
│ └── lib/ # data layer, auth, zod validators, GitHub + LinkedIn import, markdown
└── .github/assets/ # README art: generated SVGs + screenshots
// 09 · 設計
- Ink, signal red, bone and cyan, defined once as HSL variables in
globals.cssand used everywhere, including the shaders. - Tektur for display, Space Grotesk for body text, JetBrains Mono for labels and Noto Sans JP for the Japanese section marks (経歴, 作品, 賞金稼ぎ…).
- Recurring details: hairline grids, bracket corners, cut‑corner cards, outlined headline words, marquees, scanlines, grain and a custom cursor.
- Motion respects the experience gate and
prefers-reduced-motion. The README art here does too.
The banner, terminal, divider, palette and footer above are hand-generated SVGs. The brand fonts are converted to vector paths so they render the same everywhere, animate with pure CSS, and sit still for reduced motion.
- Visual language inspired by @bikramdey2003.
- The Sharingan is a fan tribute to Naruto by Masashi Kishimoto.
- Fonts from Google Fonts (SIL Open Font License).





