A full-stack room booking platform built for the Intania (Faculty of Engineering, Chulalongkorn University) community. Users log in via LINE or email/password, browse available rooms, and submit reservation requests. Admins review and approve bookings through a separate dashboard.
Live: https://intaroomv31.vercel.app
Stack: Next.js 15 · TypeScript · Supabase · LINE LIFF · Tailwind CSS · Vercel
- How the App Works
- Authentication Architecture
- Project Structure
- Local Development Setup
- Database Setup
- Admin Account Setup
- Running Tests
- Deployment
- Known Issues & What to Do Next
There are two separate user types with completely different auth flows:
- Visit
/login, choose "User" tab - Log in with LINE (via LIFF) or email + password (Supabase Auth)
- First-time users complete a profile (full name, telephone) at
/profile - Browse rooms at
/→ submit a reservation at/reserve - View booking history and QR codes at
/my-reservations - Reservation status: Pending → Approved/Rejected (admin decides)
- Visit
/login, choose "Admin" tab - Log in with username + password (stored in
admin_profilesSupabase table, hashed with bcrypt) - Server sets an HttpOnly
isAdmincookie (7-day expiry) - Admin sees
/admindashboard: manage reservations, approve/reject, view analytics, generate QR codes /admin/calendar— visual calendar view of all bookings/admin/qr-generator— manual QR code generation
User submits → status: "pending"
Admin approves → status: "approved" → QR code generated
Admin rejects → status: "rejected"
User cancels → status: "cancelled"
This is the most important thing to understand before touching auth code.
| Regular Users | Admin Users | |
|---|---|---|
| Credential store | Supabase Auth (built-in) | admin_profiles table (bcrypt hash) |
| Session token | Supabase session cookie | HttpOnly isAdmin cookie |
| LINE support | Yes (LIFF) | No |
| Middleware guard | Supabase session check | isAdmin cookie check |
proxy.ts is the auth guard for every route. Next.js treats files named middleware.ts specially — but this project names it proxy.ts and registers it via vitest.config.ts and __tests__/proxy.test.ts. The function proxy() runs on every request before the page renders.
Request
│
├─ /_next/*, /api/*, static files → pass through (no auth check)
│
├─ /admin/* ──→ isAdmin cookie === "true"? → allow
│ └─ else: check Supabase session + profiles.role === "admin"
│ └─ else: redirect /login
│
├─ /login, /register, /forgot-password, /auth/* → allow (public)
│
└─ everything else → valid Supabase session? → allow
└─ else: redirect /login?redirectedFrom=<path>
POST /api/admin/login
1. Rate limit check (5 attempts / 15 min per IP, in-memory)
2. Look up username in admin_profiles table (via service role key, bypasses RLS)
3. bcrypt.compare(password, stored_hash)
4. On success → set HttpOnly isAdmin=true cookie (SameSite=Strict, 7 days)
GET /api/admin/verify
→ reads isAdmin cookie server-side → { isAdmin: true/false }
→ used by client components to check auth without touching localStorage
POST /api/admin/logout
→ clears the isAdmin cookie
Important: Admin auth does NOT use localStorage. Any code that reads localStorage.getItem("isAdmin") is a bug — the check was removed in April 2025. Always use fetch("/api/admin/verify") instead.
Contains two server-side helpers:
getAdminSession(req, res)— used inproxy.tsmiddleware, checks Supabase session +profiles.rolerequireAdmin()— used in Server Components/Route Handlers, redirects to/loginif not admin
Intaroom/
├── app/ # Next.js App Router
│ ├── layout.tsx # Root layout (fonts, providers)
│ ├── page.tsx # Home / room listing
│ ├── env.ts # Validates + exports env vars at startup
│ ├── globals.css
│ │
│ ├── admin/ # Admin-only pages (guarded by proxy.ts)
│ │ ├── page.tsx # Main dashboard (reservations table + analytics)
│ │ ├── calendar/ # Calendar view of all bookings
│ │ └── qr-generator/ # Manual QR code generation
│ │
│ ├── api/
│ │ └── admin/
│ │ ├── login/route.ts # POST — bcrypt verify, sets isAdmin cookie
│ │ ├── logout/route.ts # POST — clears isAdmin cookie
│ │ └── verify/route.ts # GET — checks isAdmin cookie server-side
│ │
│ ├── login/page.tsx # Login page (user + admin tabs)
│ ├── register/ # Email registration
│ ├── profile/ # User profile setup
│ ├── reserve/ # Reservation form
│ ├── my-reservations/ # User's booking history + QR codes
│ ├── reservation-details/ # Single reservation detail
│ ├── summary/ # Booking summary
│ └── auth/callback/ # Supabase OAuth callback
│
├── components/
│ ├── ui/ # shadcn/ui primitives (button, card, chart, …)
│ ├── auth-wrapper.tsx # Client-side auth guard for user pages
│ ├── liff-provider.tsx # LINE LIFF context
│ ├── logout-button.tsx # Handles both user and admin logout
│ └── line-profile.tsx # Displays LINE user info
│
├── lib/
│ ├── admin-auth.ts # Server-side admin auth helpers
│ ├── admin-api.ts # Admin data fetching utilities
│ ├── supabase.ts # Supabase client (browser + server)
│ ├── reservation-utils.ts # Booking conflict detection, validation
│ ├── user-validation.ts # User input validation helpers
│ ├── cors.ts # CORS header utilities
│ └── utils.ts # General utilities (cn, etc.)
│
├── hooks/
│ ├── use-mobile.tsx # Responsive breakpoint hook
│ ├── use-telephone-validation.tsx
│ └── use-toast.ts
│
├── __tests__/
│ ├── proxy.test.ts # Middleware auth guard tests
│ ├── setup.ts # Vitest global mocks (next/server, Supabase)
│ ├── api/admin/ # Admin API route tests
│ └── lib/ # Unit tests for lib utilities
│
├── db_setup/
│ ├── DATABASE_SETUP.md # Step-by-step DB setup guide
│ ├── database-schema.sql # Core tables
│ ├── auth-and-users.sql # Auth + profiles setup
│ ├── migrations-and-fixes.sql # Incremental migrations
│ └── fix-reservations-profiles-relationship.sql
│
├── scripts/
│ ├── generate-qr-codes.js # CLI script to bulk-generate QR codes
│ └── test-admin-performance.js # Manual perf testing script
│
├── docs/ # Extended developer documentation
│ ├── ADMIN_DASHBOARD_OPTIMIZATION.md
│ ├── SUPABASE_AUTH_CONFIG.md
│ └── …
│
├── proxy.ts # Auth middleware (runs on every request)
├── next.config.mjs # Security headers, image config
├── vercel.json # Vercel region (sin1), headers
├── tailwind.config.ts
├── tsconfig.json
└── vitest.config.ts
- Node.js 18+
- npm (the only lock file is
package-lock.json— don't use pnpm or yarn) - A Supabase project
- (Optional) A LINE Developers account for LIFF
npm installCopy from the example and fill in real values:
cp .env.example .env.local| Variable | Where to find it | Required |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Supabase → Settings → API | Yes |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Supabase → Settings → API | Yes |
SUPABASE_SERVICE_ROLE_KEY |
Supabase → Settings → API (secret) | Yes |
NEXT_PUBLIC_APP_URL |
Your Vercel URL or http://localhost:3000 |
Yes |
NEXT_PUBLIC_LIFF_ID |
LINE Developers console | No |
Never commit .env.local or any file containing the service role key.
npm run devVisit http://localhost:3000
All SQL lives in db_setup/. Run these in order in the Supabase SQL Editor:
1. database-schema.sql — tables: rooms, reservations, profiles, admin_profiles
2. auth-and-users.sql — RLS policies, triggers for new user profiles
3. migrations-and-fixes.sql — incremental changes (safe to re-run)
4. fix-reservations-profiles-relationship.sql — FK fix (run if you see relation errors)
Read db_setup/DATABASE_SETUP.md for the full annotated walkthrough.
| Table | Purpose |
|---|---|
profiles |
Regular user profiles linked to Supabase Auth (id = auth.users.id) |
admin_profiles |
Admin accounts: username, password_hash (bcrypt) |
rooms |
Room definitions (name, capacity, image, description) |
reservations |
Bookings: FK to profiles + rooms, status field |
Admin credentials are stored in the admin_profiles table as bcrypt hashes. There is no sign-up flow — an admin must insert the row manually.
node -e "const b = require('bcryptjs'); b.hash('YOUR_PASSWORD', 12).then(console.log)"INSERT INTO admin_profiles (username, password_hash)
VALUES ('your_username', '$2a$12$...(bcrypt hash here)...');Go to /login → Admin tab → enter username + password.
On success, the server sets an HttpOnly isAdmin cookie and redirects to /admin.
npm test # run all tests once
npm run test:watch # watch mode
npm run test:coverage # coverage report → coverage/Tests use Vitest with Node environment. The test suite covers:
proxy.tsmiddleware — all auth scenarios (cookie, Supabase fallback, public routes)lib/admin-auth.ts—getAdminSessionandrequireAdminlib/cors.ts,lib/reservation-utils.ts, filter/state utilities- Admin API routes
Coverage thresholds are set in vitest.config.ts (80% lines/branches).
The app deploys to Vercel (Singapore region, sin1).
- Push to
main→ Vercel auto-deploys - Set all environment variables in Vercel → Settings → Environment Variables
- In Supabase → Authentication → URL Configuration:
- Site URL:
https://intaroomv31.vercel.app - Redirect URLs:
https://intaroomv31.vercel.app/auth/callback
- Site URL:
Set in both next.config.mjs (dev + prod) and vercel.json (prod):
X-Frame-Options: DENYX-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-origin- CORS restricted to same origin for
/api/admin/*
| Issue | Detail |
|---|---|
| In-memory rate limiter | app/api/admin/login/route.ts uses a module-level Map for rate limiting. This resets on every cold start and doesn't share state across Vercel instances. Replace with Upstash Rate Limit + Redis when traffic increases. |
| No real middleware file | proxy.ts is a manually-named middleware. Next.js only auto-runs middleware.ts at the root. Check whether proxy.ts is being wired up correctly — if not, rename it to middleware.ts and update vitest.config.ts imports. |
admin_profiles password migration |
If the database still has SHA-256 hashes (old format), they must be re-generated as bcrypt before login works. See Admin Account Setup. |
| Feature | Notes |
|---|---|
| Email notifications | Send confirmation/rejection emails to users. Supabase has an Edge Functions trigger system. |
| Real-time status updates | Use Supabase Realtime to push reservation status changes to users without polling. |
| LINE push notifications | When a reservation is approved/rejected, send a LINE message via the LINE Messaging API. |
| Pagination on admin dashboard | The dashboard loads all reservations; add server-side pagination for large datasets. |
| Room image uploads | Currently room images are stored as URLs. Add Supabase Storage for direct uploads. |
| Multi-language support | UI is Thai/English mixed. Pick one or add i18n. |
Only use npm. package-lock.json is the single lock file. Do not run pnpm install or yarn — it will generate a second lock file and cause version drift.
- No
localStoragefor auth state. Usefetch("/api/admin/verify")to check admin session. Use SupabasegetSession()for user session. - Server-side secrets. Only
NEXT_PUBLIC_*vars are available in the browser.SUPABASE_SERVICE_ROLE_KEYmust never appear in client code. app/env.tsvalidates required env vars at startup — if a required var is missing the server throws immediately rather than failing silently at runtime.- Commit style:
type: short description— types arefeat,fix,refactor,docs,test,chore.