ใน repository นี้เป็นแอปพลิเคชันสำหรับการเลือกตั้งคณะกรรมการสโมสรนิสิต คณะวิทยาศาสตร์ จุฬาลงกรณ์มหาวิทยาลัย โดยแบ่งเป็น 2 แอปพลิเคชันย่อยในรูปแบบ Monorepo
apps/backendเป็น Elysia Instance สำหรับรัน API ของแอปพลิเคชันapps/frontendเป็นเว็บ Astro สำหรับแสดงผลและให้ผู้ใช้โหวต
นอกจากแอปที่ใช้รันด้านบน จะมี package ที่ใช้สำหรับเก็บโค้ดที่ใช้ร่วมกันระหว่างแอปพลิเคชันทั้งสอง ได้แก่
packages/apiเป็น Elysia Instance นั่นแหละที่จะถูก Import ไปใช้ในapps/backendอีกทีหนึ่ง โดยการเขียน API จะเขียนที่นี่เป็นหลัก ส่วนapps/backendจะเป็นเพียงตัวที่ใช้ deploy เท่านั้นpackages/constantsเป็นที่เก็บค่าคงที่ต่าง ๆ ที่ใช้ร่วมกันระหว่างแอปพลิเคชันทั้งสอง เช่น ค่าต่าง ๆ ที่เกี่ยวกับการเลือกตั้ง ทั้งข้อมูลผู้สมัคร ตำแหน่ง และข้อความระบบต่าง ๆ ที่ใช้ในแอปพลิเคชัน
| ส่วน | เทคโนโลยี |
|---|---|
| Backend | Elysia บน Cloudflare Workers |
| Database | Cloudflare D1 (SQLite) + KV |
| Frontend | Astro + Svelte + React + Tailwind CSS v4 |
| Deploy | Cloudflare Workers (ทั้ง frontend และ backend) |
| Package Manager | pnpm (workspace) |
ถ้าไม่ได้บอกไว้เป็นอื่น ให้รันคำสั่งทั้งหมดจาก root ของ repository
# ติดตั้ง dependencies ทั้ง monorepo
pnpm installสร้างไฟล์ apps/backend/.dev.vars จาก example:
cp apps/backend/.dev.vars.example apps/backend/.dev.varsแก้ไขค่าตามต้องการ:
# สร้างได้จาก: openssl rand -hex 32
JWT_SECRET=your_jwt_secret_here# รัน backend + frontend พร้อมกัน
pnpm dev| แอป | URL |
|---|---|
| Frontend | http://localhost:4321 |
| Backend API | http://localhost:8787 |
pnpm --filter election-backend run db:seedBase URL (production): https://election-api.vidyachula.org
| Method | Path | คำอธิบาย |
|---|---|---|
GET |
/health |
Health check |
POST |
/auth/login |
Login ด้วย Google ID Token → JWT |
GET |
/auth/me |
ดูข้อมูลผู้ใช้ปัจจุบัน |
GET |
/election/voter-count |
จำนวนผู้ลงคะแนนทั้งหมด |
GET |
/election/eligibility |
ตรวจสอบสิทธิ์โหวตของผู้ใช้ (ต้องใช้ JWT) |
POST |
/election/cast-vote |
ส่งคะแนนโหวต (ต้องใช้ JWT) |
GET |
/election/result |
ผลการเลือกตั้ง (เข้าถึงได้หลังประกาศผล) |
OpenAPI spec: https://election-api.vidyachula.org/reference
ระบบใช้ Google OAuth โดย frontend ส่ง Google ID Token ไปที่ POST /auth/login แล้วรับ JWT session token กลับมา (หมดอายุใน 30 นาที) จากนั้นใช้ JWT ใน header Authorization: Bearer <token> สำหรับ endpoint ที่ต้องการ auth
เฉพาะ email ที่ลงท้ายด้วย 23@student.chula.ac.th เท่านั้นที่สามารถโหวตได้
pnpm deploy:staging| แอป | URL |
|---|---|
| Frontend | https://election-staging.vidyachula.org |
| Backend | https://election-api-staging.vidyachula.org |
pnpm deploy:production| แอป | URL |
|---|---|
| Frontend | https://election.vidyachula.org |
| Backend | https://election-api.vidyachula.org |
# Staging
pnpm --filter election-backend run db:seed:staging
# Production
pnpm --filter election-backend run db:seed:productionข้อมูลทั้งหมดที่ต้องแก้ไขอยู่ใน packages/constants/src/ เป็นหลัก ไม่ต้องแตะโค้ด backend หรือ frontend
ไฟล์: packages/constants/src/event.ts
// ชื่อการเลือกตั้ง
export const full_name = "การเลือกตั้ง...";
export const short_name_th = "...";
// วันเวลาเปิด-ปิดโหวต (ISO 8601, timezone +07:00)
export const votingStartString = "YYYY-MM-DDTHH:mm:ss+07:00";
export const votingEndString = "YYYY-MM-DDTHH:mm:ss+07:00";
// เปิด false ระหว่างโหวต → true เมื่อพร้อมประกาศผล
export const isResultAnnounced = false;
isResultAnnounced = trueทำให้GET /election/resultเปิดให้เข้าถึงได้
ไฟล์: packages/constants/src/candidates.ts — array running_positions
export const running_positions = [
{
position_id: "president" as const, // ต้อง unique, ใช้ใน DB
name: { th: "นายกสโมสรนิสิต", en: "President" },
},
// เพิ่ม/ลดตำแหน่งตามจริง
] satisfies Position[];
position_idถูกเก็บลงใน D1 ตรง ๆ อย่าเปลี่ยน ID ของ election ที่รันไปแล้ว
ไฟล์: packages/constants/src/candidates.ts — array parties และ candidates
พรรค:
export const parties = [
{
party_id: "my-party" as const,
name: { th: "...", en: "..." },
visions: { th: "...", en: "..." },
color: "#RRGGBB",
},
] satisfies Party[];ผู้สมัคร:
export const candidates = [
{
candidate_id: "c1" as const, // ต้อง unique
full_name: "ชื่อ นามสกุล",
study_year: 3,
study_program: { th: "...", en: "..." },
position_id: "president", // ต้องตรงกับ running_positions
party_id: "my-party", // ต้องตรงกับ parties
personal_vision: { th: "...", en: "..." },
personal_mission: { th: "...", en: "..." },
personal_experience: { th: "...", en: "..." },
image: "/c1.jpg", // path ใต้ apps/frontend/src/assets/
},
] satisfies Candidate[];รูปของผู้สมัครอยู่ใน apps/frontend/src/assets/ โดย path ใน field image ของ candidate เป็น path สัมพัทธ์จาก folder นี้ (ขึ้นต้นด้วย /)
apps/frontend/src/assets/
├── c1.jpg ← รูปผู้สมัคร candidate_id = "c1"
├── c1_remove.png ← รูปแบบ background removed (ใช้แสดงบนหน้าโหวต)
├── c2.jpg
├── c2_remove.png
└── ...
ขั้นตอน:
- เตรียมรูปในรูปแบบ
.jpgหรือ.pngขนาดแนะนำ 400×500px ขึ้นไป - วางไว้ใน
apps/frontend/src/assets/ตั้งชื่อให้ตรงกับcandidate_idเช่นc1.jpg - เตรียมรูปแบบ background removed (
.pngพื้นโปร่งใส) ใช้ชื่อc1_remove.png - ตรวจสอบว่า field
imageในcandidates.tsชี้ถูกต้อง เช่นimage: "/c1.jpg"
ฐานข้อมูล D1 เก็บ ballot และ voter ไว้ในตาราง ballots และ voters seed ใหม่จะไม่ลบข้อมูลเก่า ต้องทำผ่าน Cloudflare Dashboard หรือรัน SQL ตรง:
DELETE FROM ballots;
DELETE FROM voters;election/
├── apps/
│ ├── backend/ # Cloudflare Worker — Elysia API server
│ ├── frontend/ # Cloudflare Worker — Astro static site
│ └── frontendv2/ # (WIP) Next.js frontend
├── packages/
│ ├── api/ # Elysia app logic (shared, imported by backend)
│ └── constants/ # ข้อมูลผู้สมัคร ตำแหน่ง และวันเลือกตั้ง
└── readme.md
MIT — จัดทำโดย ฝ่าย IT สโมสรนิสิตคณะวิทยาศาสตร์ จุฬาลงกรณ์มหาวิทยาลัย