-
-
-
Phim gần đây
-
- Xem tất cả
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
{{ movie.name }}
-
{{ movie.views.toLocaleString() }} lượt xem
-
-
-
- {{ movie.status === 'active' ? 'Hoạt động' : 'Nháp' }}
-
-
-
-
+
Đồng bộ phim
+
Chọn nguồn để đồng bộ phim vào hệ thống
-
-
-
Hoạt động gần đây
-
+
+
-
-
-
-
-
-
-
- Phim mới đã được thêm
-
-
2 phút trước
-
-
+
-
-
-
-
- 15 thành viên mới đăng ký
-
-
1 giờ trước
-
-
-
-
-
-
-
-
- Lượt xem tăng 23% so với hôm qua
-
-
3 giờ trước
-
-
-
-
-
-
-
-
-
- Doanh thu tháng đạt 12.5M
-
-
5 giờ trước
+
+
+
-
-
+
+
+
+
diff --git a/app/pages/admin/phim.vue b/app/pages/admin/phim.vue
deleted file mode 100644
index 3ffe027..0000000
--- a/app/pages/admin/phim.vue
+++ /dev/null
@@ -1,126 +0,0 @@
-
-
-
-
-
-
-
Quản lý phim
-
Quản lý tất cả phim trên CineK
-
-
-
-
-
-
-
-
-
-
-
- | Phim |
- Lượt xem |
- Trạng thái |
- Cập nhật |
- Thao tác |
-
-
-
-
-
-
-
-
-
-
- {{ movie.name }}
- {{ movie.originName }}
-
-
- |
- {{ movie.views.toLocaleString() }} |
-
-
- {{ movie.status === 'active' ? 'Hoạt động' : 'Nháp' }}
-
- |
- {{ movie.updatedAt }} |
-
-
-
-
-
-
- |
-
-
-
-
-
-
-
Hiển thị 1-5 / 1,234 phim
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/pages/admin/phim/[id].vue b/app/pages/admin/phim/[id].vue
new file mode 100644
index 0000000..cac5d2f
--- /dev/null
+++ b/app/pages/admin/phim/[id].vue
@@ -0,0 +1,282 @@
+
+
+
+
+
+
+ Quay lại danh sách phim
+
+
+
+
+
+
+
+
{{ movie.name }}
+
+ {{ movie.originName }} · {{ movie.source?.toUpperCase() }} · {{ movie.year || 'N/A' }}
+
+
+
+
+
+
+
+
+ Đã lưu thành công
+
+
+
+
+
+
+
+
+
+
+
+
Mô tả phim
+
Để trống để dùng mô tả từ API nguồn
+
+
+
+
+
+
+
+
+
+
+
+
+
Tập phim tuỳ chỉnh
+
Thêm tập phim thủ công khi API thiếu tập hoặc link bị die. Để trống tất cả để dùng từ API.
+
+
+
+
+
+
+
Chưa có tập tuỳ chỉnh. Bấm "Thêm tập" để bắt đầu.
+
+
+
+
+
+
+
+
+
+
Ảnh poster
+
URL ảnh poster (để trống dùng từ API)
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
Ảnh thumbnail
+
URL ảnh thumbnail (để trống dùng từ API)
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/pages/admin/phim/index.vue b/app/pages/admin/phim/index.vue
new file mode 100644
index 0000000..6220709
--- /dev/null
+++ b/app/pages/admin/phim/index.vue
@@ -0,0 +1,314 @@
+
+
+
+
+
+
+
Quản lý phim
+
Quản lý phim hiển thị trên CineK ({{ data?.total || 0 }} phim)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | STT |
+ Phim |
+ Nguồn |
+ Trạng thái |
+ Chỉnh sửa |
+ Thao tác |
+
+
+
+
+ |
+ {{ (currentPage - 1) * 20 + movies.indexOf(movie) + 1 }}
+ |
+
+
+ {{ movie.name }}
+ {{ movie.originName || movie.slug }}
+
+ |
+
+
+ {{ movie.source?.toUpperCase() }}
+
+ |
+
+
+ {{ movie.active ? 'Đang hiển thị' : 'Ẩn' }}
+
+ |
+
+
+ Mô tả
+ Poster
+ Thumb
+ {{ movie.customEpisodes.length }} tập
+ Mặc định
+
+ |
+
+
+
+
+
+ { movie.active = val; toggleActive(movie) }" />
+
+ |
+
+
+ |
+ {{ debouncedKeyword ? 'Không tìm thấy phim nào.' : 'Chưa có phim nào. Bấm "Đồng bộ từ API" để lấy phim.' }}
+ |
+
+
+
+
+
+
+
Trang {{ currentPage }} / {{ totalPages }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Đồng bộ phim
+
Chọn nguồn để đồng bộ phim vào hệ thống
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Xoá tất cả phim?
+
+ Hành động này sẽ xoá toàn bộ {{ data?.total || 0 }} phim trong hệ thống, bao gồm cả các tuỳ chỉnh. Không thể hoàn tác.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/pages/admin/thanh-vien.vue b/app/pages/admin/thanh-vien.vue
index bb801c5..6d8bdcf 100644
--- a/app/pages/admin/thanh-vien.vue
+++ b/app/pages/admin/thanh-vien.vue
@@ -1,5 +1,5 @@
@@ -31,7 +49,7 @@ const users = [
@@ -39,31 +57,25 @@ const users = [
-
- | Thành viên |
- Vai trò |
- Trạng thái |
- Tham gia |
- Thao tác |
+
+ | STT |
+ Thành viên |
+ Vai trò |
+ Trạng thái |
+ Tham gia |
+ Thao tác |
-
-
-
-
-
-
-
- {{ user.name }}
-
-
- {{ user.email }}
-
-
+
+ | {{ filteredUsers.indexOf(user) + 1 }} |
+
+
+ {{ user.name }}
+ {{ user.email }}
|
-
+ |
+ |
{{ user.status === 'active' ? 'Hoạt động' : 'Khóa' }}
|
- {{ user.joined }} |
-
-
+ {{ user.joined }} |
+
+
|
|
+
+ |
+ {{ debouncedKeyword ? 'Không tìm thấy thành viên nào.' : 'Chưa có thành viên nào.' }}
+ |
+
|
-
Hiển thị 1-5 / 5,678 thành viên
-
-
-
-
-
-
-
+
Hiển thị {{ filteredUsers.length }} / {{ allUsers.length }} thành viên
diff --git a/docker-compose.yml b/docker-compose.yml
index c6ed570..b3b2523 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,4 +1,27 @@
services:
+ mysql:
+ image: mysql:8.0
+ container_name: cinek-mysql
+ restart: unless-stopped
+ environment:
+ MYSQL_ROOT_PASSWORD: rootpassword
+ MYSQL_DATABASE: cinek
+ MYSQL_USER: cinek
+ MYSQL_PASSWORD: cinekpassword
+ ports:
+ - "3306:3306"
+ volumes:
+ - mysql_data:/var/lib/mysql
+
+ redis:
+ image: redis:7-alpine
+ container_name: cinek-redis
+ restart: unless-stopped
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis_data:/data
+
cinek:
build:
context: .
@@ -8,8 +31,17 @@ services:
environment:
HOST: 0.0.0.0
PORT: 3002
+ DATABASE_URL: mysql://cinek:cinekpassword@mysql:3306/cinek
+ REDIS_URL: redis://redis:6379
ports:
- "3002:3002"
volumes:
- .:/app
- /app/node_modules
+ depends_on:
+ - mysql
+ - redis
+
+volumes:
+ mysql_data:
+ redis_data:
diff --git a/drizzle.config.ts b/drizzle.config.ts
new file mode 100644
index 0000000..3ce4ef7
--- /dev/null
+++ b/drizzle.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from 'drizzle-kit'
+
+export default defineConfig({
+ schema: './server/database/',
+ out: './server/database/migrations',
+ dialect: 'mysql',
+ dbCredentials: {
+ url: process.env.DATABASE_URL || 'mysql://cinek:cinekpassword@localhost:3306/cinek',
+ },
+})
diff --git a/package-lock.json b/package-lock.json
index 473bcc2..5fed9e4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,12 +8,18 @@
"hasInstallScript": true,
"dependencies": {
"@tailwindcss/vite": "^4.3.0",
+ "drizzle-orm": "^0.45.2",
"hls.js": "^1.6.16",
+ "ioredis": "^5.11.1",
"lucide-vue-next": "^1.0.0",
+ "mysql2": "^3.23.1",
"nuxt": "^4.4.5",
"tailwindcss": "^4.3.0",
"vue": "^3.5.34",
"vue-router": "^5.0.6"
+ },
+ "devDependencies": {
+ "drizzle-kit": "^0.31.10"
}
},
"node_modules/@babel/code-frame": {
@@ -481,6 +487,13 @@
"integrity": "sha512-kIxYSfA5T8HXjav55UaaH/o/cKivF6jCCGIb8eqtcsfI46wsvlSiT8jMDyrl779qLec3c2c2oHBZo4oAhvbjrQ==",
"license": "MIT"
},
+ "node_modules/@drizzle-team/brocli": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
+ "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/@dxup/nuxt": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@dxup/nuxt/-/nuxt-0.4.1.tgz",
@@ -539,6 +552,442 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@esbuild-kit/core-utils": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
+ "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
+ "deprecated": "Merged into tsx: https://tsx.hirok.io",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.18.20",
+ "source-map-support": "^0.5.21"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+ "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+ "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+ "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+ "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+ "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+ "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+ "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+ "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+ "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+ "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+ "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+ "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+ "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+ "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+ "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+ "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+ "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+ "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+ "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+ "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+ "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.18.20",
+ "@esbuild/android-arm64": "0.18.20",
+ "@esbuild/android-x64": "0.18.20",
+ "@esbuild/darwin-arm64": "0.18.20",
+ "@esbuild/darwin-x64": "0.18.20",
+ "@esbuild/freebsd-arm64": "0.18.20",
+ "@esbuild/freebsd-x64": "0.18.20",
+ "@esbuild/linux-arm": "0.18.20",
+ "@esbuild/linux-arm64": "0.18.20",
+ "@esbuild/linux-ia32": "0.18.20",
+ "@esbuild/linux-loong64": "0.18.20",
+ "@esbuild/linux-mips64el": "0.18.20",
+ "@esbuild/linux-ppc64": "0.18.20",
+ "@esbuild/linux-riscv64": "0.18.20",
+ "@esbuild/linux-s390x": "0.18.20",
+ "@esbuild/linux-x64": "0.18.20",
+ "@esbuild/netbsd-x64": "0.18.20",
+ "@esbuild/openbsd-x64": "0.18.20",
+ "@esbuild/sunos-x64": "0.18.20",
+ "@esbuild/win32-arm64": "0.18.20",
+ "@esbuild/win32-ia32": "0.18.20",
+ "@esbuild/win32-x64": "0.18.20"
+ }
+ },
+ "node_modules/@esbuild-kit/esm-loader": {
+ "version": "2.6.5",
+ "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
+ "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
+ "deprecated": "Merged into tsx: https://tsx.hirok.io",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@esbuild-kit/core-utils": "^3.3.2",
+ "get-tsconfig": "^4.7.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
@@ -956,9 +1405,9 @@
}
},
"node_modules/@ioredis/commands": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz",
- "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==",
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz",
+ "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==",
"license": "MIT"
},
"node_modules/@isaacs/cliui": {
@@ -3672,6 +4121,16 @@
"integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==",
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "26.1.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
+ "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~8.3.0"
+ }
+ },
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -4298,6 +4757,15 @@
"postcss": "^8.1.0"
}
},
+ "node_modules/aws-ssl-profiles": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+ "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
"node_modules/b4a": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
@@ -4693,9 +5161,9 @@
}
},
"node_modules/cluster-key-slot": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
- "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz",
+ "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
@@ -5308,6 +5776,631 @@
"url": "https://dotenvx.com"
}
},
+ "node_modules/drizzle-kit": {
+ "version": "0.31.10",
+ "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz",
+ "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@drizzle-team/brocli": "^0.10.2",
+ "@esbuild-kit/esm-loader": "^2.5.5",
+ "esbuild": "^0.25.4",
+ "tsx": "^4.21.0"
+ },
+ "bin": {
+ "drizzle-kit": "bin.cjs"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/drizzle-kit/node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/drizzle-orm": {
+ "version": "0.45.2",
+ "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz",
+ "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@aws-sdk/client-rds-data": ">=3",
+ "@cloudflare/workers-types": ">=4",
+ "@electric-sql/pglite": ">=0.2.0",
+ "@libsql/client": ">=0.10.0",
+ "@libsql/client-wasm": ">=0.10.0",
+ "@neondatabase/serverless": ">=0.10.0",
+ "@op-engineering/op-sqlite": ">=2",
+ "@opentelemetry/api": "^1.4.1",
+ "@planetscale/database": ">=1.13",
+ "@prisma/client": "*",
+ "@tidbcloud/serverless": "*",
+ "@types/better-sqlite3": "*",
+ "@types/pg": "*",
+ "@types/sql.js": "*",
+ "@upstash/redis": ">=1.34.7",
+ "@vercel/postgres": ">=0.8.0",
+ "@xata.io/client": "*",
+ "better-sqlite3": ">=7",
+ "bun-types": "*",
+ "expo-sqlite": ">=14.0.0",
+ "gel": ">=2",
+ "knex": "*",
+ "kysely": "*",
+ "mysql2": ">=2",
+ "pg": ">=8",
+ "postgres": ">=3",
+ "sql.js": ">=1",
+ "sqlite3": ">=5"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/client-rds-data": {
+ "optional": true
+ },
+ "@cloudflare/workers-types": {
+ "optional": true
+ },
+ "@electric-sql/pglite": {
+ "optional": true
+ },
+ "@libsql/client": {
+ "optional": true
+ },
+ "@libsql/client-wasm": {
+ "optional": true
+ },
+ "@neondatabase/serverless": {
+ "optional": true
+ },
+ "@op-engineering/op-sqlite": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@planetscale/database": {
+ "optional": true
+ },
+ "@prisma/client": {
+ "optional": true
+ },
+ "@tidbcloud/serverless": {
+ "optional": true
+ },
+ "@types/better-sqlite3": {
+ "optional": true
+ },
+ "@types/pg": {
+ "optional": true
+ },
+ "@types/sql.js": {
+ "optional": true
+ },
+ "@upstash/redis": {
+ "optional": true
+ },
+ "@vercel/postgres": {
+ "optional": true
+ },
+ "@xata.io/client": {
+ "optional": true
+ },
+ "better-sqlite3": {
+ "optional": true
+ },
+ "bun-types": {
+ "optional": true
+ },
+ "expo-sqlite": {
+ "optional": true
+ },
+ "gel": {
+ "optional": true
+ },
+ "knex": {
+ "optional": true
+ },
+ "kysely": {
+ "optional": true
+ },
+ "mysql2": {
+ "optional": true
+ },
+ "pg": {
+ "optional": true
+ },
+ "postgres": {
+ "optional": true
+ },
+ "prisma": {
+ "optional": true
+ },
+ "sql.js": {
+ "optional": true
+ },
+ "sqlite3": {
+ "optional": true
+ }
+ }
+ },
"node_modules/duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@@ -5723,6 +6816,15 @@
"integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==",
"license": "BSD-3-Clause"
},
+ "node_modules/generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-property": "^1.0.2"
+ }
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -5771,6 +6873,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+ "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
"node_modules/giget": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz",
@@ -5970,6 +7085,22 @@
"node": ">=16.17.0"
}
},
+ "node_modules/iconv-lite": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -6034,20 +7165,18 @@
}
},
"node_modules/ioredis": {
- "version": "5.10.1",
- "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz",
- "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==",
+ "version": "5.11.1",
+ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz",
+ "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==",
"license": "MIT",
"dependencies": {
- "@ioredis/commands": "1.5.1",
- "cluster-key-slot": "^1.1.0",
- "debug": "^4.3.4",
- "denque": "^2.1.0",
- "lodash.defaults": "^4.2.0",
- "lodash.isarguments": "^3.1.0",
- "redis-errors": "^1.2.0",
- "redis-parser": "^3.0.0",
- "standard-as-callback": "^2.1.0"
+ "@ioredis/commands": "1.10.0",
+ "cluster-key-slot": "1.1.1",
+ "debug": "4.4.3",
+ "denque": "2.1.0",
+ "redis-errors": "1.2.0",
+ "redis-parser": "3.0.0",
+ "standard-as-callback": "2.1.0"
},
"engines": {
"node": ">=12.22.0"
@@ -6199,6 +7328,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+ "license": "MIT"
+ },
"node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
@@ -6694,18 +7829,6 @@
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
- "node_modules/lodash.defaults": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
- "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
- "license": "MIT"
- },
- "node_modules/lodash.isarguments": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
- "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
- "license": "MIT"
- },
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -6718,6 +7841,12 @@
"integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
"license": "MIT"
},
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -6727,6 +7856,21 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lru.min": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
+ "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=1.30.0",
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wellwelwel"
+ }
+ },
"node_modules/lucide-vue-next": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-1.0.0.tgz",
@@ -7001,6 +8145,40 @@
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
"license": "MIT"
},
+ "node_modules/mysql2": {
+ "version": "3.23.1",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.23.1.tgz",
+ "integrity": "sha512-tTuRnC7qCet2IOfSNMYZ5SwXuBnfvBPAcIA28P0gtruXyZlU1LMxA6uha32kYypoFgyYklMqhLWwt4laYwXR/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "aws-ssl-profiles": "^1.1.2",
+ "denque": "^2.1.0",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.7.2",
+ "long": "^5.3.2",
+ "lru.min": "^1.1.4",
+ "named-placeholders": "^1.1.6",
+ "sql-escaper": "^1.5.1"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 8"
+ }
+ },
+ "node_modules/named-placeholders": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
+ "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
+ "license": "MIT",
+ "dependencies": {
+ "lru.min": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
@@ -8351,6 +9529,16 @@
"node": ">=8"
}
},
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
"node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@@ -8511,6 +9699,12 @@
],
"license": "MIT"
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
"node_modules/sax": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
@@ -8756,6 +9950,21 @@
"node": ">=0.10.0"
}
},
+ "node_modules/sql-escaper": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.1.tgz",
+ "integrity": "sha512-4toX5E1fQbBrpfXidaHnF0669nkAdETeIPTs2SUjxxD7RRIs9ICG4gtpmfc68JCEKehsdwLFqBu9VlQqZ1P1gg==",
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=2.0.0",
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
+ }
+ },
"node_modules/srvx": {
"version": "0.11.15",
"resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.15.tgz",
@@ -9208,6 +10417,25 @@
"license": "0BSD",
"optional": true
},
+ "node_modules/tsx": {
+ "version": "4.23.1",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz",
+ "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
"node_modules/type-fest": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz",
@@ -9283,6 +10511,13 @@
"node": ">=18.12.0"
}
},
+ "node_modules/undici-types": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
+ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/unenv": {
"version": "2.0.0-rc.24",
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
diff --git a/package.json b/package.json
index 72451f9..3f0faa1 100644
--- a/package.json
+++ b/package.json
@@ -8,15 +8,23 @@
"generate": "nuxt generate",
"preview": "nuxt preview --host 0.0.0.0 --port 3002",
"start": "node .output/server/index.mjs",
- "postinstall": "nuxt prepare"
+ "postinstall": "nuxt prepare",
+ "db:generate": "drizzle-kit generate",
+ "db:migrate": "drizzle-kit migrate"
},
"dependencies": {
"@tailwindcss/vite": "^4.3.0",
+ "drizzle-orm": "^0.45.2",
"hls.js": "^1.6.16",
+ "ioredis": "^5.11.1",
"lucide-vue-next": "^1.0.0",
+ "mysql2": "^3.23.1",
"nuxt": "^4.4.5",
"tailwindcss": "^4.3.0",
"vue": "^3.5.34",
"vue-router": "^5.0.6"
+ },
+ "devDependencies": {
+ "drizzle-kit": "^0.31.10"
}
}
diff --git a/server/api/admin/movies.delete.ts b/server/api/admin/movies.delete.ts
new file mode 100644
index 0000000..a54168d
--- /dev/null
+++ b/server/api/admin/movies.delete.ts
@@ -0,0 +1,16 @@
+import { movies } from '../../database/schema'
+import { useRedis } from '../../utils/redis'
+
+export default defineEventHandler(async () => {
+ const db = useDb()
+
+ await db.delete(movies)
+
+ try {
+ const redis = useRedis()
+ const keys = await redis.keys('cinek:public:*')
+ if (keys.length) await redis.del(...keys)
+ } catch {}
+
+ return { success: true }
+})
diff --git a/server/api/admin/movies.get.ts b/server/api/admin/movies.get.ts
new file mode 100644
index 0000000..fdc6d38
--- /dev/null
+++ b/server/api/admin/movies.get.ts
@@ -0,0 +1,47 @@
+import { movies } from '../../database/schema'
+import { desc, like, sql } from 'drizzle-orm'
+
+export default defineEventHandler(async (event) => {
+ const db = useDb()
+ const query = getQuery(event)
+ const page = Math.max(Number(query.page) || 1, 1)
+ const limit = Math.min(Math.max(Number(query.limit) || 20, 1), 100)
+ const offset = (page - 1) * limit
+ const keyword = typeof query.keyword === 'string' ? query.keyword.trim() : ''
+ const status = typeof query.status === 'string' ? query.status : ''
+
+ let whereClause = undefined
+ if (keyword && status === 'active') {
+ whereClause = sql`${like(movies.name, `%${keyword}%`)} AND ${movies.active} = true`
+ } else if (keyword) {
+ whereClause = like(movies.name, `%${keyword}%`)
+ } else if (status === 'active') {
+ whereClause = eq(movies.active, true)
+ } else if (status === 'inactive') {
+ whereClause = eq(movies.active, false)
+ }
+
+ const [items, countResult] = await Promise.all([
+ db
+ .select()
+ .from(movies)
+ .where(whereClause)
+ .orderBy(desc(movies.syncedAt))
+ .limit(limit)
+ .offset(offset),
+ db
+ .select({ count: sql
`count(*)` })
+ .from(movies)
+ .where(whereClause),
+ ])
+
+ const total = countResult[0]?.count ?? 0
+
+ return {
+ items,
+ page,
+ limit,
+ total,
+ totalPages: Math.ceil(total / limit),
+ }
+})
diff --git a/server/api/admin/movies/[id].get.ts b/server/api/admin/movies/[id].get.ts
new file mode 100644
index 0000000..f5e1451
--- /dev/null
+++ b/server/api/admin/movies/[id].get.ts
@@ -0,0 +1,18 @@
+import { movies } from '../../../database/schema'
+import { eq } from 'drizzle-orm'
+
+export default defineEventHandler(async (event) => {
+ const db = useDb()
+ const id = Number(getRouterParam(event, 'id'))
+
+ if (!id) {
+ throw createError({ statusCode: 400, message: 'Thiếu ID phim' })
+ }
+
+ const result = await db.select().from(movies).where(eq(movies.id, id)).limit(1)
+ if (!result.length) {
+ throw createError({ statusCode: 404, message: 'Không tìm thấy phim' })
+ }
+
+ return result[0]
+})
diff --git a/server/api/admin/movies/[id].patch.ts b/server/api/admin/movies/[id].patch.ts
new file mode 100644
index 0000000..4413fbe
--- /dev/null
+++ b/server/api/admin/movies/[id].patch.ts
@@ -0,0 +1,39 @@
+import { movies } from '../../../database/schema'
+import { eq } from 'drizzle-orm'
+
+export default defineEventHandler(async (event) => {
+ const db = useDb()
+ const id = Number(getRouterParam(event, 'id'))
+ const body = await readBody(event)
+
+ if (!id) {
+ throw createError({ statusCode: 400, message: 'Thiếu ID phim' })
+ }
+
+ const existing = await db.select().from(movies).where(eq(movies.id, id)).limit(1)
+ if (!existing.length) {
+ throw createError({ statusCode: 404, message: 'Không tìm thấy phim' })
+ }
+
+ const updates: Record = {}
+
+ if (typeof body.active === 'boolean') {
+ updates.active = body.active
+ }
+
+ if (!Object.keys(updates).length) {
+ throw createError({ statusCode: 400, message: 'Không có dữ liệu cập nhật' })
+ }
+
+ await db.update(movies).set(updates).where(eq(movies.id, id))
+
+ try {
+ const redis = useRedis()
+ const keys = await redis.keys('cinek:public:*')
+ if (keys.length) {
+ await redis.del(...keys)
+ }
+ } catch {}
+
+ return { success: true, id, ...updates }
+})
diff --git a/server/api/admin/movies/[id].put.ts b/server/api/admin/movies/[id].put.ts
new file mode 100644
index 0000000..d27c5d0
--- /dev/null
+++ b/server/api/admin/movies/[id].put.ts
@@ -0,0 +1,48 @@
+import { movies } from '../../../database/schema'
+import { eq } from 'drizzle-orm'
+import { useRedis } from '../../../utils/redis'
+
+export default defineEventHandler(async (event) => {
+ const db = useDb()
+ const id = Number(getRouterParam(event, 'id'))
+ const body = await readBody(event)
+
+ if (!id) {
+ throw createError({ statusCode: 400, message: 'Thiếu ID phim' })
+ }
+
+ const existing = await db.select().from(movies).where(eq(movies.id, id)).limit(1)
+ if (!existing.length) {
+ throw createError({ statusCode: 404, message: 'Không tìm thấy phim' })
+ }
+
+ const updates: Record = {}
+
+ for (const field of ['customPoster', 'customThumb', 'customContent'] as const) {
+ if (field in body) {
+ updates[field] = typeof body[field] === 'string' && body[field].trim()
+ ? body[field].trim()
+ : null
+ }
+ }
+
+ if ('customEpisodes' in body) {
+ updates.customEpisodes = Array.isArray(body.customEpisodes) && body.customEpisodes.length
+ ? body.customEpisodes.filter((ep: any) => ep.name?.trim())
+ : null
+ }
+
+ if (!Object.keys(updates).length) {
+ throw createError({ statusCode: 400, message: 'Không có dữ liệu cập nhật' })
+ }
+
+ await db.update(movies).set(updates).where(eq(movies.id, id))
+
+ try {
+ const redis = useRedis()
+ const keys = await redis.keys('cinek:public:*')
+ if (keys.length) await redis.del(...keys)
+ } catch {}
+
+ return { success: true, id }
+})
diff --git a/server/api/admin/stats.get.ts b/server/api/admin/stats.get.ts
new file mode 100644
index 0000000..4108a88
--- /dev/null
+++ b/server/api/admin/stats.get.ts
@@ -0,0 +1,18 @@
+import { movies } from '../../database/schema'
+import { sql } from 'drizzle-orm'
+
+export default defineEventHandler(async () => {
+ const db = useDb()
+
+ const [stats] = await db.select({
+ total: sql`count(*)`,
+ active: sql`SUM(CASE WHEN ${movies.active} = true THEN 1 ELSE 0 END)`,
+ inactive: sql`SUM(CASE WHEN ${movies.active} = false THEN 1 ELSE 0 END)`,
+ }).from(movies)
+
+ return {
+ total: stats?.total ?? 0,
+ active: stats?.active ?? 0,
+ inactive: stats?.inactive ?? 0,
+ }
+})
diff --git a/server/api/admin/sync.post.ts b/server/api/admin/sync.post.ts
new file mode 100644
index 0000000..f34763a
--- /dev/null
+++ b/server/api/admin/sync.post.ts
@@ -0,0 +1,104 @@
+import { movies } from '../../database/schema'
+import { eq, and } from 'drizzle-orm'
+import { getOphimKoreanMovies, getNguoncKoreanMovies, getKkphimKoreanMovies } from '../../utils/movies'
+
+const SYNC_PAGES = 10
+const ALL_SOURCES = ['ophim', 'nguonc', 'kkphim'] as const
+
+export default defineEventHandler(async (event) => {
+ const db = useDb()
+ const body = await readBody(event).catch(() => ({})) || {}
+ const requestedSources = Array.isArray(body.sources) ? body.sources : ALL_SOURCES
+ const sources = requestedSources.filter((s: string) => ALL_SOURCES.includes(s as any))
+
+ if (!sources.length) {
+ throw createError({ statusCode: 400, message: 'Nguồn không hợp lệ' })
+ }
+
+ const allMovies: any[] = []
+ const fetchers: Record Promise> = {
+ ophim: getOphimKoreanMovies,
+ nguonc: getNguoncKoreanMovies,
+ kkphim: getKkphimKoreanMovies,
+ }
+
+ for (const sourceName of sources) {
+ const fetcher = fetchers[sourceName]
+ if (!fetcher) continue
+ for (let page = 1; page <= SYNC_PAGES; page++) {
+ try {
+ const result = await fetcher(page)
+ for (const movie of result.items) {
+ allMovies.push({ ...movie, source: sourceName })
+ }
+ if (!result.items.length) break
+ } catch {
+ break
+ }
+ }
+ }
+
+ let created = 0
+ let updated = 0
+
+ for (const movie of allMovies) {
+ const existing = await db
+ .select()
+ .from(movies)
+ .where(and(eq(movies.source, movie.source), eq(movies.slug, movie.slug)))
+ .limit(1)
+
+ if (existing.length) {
+ await db
+ .update(movies)
+ .set({
+ name: movie.name,
+ originName: movie.originName || null,
+ thumb: movie.thumb || null,
+ poster: movie.poster || null,
+ year: movie.year || null,
+ time: movie.time || null,
+ episode: movie.episode || null,
+ quality: movie.quality || null,
+ lang: movie.lang || null,
+ type: movie.type || null,
+ rating: movie.rating || null,
+ categories: movie.categories || null,
+ countries: movie.countries || null,
+ sources: movie.sources || null,
+ syncedAt: new Date(),
+ })
+ .where(eq(movies.id, existing[0].id))
+ updated++
+ } else {
+ await db.insert(movies).values({
+ source: movie.source,
+ slug: movie.slug,
+ name: movie.name,
+ originName: movie.originName || null,
+ thumb: movie.thumb || null,
+ poster: movie.poster || null,
+ year: movie.year || null,
+ time: movie.time || null,
+ episode: movie.episode || null,
+ quality: movie.quality || null,
+ lang: movie.lang || null,
+ type: movie.type || null,
+ rating: movie.rating || null,
+ categories: movie.categories || null,
+ countries: movie.countries || null,
+ sources: movie.sources || null,
+ active: false,
+ })
+ created++
+ }
+ }
+
+ return {
+ success: true,
+ sources,
+ total: allMovies.length,
+ created,
+ updated,
+ }
+})
diff --git a/server/api/movies.get.ts b/server/api/movies.get.ts
index cdc2661..78eae6c 100644
--- a/server/api/movies.get.ts
+++ b/server/api/movies.get.ts
@@ -1,12 +1,83 @@
-import { getKoreanMovies } from '../utils/movies'
+import { movies } from '../database/schema'
+import { desc, like, and, eq, sql } from 'drizzle-orm'
export default defineEventHandler(async (event) => {
+ const db = useDb()
const query = getQuery(event)
- const page = Math.max(Number(query.page || 1), 1)
+ const page = Math.max(Number(query.page) || 1, 1)
+ const limit = 24
+ const offset = (page - 1) * limit
const keyword = typeof query.keyword === 'string' ? query.keyword.trim() : ''
- const source = typeof query.source === 'string' && ['ophim', 'nguonc', 'kkphim'].includes(query.source)
- ? query.source as 'ophim' | 'nguonc' | 'kkphim'
- : 'all'
- return await getKoreanMovies(page, keyword, source)
+ const cacheKey = `cinek:public:movies:${page}:${keyword}`
+
+ try {
+ const redis = useRedis()
+ const cached = await redis.get(cacheKey)
+ if (cached) {
+ return JSON.parse(cached)
+ }
+ } catch {}
+
+ const whereClause = keyword
+ ? and(
+ eq(movies.active, true),
+ like(movies.name, `%${keyword}%`),
+ )
+ : eq(movies.active, true)
+
+ const [items, countResult] = await Promise.all([
+ db
+ .select()
+ .from(movies)
+ .where(whereClause)
+ .orderBy(desc(movies.syncedAt))
+ .limit(limit)
+ .offset(offset),
+ db
+ .select({ count: sql`count(*)` })
+ .from(movies)
+ .where(whereClause),
+ ])
+
+ const total = countResult[0]?.count ?? 0
+
+ const result = {
+ items: items.map(mapMovieToResponse),
+ page,
+ pagination: {
+ currentPage: page,
+ totalPages: Math.ceil(total / limit),
+ totalItems: total,
+ },
+ }
+
+ try {
+ const redis = useRedis()
+ await redis.set(cacheKey, JSON.stringify(result), 'EX', 300)
+ } catch {}
+
+ return result
})
+
+function mapMovieToResponse(movie: any) {
+ return {
+ id: `${movie.source}:${movie.slug}`,
+ source: movie.source,
+ slug: movie.slug,
+ name: movie.name,
+ originName: movie.originName || '',
+ thumb: movie.customThumb || movie.thumb || '',
+ poster: movie.customPoster || movie.poster || '',
+ year: movie.year || undefined,
+ time: movie.time || undefined,
+ episode: movie.episode || undefined,
+ quality: movie.quality || undefined,
+ lang: movie.lang || undefined,
+ type: movie.type || undefined,
+ rating: movie.rating || undefined,
+ categories: movie.categories || [],
+ countries: movie.countries || [],
+ sources: movie.sources || [],
+ }
+}
diff --git a/server/api/movies/[slug].get.ts b/server/api/movies/[slug].get.ts
index 8b550e2..78b5325 100644
--- a/server/api/movies/[slug].get.ts
+++ b/server/api/movies/[slug].get.ts
@@ -1,3 +1,5 @@
+import { movies } from '../../database/schema'
+import { eq, and } from 'drizzle-orm'
import { getMovieDetailGroup, parseSourceRefs } from '../../utils/movies'
export default defineEventHandler(async (event) => {
@@ -8,6 +10,27 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 400, statusMessage: 'Thiếu slug phim' })
}
+ const source = typeof query.source === 'string' ? query.source : 'nguonc'
+ const db = useDb()
+
+ const existing = await db
+ .select()
+ .from(movies)
+ .where(and(eq(movies.slug, slug), eq(movies.source, source), eq(movies.active, true)))
+ .limit(1)
+
+ if (!existing.length) {
+ const fallback = await db
+ .select()
+ .from(movies)
+ .where(and(eq(movies.slug, slug), eq(movies.active, true)))
+ .limit(1)
+
+ if (!fallback.length) {
+ throw createError({ statusCode: 404, statusMessage: 'Phim không tồn tại hoặc đã bị ẩn' })
+ }
+ }
+
const refs = parseSourceRefs(query.sources || query.srcs, query.source, slug)
return await getMovieDetailGroup(refs)
diff --git a/server/database/migrations/0000_regular_quentin_quire.sql b/server/database/migrations/0000_regular_quentin_quire.sql
new file mode 100644
index 0000000..a373fd3
--- /dev/null
+++ b/server/database/migrations/0000_regular_quentin_quire.sql
@@ -0,0 +1,25 @@
+CREATE TABLE `movies` (
+ `id` int AUTO_INCREMENT NOT NULL,
+ `source` varchar(50) NOT NULL,
+ `slug` varchar(500) NOT NULL,
+ `name` varchar(500) NOT NULL,
+ `origin_name` varchar(500),
+ `thumb` text,
+ `poster` text,
+ `year` int,
+ `time` varchar(100),
+ `episode` varchar(100),
+ `quality` varchar(50),
+ `lang` varchar(50),
+ `type` varchar(50),
+ `rating` int,
+ `content` text,
+ `categories` json,
+ `countries` json,
+ `sources` json,
+ `active` boolean NOT NULL DEFAULT false,
+ `synced_at` timestamp NOT NULL DEFAULT (now()),
+ `created_at` timestamp NOT NULL DEFAULT (now()),
+ `updated_at` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
+ CONSTRAINT `movies_id` PRIMARY KEY(`id`)
+);
diff --git a/server/database/migrations/0001_lively_nightmare.sql b/server/database/migrations/0001_lively_nightmare.sql
new file mode 100644
index 0000000..836dc37
--- /dev/null
+++ b/server/database/migrations/0001_lively_nightmare.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `movies` ADD `custom_poster` text;--> statement-breakpoint
+ALTER TABLE `movies` ADD `custom_thumb` text;--> statement-breakpoint
+ALTER TABLE `movies` ADD `custom_content` text;
\ No newline at end of file
diff --git a/server/database/migrations/0002_small_black_panther.sql b/server/database/migrations/0002_small_black_panther.sql
new file mode 100644
index 0000000..6516dda
--- /dev/null
+++ b/server/database/migrations/0002_small_black_panther.sql
@@ -0,0 +1 @@
+ALTER TABLE `movies` ADD `custom_episodes` json;
\ No newline at end of file
diff --git a/server/database/migrations/meta/0000_snapshot.json b/server/database/migrations/meta/0000_snapshot.json
new file mode 100644
index 0000000..cd91c2d
--- /dev/null
+++ b/server/database/migrations/meta/0000_snapshot.json
@@ -0,0 +1,194 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "f8d4d76b-e681-4b54-8634-7ceaed266b06",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "tables": {
+ "movies": {
+ "name": "movies",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "origin_name": {
+ "name": "origin_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "thumb": {
+ "name": "thumb",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "poster": {
+ "name": "poster",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "year": {
+ "name": "year",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "time": {
+ "name": "time",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "episode": {
+ "name": "episode",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "quality": {
+ "name": "quality",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lang": {
+ "name": "lang",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "rating": {
+ "name": "rating",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "categories": {
+ "name": "categories",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "countries": {
+ "name": "countries",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sources": {
+ "name": "sources",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "synced_at": {
+ "name": "synced_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "movies_id": {
+ "name": "movies_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ }
+ },
+ "views": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "tables": {},
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/server/database/migrations/meta/0001_snapshot.json b/server/database/migrations/meta/0001_snapshot.json
new file mode 100644
index 0000000..b08b81e
--- /dev/null
+++ b/server/database/migrations/meta/0001_snapshot.json
@@ -0,0 +1,215 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "3843ed51-a68b-4b7b-95fb-00b28b4aba56",
+ "prevId": "f8d4d76b-e681-4b54-8634-7ceaed266b06",
+ "tables": {
+ "movies": {
+ "name": "movies",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "origin_name": {
+ "name": "origin_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "thumb": {
+ "name": "thumb",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "poster": {
+ "name": "poster",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "year": {
+ "name": "year",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "time": {
+ "name": "time",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "episode": {
+ "name": "episode",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "quality": {
+ "name": "quality",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lang": {
+ "name": "lang",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "rating": {
+ "name": "rating",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "categories": {
+ "name": "categories",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "countries": {
+ "name": "countries",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sources": {
+ "name": "sources",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "custom_poster": {
+ "name": "custom_poster",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "custom_thumb": {
+ "name": "custom_thumb",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "custom_content": {
+ "name": "custom_content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "synced_at": {
+ "name": "synced_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "movies_id": {
+ "name": "movies_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ }
+ },
+ "views": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "tables": {},
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/server/database/migrations/meta/0002_snapshot.json b/server/database/migrations/meta/0002_snapshot.json
new file mode 100644
index 0000000..a7b7a94
--- /dev/null
+++ b/server/database/migrations/meta/0002_snapshot.json
@@ -0,0 +1,222 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "b3692b95-4029-4e2c-bab4-ffd0696fa2a1",
+ "prevId": "3843ed51-a68b-4b7b-95fb-00b28b4aba56",
+ "tables": {
+ "movies": {
+ "name": "movies",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "origin_name": {
+ "name": "origin_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "thumb": {
+ "name": "thumb",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "poster": {
+ "name": "poster",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "year": {
+ "name": "year",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "time": {
+ "name": "time",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "episode": {
+ "name": "episode",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "quality": {
+ "name": "quality",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lang": {
+ "name": "lang",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "rating": {
+ "name": "rating",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "categories": {
+ "name": "categories",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "countries": {
+ "name": "countries",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sources": {
+ "name": "sources",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "custom_poster": {
+ "name": "custom_poster",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "custom_thumb": {
+ "name": "custom_thumb",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "custom_content": {
+ "name": "custom_content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "custom_episodes": {
+ "name": "custom_episodes",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "synced_at": {
+ "name": "synced_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "movies_id": {
+ "name": "movies_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ }
+ },
+ "views": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "tables": {},
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/server/database/migrations/meta/_journal.json b/server/database/migrations/meta/_journal.json
new file mode 100644
index 0000000..4d1fee4
--- /dev/null
+++ b/server/database/migrations/meta/_journal.json
@@ -0,0 +1,27 @@
+{
+ "version": "7",
+ "dialect": "mysql",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "5",
+ "when": 1784692643696,
+ "tag": "0000_regular_quentin_quire",
+ "breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "5",
+ "when": 1784693272000,
+ "tag": "0001_lively_nightmare",
+ "breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "5",
+ "when": 1784694350192,
+ "tag": "0002_small_black_panther",
+ "breakpoints": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/server/database/schema.ts b/server/database/schema.ts
new file mode 100644
index 0000000..d134a6c
--- /dev/null
+++ b/server/database/schema.ts
@@ -0,0 +1,33 @@
+import { mysqlTable, varchar, text, int, boolean, timestamp, json } from 'drizzle-orm/mysql-core'
+
+export const movies = mysqlTable('movies', {
+ id: int('id').primaryKey().autoincrement(),
+ source: varchar('source', { length: 50 }).notNull(),
+ slug: varchar('slug', { length: 500 }).notNull(),
+ name: varchar('name', { length: 500 }).notNull(),
+ originName: varchar('origin_name', { length: 500 }),
+ thumb: text('thumb'),
+ poster: text('poster'),
+ year: int('year'),
+ time: varchar('time', { length: 100 }),
+ episode: varchar('episode', { length: 100 }),
+ quality: varchar('quality', { length: 50 }),
+ lang: varchar('lang', { length: 50 }),
+ type: varchar('type', { length: 50 }),
+ rating: int('rating'),
+ content: text('content'),
+ categories: json('categories').$type(),
+ countries: json('countries').$type(),
+ sources: json('sources').$type<{ source: string, slug: string, name?: string }[]>(),
+ customPoster: text('custom_poster'),
+ customThumb: text('custom_thumb'),
+ customContent: text('custom_content'),
+ customEpisodes: json('custom_episodes').$type<{ name: string, linkEmbed?: string, linkM3u8?: string }[]>(),
+ active: boolean('active').notNull().default(false),
+ syncedAt: timestamp('synced_at').notNull().defaultNow(),
+ createdAt: timestamp('created_at').notNull().defaultNow(),
+ updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow(),
+})
+
+export type Movie = typeof movies.$inferSelect
+export type NewMovie = typeof movies.$inferInsert
diff --git a/server/utils/db.ts b/server/utils/db.ts
new file mode 100644
index 0000000..389d946
--- /dev/null
+++ b/server/utils/db.ts
@@ -0,0 +1,15 @@
+import { drizzle } from 'drizzle-orm/mysql2'
+import mysql from 'mysql2/promise'
+import * as schema from '../database/schema'
+
+let _db: ReturnType> | undefined
+
+export function useDb() {
+ if (_db) return _db
+
+ const url = process.env.DATABASE_URL || 'mysql://cinek:cinekpassword@localhost:3306/cinek'
+ const pool = mysql.createPool(url)
+
+ _db = drizzle(pool, { mode: 'default', schema })
+ return _db
+}
diff --git a/server/utils/redis.ts b/server/utils/redis.ts
new file mode 100644
index 0000000..fd1054c
--- /dev/null
+++ b/server/utils/redis.ts
@@ -0,0 +1,19 @@
+import Redis from 'ioredis'
+
+let _redis: Redis | undefined
+
+export function useRedis() {
+ if (_redis) return _redis
+
+ const url = process.env.REDIS_URL || 'redis://localhost:6379'
+ _redis = new Redis(url, {
+ maxRetriesPerRequest: 3,
+ lazyConnect: true,
+ })
+
+ _redis.on('error', (err) => {
+ console.error('[Redis] Connection error:', err.message)
+ })
+
+ return _redis
+}