From d9824494645befa76c845ed646a4b58d64af88ca Mon Sep 17 00:00:00 2001 From: ngthanhvu Date: Wed, 22 Jul 2026 04:26:58 -0400 Subject: [PATCH] feat: add authentication modal component and user authentication logic - Implemented AuthModal.vue for user login and registration with form handling. - Created useAuth composable for managing user state and authentication requests. - Added admin route middleware to protect admin routes. - Developed admin login page with authentication checks. - Implemented API endpoints for user management (login, logout, fetch user, register). - Created database migrations for users table and updated movies table. - Added utility functions for password hashing and JWT token management. --- README.md | 322 +++++++++++++----- app/components/AppHeader.vue | 119 +++++-- app/components/AuthModal.vue | 173 ++++++++++ app/composables/useAuth.ts | 23 ++ app/layouts/admin.vue | 36 +- app/middleware/admin.global.ts | 16 + app/pages/admin/login.vue | 109 ++++++ app/pages/admin/thanh-vien.vue | 103 +++--- app/pages/thanh-vien.vue | 109 ++++-- package-lock.json | 142 +++++++- package.json | 4 + server/api/admin/movies.get.ts | 4 +- server/api/admin/sync.post.ts | 2 + server/api/admin/thanh-vien.get.ts | 38 +++ server/api/admin/thanh-vien/[id].delete.ts | 25 ++ server/api/admin/thanh-vien/[id].patch.ts | 35 ++ server/api/auth/login.post.ts | 42 +++ server/api/auth/logout.post.ts | 4 + server/api/auth/me.get.ts | 32 ++ server/api/auth/register.post.ts | 56 +++ server/api/movies.get.ts | 2 +- .../migrations/0003_slow_masked_marvel.sql | 13 + .../migrations/0004_opposite_sasquatch.sql | 1 + .../migrations/meta/0003_snapshot.json | 314 +++++++++++++++++ .../migrations/meta/0004_snapshot.json | 321 +++++++++++++++++ server/database/migrations/meta/_journal.json | 14 + server/database/schema.ts | 15 + server/middleware/auth.ts | 23 ++ server/utils/auth.ts | 46 +++ 29 files changed, 1965 insertions(+), 178 deletions(-) create mode 100644 app/components/AuthModal.vue create mode 100644 app/composables/useAuth.ts create mode 100644 app/middleware/admin.global.ts create mode 100644 app/pages/admin/login.vue create mode 100644 server/api/admin/thanh-vien.get.ts create mode 100644 server/api/admin/thanh-vien/[id].delete.ts create mode 100644 server/api/admin/thanh-vien/[id].patch.ts create mode 100644 server/api/auth/login.post.ts create mode 100644 server/api/auth/logout.post.ts create mode 100644 server/api/auth/me.get.ts create mode 100644 server/api/auth/register.post.ts create mode 100644 server/database/migrations/0003_slow_masked_marvel.sql create mode 100644 server/database/migrations/0004_opposite_sasquatch.sql create mode 100644 server/database/migrations/meta/0003_snapshot.json create mode 100644 server/database/migrations/meta/0004_snapshot.json create mode 100644 server/middleware/auth.ts create mode 100644 server/utils/auth.ts diff --git a/README.md b/README.md index 05eb83d..90fde3f 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,305 @@ # CineK -CineK is a Korean movie streaming web app built with Nuxt and Tailwind CSS. It uses NguonC movie data, normalizes the responses on the server, and presents the content with a blue Netflix-inspired interface. +CineK là website xem phim Hàn Quốc online với giao diện dark theme, hệ thống admin quản lý phim, đăng ký/đăng nhập thành viên, và đồng bộ dữ liệu từ nhiều nguồn API. -## Features +## Tính năng -- Korean-only movie catalogue from NguonC. -- Netflix-style home page with a large hero carousel. -- Floating movie preview cards on home page hover. -- Browse page with search, type filters, and pagination. -- Movie detail page with embedded player and episode selector. -- Shared responsive header with centered navigation and global search. -- Shared footer and scroll-to-top button. -- Tailwind CSS styling with the `Be Vietnam Pro` font. -- Lucide icons for UI actions. +### Người dùng +- Trang chủ với hero carousel phim Hàn Quốc +- Duyệt phim theo loại (phim bộ, phim lẻ), tìm kiếm, phân trang +- Trang chi tiết phim với trình phát HLS + Nhúng +- Đăng ký / Đăng nhập tài khoản thành viên (popup modal) +- Lịch sử xem, danh sách phim yêu thích (lưu localStorage) + +### Admin (`/admin`) +- Dashboard tổng quan (tổng phim, đang hiển thị, chưa hiển thị) +- Đồng bộ phim từ API (chọn nguồn: OPhim, NguonC, KKPhim) +- Bật/tắt hiển thị từng phim +- Chỉnh sửa phim: mô tả, poster, thumbnail, tập phim tuỳ chỉnh +- Quản lý thành viên: đổi role, bật/tắt, xoá tài khoản +- Xoá toàn bộ phim +- Phân quyền: chỉ role `admin` mới truy cập được ## Tech Stack -- Nuxt 4 -- Vue 3 -- Tailwind CSS 4 -- `@tailwindcss/vite` -- `lucide-vue-next` +- **Framework:** Nuxt 4 (Vue 3) +- **Styling:** Tailwind CSS 4 +- **Database:** MySQL 8.0 + Drizzle ORM +- **Cache:** Redis 7 +- **Auth:** JWT (bcryptjs + jsonwebtoken) +- **Icons:** Lucide Vue Next +- **Player:** HLS.js +- **Container:** Docker + Docker Compose -## Project Structure +## Cấu trúc thư mục ```text app/ - assets/css/main.css Global Tailwind and theme styles - components/ Shared UI components - pages/index.vue Home page - pages/phim/index.vue Browse, search, filter, and pagination page - pages/phim/[slug].vue Movie detail and player page + components/ Components dùng chung (AppHeader, AppLogo, AuthModal, AdminToggle...) + composables/ Composables (useAuth, useMovieLibrary, useWatchHistory) + layouts/ + admin.vue Layout cho trang admin (sidebar + header) + middleware/ + admin.global.ts Middleware chặn route /admin/* nếu chưa đăng nhập admin + pages/ + index.vue Trang chủ + phim/ Trang chi tiết phim + xem/[slug].vue Trang xem phim (player) + thanh-vien.vue Trang cá nhân thành viên + yeu-thich.vue Phim yêu thích + lich-su.vue Lịch sử xem + dang-nhap.vue Đăng nhập + dang-ky.vue Đăng ký + admin/ + login.vue Trang đăng nhập admin + index.vue Dashboard admin + phim/index.vue Quản lý phim + phim/[id].vue Chỉnh sửa phim + thanh-vien.vue Quản lý thành viên + cai-dat.vue Cài đặt hệ thống + server/ - api/movies.get.ts Movie list API endpoint - api/movies/[slug].get.ts Movie detail API endpoint - utils/movies.ts API adapters and normalization helpers -API_nguonc.md NguonC API notes + api/ + movies.get.ts API danh sách phim công khai (chỉ phim active) + movies/[slug].get.ts API chi tiết phim công khai + auth/ API xác thực (register, login, logout, me) + admin/ API admin (sync, movies, thanh-vien, stats) + database/ + schema.ts Schema Drizzle (bảng users, movies) + migrations/ File migration SQL + middleware/ + auth.ts Middleware chặn /api/admin/* nếu chưa đăng nhập admin + utils/ + auth.ts Tiện ích auth (hash password, JWT sign/verify) + db.ts Kết nối MySQL + redis.ts Kết nối Redis + movies.ts Adapter API phim + chuẩn hoá dữ liệu ``` -## Setup +## Setup & Chạy dự án -Install dependencies: +### Yêu cầu + +- Docker & Docker Compose +- Node.js 22+ (nếu chạy ngoài Docker) + +### 1. Clone & cài dependencies ```bash +git clone +cd kr-phim npm install ``` -## Development +### 2. Cấu hình biến môi trường -Start the development server: +Tạo file `.env`: + +```env +DATABASE_URL=mysql://cinek:cinekpassword@localhost:3306/cinek +REDIS_URL=redis://localhost:6379 +JWT_SECRET=your-secret-key-change-this +``` + +### 3. Chạy MySQL & Redis + +```bash +# Chỉ bật MySQL và Redis +docker compose up -d mysql redis +``` + +Hoặc chạy tất cả (app + mysql + redis): + +```bash +docker compose up -d +``` + +#### Thông tin kết nối mặc định + +| Service | Host | Port | User | Password | Database | +|----------|-----------|------|---------|----------------|----------| +| MySQL | localhost | 3306 | cinek | cinekpassword | cinek | +| MySQL | localhost | 3306 | root | rootpassword | - | +| Redis | localhost | 6379 | - | - | - | +| App | localhost | 3002 | - | - | - | + +### 4. Tạo bảng database (migration) + +```bash +# Tạo file migration từ schema +npx drizzle-kit generate + +# Áp dụng migration vào MySQL +npx drizzle-kit migrate +``` + +### 5. Chạy dev server ```bash npm run dev ``` -The app will usually be available at: +App chạy tại `http://localhost:3002`. Hot reload tự động khi sửa code. -```text -http://localhost:3002 -``` +### 6. Tạo tài khoản admin đầu tiên -## Production Build +Truy cập `http://localhost:3002` → bấm **Đăng nhập** → chuyển sang tab **Đăng ký**. -Build the app for production: +Tài khoản đầu tiên đăng ký sẽ tự động được gán role **admin**. -```bash -npm run build -``` +Sau đó truy cập `http://localhost:3002/admin` để vào trang quản trị. -Preview the production build locally: +### 7. Đồng bộ phim -```bash -npm run preview -``` +Vào **Admin → Dashboard** hoặc **Admin → Phim** → bấm **Đồng bộ từ API**. -Run the production server after building: +Chọn nguồn muốn đồng bộ (OPhim, NguonC, KKPhim) → bấm **Đồng bộ**. -```bash -npm run start -``` - -The production server listens on port `3002` by default. +Mặc định phim mới đồng bộ sẽ ở trạng thái **Ẩn**. Bật từng phim để hiển thị ra trang công khai. ## Docker -Build and run with Docker Compose: +### Dev mode (có hot reload) ```bash docker compose up -d --build ``` -The container exposes the app at: +File code được mount vào container qua volume, sửa file tự reload. -```text -http://localhost:3002 -``` - -Stop the container: +### Production build ```bash -docker compose down +# Build image production +docker build -t cinek . + +# Chạy +docker run -p 3002:3002 \ + -e DATABASE_URL=mysql://cinek:cinekpassword@host.docker.internal:3306/cinek \ + -e REDIS_URL=redis://host.docker.internal:6379 \ + cinek ``` -The Compose service is named `cinek` and maps host port `3002` to container port `3002`. +### Dừng & xoá containers -## Available Routes +```bash +docker compose down # Dừng containers +docker compose down -v # Dừng + xoá volumes (mất dữ liệu MySQL/Redis) +``` -- `/` - Home page with hero carousel and movie rows. -- `/phim` - Browse page with search, filters, and pagination. -- `/phim?type=series` - Browse Korean series. -- `/phim?type=single` - Browse Korean movies. -- `/phim?q=keyword` - Search Korean movies. -- `/phim/[slug]` - Movie detail page from NguonC. +## Database -## API Notes +### Schema -The app does not call external movie APIs directly from the browser. Instead, Nuxt server endpoints request and normalize movie data: +**Bảng `users`:** -- NguonC list/detail data is handled in `server/utils/movies.ts`. -- Only Korean movies are kept in the normalized result. +| Cột | Kiểu | Mô tả | +|-----------|---------------|---------------------------| +| id | INT (PK) | Tự tăng | +| name | VARCHAR(200) | Tên hiển thị | +| email | VARCHAR(255) | Email (unique) | +| password | VARCHAR(255) | Mật khẩu (bcrypt hash) | +| role | VARCHAR(50) | admin / moderator / user | +| avatar | TEXT | URL ảnh đại diện | +| active | BOOLEAN | Trạng thái tài khoản | +| created_at| TIMESTAMP | Ngày tạo | +| updated_at| TIMESTAMP | Ngày cập nhật | -The public server endpoints use NguonC as the single movie API source. +**Bảng `movies`:** + +| Cột | Kiểu | Mô tả | +|----------------|---------------|----------------------------------| +| id | INT (PK) | Tự tăng | +| source | VARCHAR(50) | Nguồn (ophim/nguonc/kkphim) | +| slug | VARCHAR(500) | Slug phim trên nguồn | +| name | VARCHAR(500) | Tên phim | +| origin_name | VARCHAR(500) | Tên gốc | +| thumb/poster | TEXT | URL ảnh | +| year | INT | Năm sản xuất | +| active | BOOLEAN | Hiển thị trên web công khai | +| custom_poster | TEXT | Poster admin tuỳ chỉnh | +| custom_thumb | TEXT | Thumbnail admin tuỳ chỉnh | +| custom_content | TEXT | Mô tả admin tuỳ chỉnh | +| custom_episodes| JSON | Tập phim admin tuỳ chỉnh | +| api_updated_at | TIMESTAMP | Thời gian API nguồn cập nhật | +| synced_at | TIMESTAMP | Thời gian đồng bộ vào DB | + +### Migration + +```bash +# Tạo migration mới (sau khi sửa schema.ts) +npx drizzle-kit generate + +# Áp dụng migration +npx drizzle-kit migrate +``` + +### Kết nối MySQL trực tiếp + +```bash +# Qua Docker +docker exec -it cinek-mysql mysql -ucinek -pcinekpassword cinek + +# Hoặc từ host +mysql -h 127.0.0.1 -P 3306 -ucinek -pcinekpassword cinek +``` + +### Kết nối Redis trực tiếp + +```bash +docker exec -it cinek-redis redis-cli +``` + +## API Endpoints + +### Công khai + +| Method | Endpoint | Mô tả | +|--------|-----------------------|---------------------------------| +| GET | /api/movies | Danh sách phim active (có cache)| +| GET | /api/movies/[slug] | Chi tiết phim active | + +### Xác thực + +| Method | Endpoint | Mô tả | +|--------|-----------------------|---------------------------------| +| POST | /api/auth/register | Đăng ký (tài khoản đầu = admin) | +| POST | /api/auth/login | Đăng nhập (trả JWT + cookie) | +| POST | /api/auth/logout | Đăng xuất (xoá cookie) | +| GET | /api/auth/me | Thông tin user hiện tại | + +### Admin (cần role admin) + +| Method | Endpoint | Mô tả | +|--------|-----------------------------|-------------------------------| +| GET | /api/admin/stats | Thống kê tổng phim | +| POST | /api/admin/sync | Đồng bộ phim từ API nguồn | +| GET | /api/admin/movies | Danh sách tất cả phim | +| GET | /api/admin/movies/:id | Chi tiết 1 phim | +| PATCH | /api/admin/movies/:id | Bật/tắt phim | +| PUT | /api/admin/movies/:id | Cập nhật custom (poster, tập) | +| DELETE | /api/admin/movies | Xoá tất cả phim | +| GET | /api/admin/thanh-vien | Danh sách thành viên | +| PATCH | /api/admin/thanh-vien/:id | Đổi role / bật/tắt | +| DELETE | /api/admin/thanh-vien/:id | Xoá thành viên | ## Scripts ```bash -npm run dev # Start Nuxt development server -npm run build # Build for production -npm run preview # Preview the production build -npm run start # Start the built Nitro server -npm run generate # Generate a static build when supported +npm run dev # Dev server (hot reload) +npm run build # Build production +npm run preview # Preview production build +npm run start # Chạy production server +npm run db:generate # Tạo migration từ schema +npm run db:migrate # Áp dụng migration ``` -## Notes +## Nguồn phim -This project is intended as a movie browsing and streaming UI experiment using public movie API sources. Availability of movie metadata, images, and episode embeds depends on the upstream APIs. +Dữ liệu phim được lấy từ 3 API nguồn: + +- **OPhim** - `https://ophim1.com` +- **NguonC** - `https://phim.nguonc.com` +- **KKPhim** - `https://phimapi.com` + +Chỉ lấy phim quốc gia Hàn Quốc. Dữ liệu bao gồm: tên phim, ảnh, thể loại, tập phim, link embed, link HLS. diff --git a/app/components/AppHeader.vue b/app/components/AppHeader.vue index c4b7cc0..3706535 100644 --- a/app/components/AppHeader.vue +++ b/app/components/AppHeader.vue @@ -2,10 +2,11 @@ import { Heart, History, + LogOut, Menu, Search, + Settings, User, - UserRound, X, } from 'lucide-vue-next' @@ -14,6 +15,17 @@ const router = useRouter() const keyword = ref(typeof route.query.q === 'string' ? route.query.q : '') const mobileMenuOpen = ref(false) const memberMenuOpen = ref(false) +const authModalOpen = ref(false) + +const { user, fetchUser, logout: doLogout } = useAuth() +await fetchUser() + +const displayName = computed(() => { + if (!user.value) return '' + return user.value.name || user.value.email?.split('@')[0] || 'Thành viên' +}) + +const displayInitial = computed(() => displayName.value.charAt(0).toUpperCase()) const navItems = [ { label: 'Trang chủ', to: '/' }, @@ -23,10 +35,17 @@ const navItems = [ { label: 'Phim lẻ', to: '/phim?type=single' }, ] -const memberMenuItems = [ - { label: 'Yêu thích', icon: Heart, to: '/yeu-thich' }, - { label: 'Lịch sử', icon: History, to: '/lich-su' }, -] +const memberMenuItems = computed(() => { + const items = [ + { label: 'Trang cá nhân', icon: User, to: '/thanh-vien' }, + { label: 'Yêu thích', icon: Heart, to: '/yeu-thich' }, + { label: 'Lịch sử', icon: History, to: '/lich-su' }, + ] + if (user.value?.role === 'admin') { + items.push({ label: 'Trang quản trị', icon: Settings, to: '/admin' }) + } + return items +}) function isActive(to: string) { if (to === '/') return route.path === '/' @@ -45,10 +64,24 @@ function closeMobileMenu() { mobileMenuOpen.value = false } +function openAuthModal() { + authModalOpen.value = true +} + function handleMemberClick() { + if (!user.value) { + openAuthModal() + return + } memberMenuOpen.value = !memberMenuOpen.value } +async function handleLogout() { + await doLogout() + memberMenuOpen.value = false + router.push('/') +} + watch(() => route.path, () => { closeMobileMenu() memberMenuOpen.value = false @@ -56,7 +89,7 @@ watch(() => route.path, () => { diff --git a/app/components/AuthModal.vue b/app/components/AuthModal.vue new file mode 100644 index 0000000..1116397 --- /dev/null +++ b/app/components/AuthModal.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/app/composables/useAuth.ts b/app/composables/useAuth.ts new file mode 100644 index 0000000..d466d11 --- /dev/null +++ b/app/composables/useAuth.ts @@ -0,0 +1,23 @@ +export function useAuth() { + const user = useState('auth-user', () => null) + const loading = useState('auth-loading', () => true) + + async function fetchUser() { + try { + loading.value = true + const data = await $fetch('/api/auth/me') + user.value = data + } catch { + user.value = null + } finally { + loading.value = false + } + } + + async function logout() { + await $fetch('/api/auth/logout', { method: 'POST' }) + user.value = null + } + + return { user, loading, fetchUser, logout } +} diff --git a/app/layouts/admin.vue b/app/layouts/admin.vue index 5e3923c..9307e73 100644 --- a/app/layouts/admin.vue +++ b/app/layouts/admin.vue @@ -1,6 +1,5 @@