-
{{ item.title }}
-
-
-
-
-
-
+
+
{{ item.title }}
+
+
+
+
+
+
+
@@ -53,85 +75,125 @@
\ No newline at end of file
diff --git a/player/src/components/VideoPlayer.vue b/player/src/components/VideoPlayer.vue
index f0b8fae..7913732 100644
--- a/player/src/components/VideoPlayer.vue
+++ b/player/src/components/VideoPlayer.vue
@@ -19,6 +19,7 @@ import type Player from 'video.js/dist/types/player';
import 'video.js/dist/video-js.min.css'
import { usePlaylistStore } from '@/stores/playlist';
import { usePlayerStore } from '@/stores/player';
+import { useUserStore } from '@/stores/user';
import logger from '@/utils/logger';
import { syncManager } from '@/utils/sync/syncManager';
@@ -34,11 +35,11 @@ interface SyncData {
let player: Player | null = null;
let enable_sync = true;
let please_enable_sync = false;
-let current_video_id = 0;
const syncThreshold = 1;
const playlistStore = usePlaylistStore();
const playerStore = usePlayerStore();
+const userStore = useUserStore();
// 添加一个ref来控制同步状态
const syncEnabled = ref(true);
@@ -89,7 +90,9 @@ function initPlayer() {
player?.on('seeked', sendSyncData);
player?.on('ended', () => {
- playlistStore.switchVideo();
+ if (userStore.roomId) {
+ playlistStore.switchVideo(userStore.roomId);
+ }
});
}
@@ -156,28 +159,26 @@ async function getSyncData() {
}
}
-watch(() => playlistStore.playlistChanged, async () => {
- if (player) {
- logger.info('Playlist changed');
- const syncData = await getSyncData();
- const currentVideoId = syncData?.videoId;
- // 判断是否在播放列表中
- if (!playlistStore.playlist.find((video) => video.id === currentVideoId)) {
- logger.warn('Current video is not in playlist');
+watch(() => playlistStore.currentVideoId, async (newVideoId) => {
+ if (player && newVideoId && newVideoId !== -1) {
+ logger.info('Current video changed:', newVideoId);
+ const currentVideo = playlistStore.currentVideoItem;
+
+ if (!currentVideo || !currentVideo.videoSources || currentVideo.videoSources.length === 0) {
+ logger.warn('No video sources available');
return;
}
- if (currentVideoId !== playlistStore.currentVideoId) {
- playlistStore.switchVideo(currentVideoId);
- }
- else {
- logger.debug('Current video is already playing');
- }
- const videoSrc = playlistStore.playlist[0].VideoSources[0].url;
+
+ const videoSrc = currentVideo.videoSources[0].url;
player?.src({
src: videoSrc,
type: 'video/mp4'
});
- updatePlayer(syncData);
+
+ const syncData = await getSyncData();
+ if (syncData) {
+ updatePlayer(syncData);
+ }
player?.play();
}
});
diff --git a/player/src/config/env.ts b/player/src/config/env.ts
index 4258c0c..a12f180 100644
--- a/player/src/config/env.ts
+++ b/player/src/config/env.ts
@@ -9,15 +9,15 @@ const getEnvValue = (key: string, defaultValue: string): string => {
export const env = {
// API配置
- API_BASE_URL: getEnvValue('VITE_API_BASE_URL', 'http://localhost:3000'),
+ API_BASE_URL: getEnvValue('VITE_API_BASE_URL', 'http://localhost:3000/api'),
WS_BASE_URL: getEnvValue('VITE_WS_BASE_URL', `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}`),
-
+
// 调试配置
DEBUG: getEnvValue('VITE_DEBUG', 'false') === 'true',
-
+
// 其他配置
APP_TITLE: getEnvValue('VITE_APP_TITLE', 'Sync Player'),
-
+
// 备案信息
ICP_NUMBER: getEnvValue('VITE_ICP_NUMBER', ''),
POLICE_NUMBER: getEnvValue('VITE_POLICE_NUMBER', ''),
diff --git a/player/src/stores/playlist.ts b/player/src/stores/playlist.ts
index 86db6d1..b698898 100644
--- a/player/src/stores/playlist.ts
+++ b/player/src/stores/playlist.ts
@@ -3,7 +3,23 @@ import { defineStore } from 'pinia'
import request from '@/utils/axios'
import logger from '@/utils/logger'
-// define an interface for the playlist object
+// VideoSource interface
+export interface VideoSource {
+ id: number
+ playlistItemId: number
+ url: string
+ label: string
+ createdTime: string
+ lastActiveTime: string
+}
+
+// VideoSource input for API calls
+export interface VideoSourceInput {
+ url: string
+ label: string
+}
+
+// PlaylistItem interface
export interface PlaylistItem {
id: number
roomId: number
@@ -11,14 +27,9 @@ export interface PlaylistItem {
orderIndex: number
playStatus: string
createdTime: string
- VideoSources: {
- id: number
- playlistItemId: number
- url: string
- createdTime: string
- lastActiveTime: string
- }[]
+ videoSources: VideoSource[]
}
+
export enum PlayStatus {
NEW = 'new',
PLAYING = 'playing',
@@ -26,147 +37,112 @@ export enum PlayStatus {
}
export const usePlaylistStore = defineStore('playlist', () => {
- const playlist = ref
([]);
- const playlistLength = computed(() => playlist.value.length);
- const playlistChanged = ref(false);
- // current playing video id is the id of the playlist item whose playStatus is PLAYING
+ const playlist = ref([])
+ const playlistLength = computed(() => playlist.value.length)
+
const currentVideoId = computed(() => {
- const playingItem = playlist.value.find((video) => video.playStatus === PlayStatus.PLAYING);
- return playingItem ? playingItem.id : -1;
- });
+ const playingItem = playlist.value.find((video) => video.playStatus === PlayStatus.PLAYING)
+ return playingItem ? playingItem.id : -1
+ })
+
const currentVideoItem = computed(() => {
- return playlist.value.find((video) => video.playStatus === PlayStatus.PLAYING);
- });
-
- async function setPlaylist(newPlaylist: PlaylistItem[]) {
- // playlist.value = newPlaylist;
- const validPlaylist = newPlaylist.filter((video) => video.playStatus !== PlayStatus.FINISHED);
- playlist.value = validPlaylist;
- playlistChanged.value = !playlistChanged.value; // FIXME: a better way to trigger the playlist update
- }
+ return playlist.value.find((video) => video.playStatus === PlayStatus.PLAYING)
+ })
- async function addVideo(roomId: number, title:string, urls:string) {
+ // Fetch playlist from server
+ async function fetchPlaylist(roomId: number): Promise {
try {
- const response = await request.post('/playlist/add', {
- roomId,
- title,
- urls
- });
- if (response.status === 200) {
- const playlistItemId = response.data.playlistItemId
- playlist.value.push({
- id: playlistItemId,
- roomId,
- title,
- orderIndex: Math.max(...playlist.value.map((video) => video.orderIndex), -1) + 1, // FIXME: a better way to calculate orderIndex
- playStatus: PlayStatus.NEW,
- createdTime: new Date().toISOString(),
- VideoSources: urls.split(',').map((url, index) => ({
- id: index,
- playlistItemId,
- url,
- createdTime: new Date().toISOString(),
- lastActiveTime: new Date().toISOString()
- }))
- });
- playlistChanged.value = !playlistChanged.value; // FIXME: a better way to trigger the playlist update
- }
- }
- catch (error) {
- logger.error('Failed to add video:', error);
+ const response = await request.get('/playlist/query', { params: { roomId } })
+ playlist.value = response.data
+ } catch (error) {
+ logger.error('Failed to fetch playlist:', error)
+ throw error
}
}
- async function deleteVideo(videoId: number) {
+ function setPlaylist(newPlaylist: PlaylistItem[]): void {
+ playlist.value = newPlaylist
+ }
+
+ async function addVideo(roomId: number, title: string, sources: VideoSourceInput[]): Promise {
try {
- await request.delete('/playlist/delete', { data: { playlistItemId: videoId } });
- playlist.value = playlist.value.filter((video) => video.id !== videoId);
+ await request.post('playlist/add', { title, sources })
+ // Refetch playlist to ensure consistency with server
+ await fetchPlaylist(roomId)
+ } catch (error) {
+ logger.error('Failed to add video:', error)
+ throw error
}
- catch (error) {
- logger.error('Failed to delete video:', error);
+ }
+
+ async function deleteVideo(roomId: number, videoId: number): Promise {
+ try {
+ await request.delete('playlist/delete', { data: { playlistItemId: videoId } })
+ await fetchPlaylist(roomId)
+ } catch (error) {
+ logger.error('Failed to delete video:', error)
+ throw error
}
}
- async function swapVideos(fromId: number, toId: number) {
- const fromIndex = playlist.value.findIndex((video) => video.id === fromId);
- const toIndex = playlist.value.findIndex((video) => video.id === toId);
+ async function swapVideos(roomId: number, fromId: number, toId: number): Promise {
+ const fromIndex = playlist.value.findIndex((video) => video.id === fromId)
+ const toIndex = playlist.value.findIndex((video) => video.id === toId)
+
+ if (fromIndex === -1 || toIndex === -1) {
+ logger.error('Video not found in playlist')
+ return
+ }
- const fromOrderIndex = playlist.value[fromIndex].orderIndex;
- const toOrderIndex = playlist.value[toIndex].orderIndex;
- // orderIndexList is an array of { playlistItemId: number, orderIndex: number }
const orderIndexList = [
- { playlistItemId: fromId, orderIndex: toOrderIndex },
- { playlistItemId: toId, orderIndex: fromOrderIndex }
- ];
+ { playlistItemId: fromId, orderIndex: playlist.value[toIndex].orderIndex },
+ { playlistItemId: toId, orderIndex: playlist.value[fromIndex].orderIndex }
+ ]
+
try {
- // update the orderIndex of the two videos in the server
- await request.post('/playlist/updateOrder', { orderIndexList });
- const temp = playlist.value[fromIndex];
-
- // swap the two videos in local playlist
- playlist.value[fromIndex] = playlist.value[toIndex];
- playlist.value[fromIndex].orderIndex = fromOrderIndex;
- playlist.value[toIndex] = temp;
- playlist.value[toIndex].orderIndex = toOrderIndex;
- }
- catch (error) {
- logger.error('Failed to swap videos:', error);
+ await request.post('playlist/updateOrder', { orderIndexList })
+ await fetchPlaylist(roomId)
+ } catch (error) {
+ logger.error('Failed to swap videos:', error)
+ throw error
}
}
- async function clearPlaylist(roomId: number) {
- logger.info('Clearing playlist in roomId', roomId);
+ async function clearPlaylist(roomId: number): Promise {
try {
- await request.delete('/playlist/clear', { data: { roomId: roomId } });
- playlist.value = [];
- }
- catch (error) {
- logger.error('Failed to clear playlist:', error);
+ await request.delete('playlist/clear', { data: { roomId } })
+ playlist.value = []
+ } catch (error) {
+ logger.error('Failed to clear playlist:', error)
+ throw error
}
}
- async function switchVideo(videoId?: number) {
+ async function switchVideo(roomId: number, videoId?: number): Promise {
try {
if (videoId === undefined) {
- // 寻找第一个state为new的视频
- const nextVideo = playlist.value.find((video) => video.playStatus === PlayStatus.NEW);
+ const nextVideo = playlist.value.find((video) => video.playStatus === PlayStatus.NEW)
if (!nextVideo) {
- logger.warn('No next video to play');
- return;
+ logger.warn('No next video to play')
+ return
}
- videoId = nextVideo.id;
+ videoId = nextVideo.id
}
- await request.post('/playlist/switch', { playlistItemId: videoId });
-
- if (currentVideoId.value !== -1) {
- // Remove the currently playing video if it is not the same as the videoId
- if (currentVideoId.value !== videoId) {
- playlist.value = playlist.value.filter(
- (video) => video.id !== currentVideoId.value
- );
- }
- }
-
- // Move the videoId video to the first position and set its status to playing
- const videoIndex = playlist.value.findIndex((video) => video.id === videoId);
- if (videoIndex !== -1) {
- const video = playlist.value.splice(videoIndex, 1)[0];
- video.playStatus = PlayStatus.PLAYING;
- playlist.value.unshift(video);
- }
-
- playlistChanged.value = !playlistChanged.value; // FIXME: a better way to trigger the playlist update
+ await request.post('playlist/switch', { playlistItemId: videoId })
+ // Refetch playlist to ensure consistency with server
+ await fetchPlaylist(roomId)
} catch (error) {
- logger.error('Failed to switch video:', error);
+ logger.error('Failed to switch video:', error)
+ throw error
}
}
return {
playlist,
playlistLength,
- playlistChanged,
currentVideoId,
currentVideoItem,
+ fetchPlaylist,
setPlaylist,
addVideo,
deleteVideo,
@@ -174,4 +150,4 @@ export const usePlaylistStore = defineStore('playlist', () => {
clearPlaylist,
switchVideo
}
-});
+})
diff --git a/player/src/stores/user.ts b/player/src/stores/user.ts
index 3ae2e89..d6d9c9b 100644
--- a/player/src/stores/user.ts
+++ b/player/src/stores/user.ts
@@ -18,6 +18,7 @@ export const useUserStore = defineStore('user', () => {
const userId = ref(null);
const roomId = ref(null);
const userList = ref([]);
+ const authToken = ref(null);
function updateUserList(users: UserListItem[]) {
userList.value = users;
@@ -54,12 +55,12 @@ export const useUserStore = defineStore('user', () => {
}
if (response.data.protocol === 'websocket') {
- const wsUrl = baseURL.replace('http', 'ws').replace('api', 'socket');
+ const wsUrl = baseURL.replace('http', 'ws').replace('/api', '/ws');
logger.info('Using WebSocket protocol, setting baseURL:', wsUrl);
syncManager.setProtocol('websocket', wsUrl);
}
else if (response.data.protocol === 'sse') {
- const sseUrl = baseURL.replace('api', 'sse');
+ const sseUrl = baseURL.replace('/api', '/sse');
logger.info('Using SSE protocol, setting baseURL:', sseUrl);
syncManager.setProtocol('sse', sseUrl);
}
@@ -87,6 +88,12 @@ export const useUserStore = defineStore('user', () => {
roomId: queryRoomId,
});
+ // Save JWT token
+ if (joinRoomResponse.data.token) {
+ authToken.value = joinRoomResponse.data.token;
+ localStorage.setItem('authToken', joinRoomResponse.data.token);
+ }
+
username.value = newUsername;
roomName.value = newRoomName;
userId.value = queryUserId;
@@ -98,6 +105,7 @@ export const useUserStore = defineStore('user', () => {
throw error;
}
+ // Also save to cookie for backward compatibility
document.cookie = `userInfo=${JSON.stringify({
username: newUsername,
roomName: newRoomName,
@@ -107,10 +115,16 @@ export const useUserStore = defineStore('user', () => {
}
function loadFromCookie() {
+ // Try to load token from localStorage first
+ const savedToken = localStorage.getItem('authToken');
+ if (savedToken) {
+ authToken.value = savedToken;
+ }
+
const userInfo = document.cookie
.split('; ')
.find(row => row.startsWith('userInfo='));
-
+
if (userInfo) {
try {
const data = JSON.parse(decodeURIComponent(userInfo.split('=')[1]));
@@ -136,6 +150,7 @@ export const useUserStore = defineStore('user', () => {
userId,
roomId,
userList,
+ authToken,
login,
loadFromCookie,
connectSyncManager: connectSyncManager,
diff --git a/player/src/utils/axios.ts b/player/src/utils/axios.ts
index fac6017..6e30dc9 100644
--- a/player/src/utils/axios.ts
+++ b/player/src/utils/axios.ts
@@ -38,6 +38,12 @@ function initAxios() {
// 请求拦截器
request.interceptors.request.use(
config => {
+ // Add JWT token to Authorization header if available
+ const token = localStorage.getItem('authToken');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+
logger.debug('Request:', config.method?.toUpperCase(), config.url);
return config;
},
diff --git a/player/src/utils/sync/adapters/sse.ts b/player/src/utils/sync/adapters/sse.ts
index 1a1c06e..46e8b6d 100644
--- a/player/src/utils/sync/adapters/sse.ts
+++ b/player/src/utils/sync/adapters/sse.ts
@@ -25,7 +25,16 @@ export class SSEAdapter implements ISyncAdapter {
this.url = url;
this.userId = userId;
this.roomId = roomId;
- const fullUrl = `${this.url}/connect?userId=${userId}&roomId=${roomId}`;
+
+ // Get JWT token from localStorage
+ const token = localStorage.getItem('authToken');
+
+ // Add token as query parameter if available (EventSource doesn't support custom headers)
+ let fullUrl = `${this.url}/connect?userId=${userId}&roomId=${roomId}`;
+ if (token) {
+ fullUrl += `&token=${encodeURIComponent(token)}`;
+ }
+
this.es = new EventSource(fullUrl);
this.es.onmessage = (event) => this.handleMessage(event);
diff --git a/player/src/utils/sync/adapters/websocket.ts b/player/src/utils/sync/adapters/websocket.ts
index 4c46bd1..e52f89d 100644
--- a/player/src/utils/sync/adapters/websocket.ts
+++ b/player/src/utils/sync/adapters/websocket.ts
@@ -10,13 +10,17 @@ export class WebSocketAdapter implements ISyncAdapter {
connect(url: string, userId: number, roomId: number): void {
logger.debug('连接WebSocket:', url);
this.ws = new WebSocket(url);
-
+
this.ws.onopen = () => {
logger.info('WebSocket连接已建立');
if (this.ws) {
+ // Get JWT token from localStorage
+ const token = localStorage.getItem('authToken');
+
this.ws.send(JSON.stringify({
type: 'auth',
payload: {
+ token,
userId,
roomId
}
diff --git a/player/vite.config.ts b/player/vite.config.ts
index d926d07..b83cdca 100644
--- a/player/vite.config.ts
+++ b/player/vite.config.ts
@@ -35,8 +35,7 @@ export default defineConfig(({ mode }) => {
'/api': {
target: env.VITE_API_BASE_URL || 'http://localhost:3000',
changeOrigin: true,
- secure: false,
- rewrite: (path) => path.replace(/^\/api/, '/api')
+ secure: false
},
'/socket': {
target: env.VITE_WS_BASE_URL || 'ws://localhost:3000',
@@ -44,6 +43,11 @@ export default defineConfig(({ mode }) => {
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/socket/, '')
+ },
+ '/sse': {
+ target: env.VITE_API_BASE_URL || 'http://localhost:3000',
+ changeOrigin: true,
+ secure: false
}
}
}
diff --git a/server/.env.example b/server/.env.example
index 7e14069..1f83e89 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -26,4 +26,13 @@ LOG_LEVEL=info # log level
DB_LOGGING=false # whether to log database queries
# SYNC Configuration
-SYNC_PROTOCOL=websocket # sync protocol websocket or sse
\ No newline at end of file
+SYNC_PROTOCOL=websocket # sync protocol websocket or sse
+
+# CORS Configuration
+# Comma-separated list of allowed origins for CORS
+# Default allows common development ports
+CORS_ALLOW_ORIGINS=http://localhost:3000,http://localhost:5173,http://localhost:8080
+
+# JWT Configuration
+JWT_SECRET=your-secret-key-change-this-in-production # JWT secret for signing tokens
+JWT_EXPIRY_HOURS=24 # JWT token expiry in hours (default 24 hours)
diff --git a/server/.gitignore b/server/.gitignore
index 750d7ce..25ff147 100644
--- a/server/.gitignore
+++ b/server/.gitignore
@@ -1,10 +1,49 @@
-**/node_modules
-dist/
-**/**/*.log
-*.sqlite
-
-logs/
-*.log
-.env
-
-**/web/
\ No newline at end of file
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool
+*.out
+
+# Go workspace file
+go.work
+
+# Dependency directories
+vendor/
+
+# Build output
+/bin/
+/dist/
+/build/
+
+# IDE and editor files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+.DS_Store
+
+# Environment variables
+.env
+.env.local
+.env.*.local
+
+# Database files
+*.db
+*.sqlite
+*.sqlite3
+
+# Log files
+*.log
+logs/
+
+# Temporary files
+tmp/
+temp/
diff --git a/server/Dockerfile b/server/Dockerfile
deleted file mode 100644
index 4848b1a..0000000
--- a/server/Dockerfile
+++ /dev/null
@@ -1,10 +0,0 @@
-FROM node:lts-alpine
-
-WORKDIR /app
-COPY package*.json ./
-RUN npm install
-COPY . .
-RUN npm run build
-
-EXPOSE 3000
-CMD ["npm", "start"]
\ No newline at end of file
diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go
new file mode 100644
index 0000000..2777af3
--- /dev/null
+++ b/server/cmd/server/main.go
@@ -0,0 +1,98 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/signal"
+ "strings"
+ "sync-player-server/internal/config"
+ "sync-player-server/internal/database"
+ "sync-player-server/internal/routes"
+ "sync-player-server/internal/sync"
+ "sync-player-server/internal/sync/adapters"
+ "syscall"
+
+ "github.com/gin-contrib/cors"
+ "github.com/gin-gonic/gin"
+)
+
+func main() {
+ if err := config.LoadEnv(); err != nil {
+ fmt.Printf("Failed to load environment: %v\n", err)
+ os.Exit(1)
+ }
+
+ if err := config.SetupLogger(); err != nil {
+ fmt.Printf("Failed to setup logger: %v\n", err)
+ os.Exit(1)
+ }
+
+ config.Logger.Info("Starting Sync Player Server...")
+
+ if err := database.InitDatabase(); err != nil {
+ config.Logger.Fatalf("Failed to initialize database: %v", err)
+ }
+ defer database.Close()
+
+ if config.Env.NodeEnv == "production" {
+ gin.SetMode(gin.ReleaseMode)
+ }
+ r := gin.Default()
+
+ corsConfig := cors.DefaultConfig()
+ allowOrigins := strings.Split(config.Env.CorsAllowOrigins, ",")
+ for i, origin := range allowOrigins {
+ allowOrigins[i] = strings.TrimSpace(origin)
+ }
+ corsConfig.AllowOrigins = allowOrigins
+ corsConfig.AllowCredentials = true
+ corsConfig.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "Cookie"}
+ corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}
+ r.Use(cors.New(corsConfig))
+
+ adapter, err := adapters.NewAdapter(config.Env.SyncProtocol)
+ if err != nil {
+ config.Logger.Fatalf("Failed to create sync adapter: %v", err)
+ }
+
+ sync.InitSyncManager(adapter)
+
+ var wsAdapter *adapters.WebSocketAdapter
+ var sseAdapter *adapters.SSEAdapter
+
+ if config.Env.SyncProtocol == "websocket" {
+ wsAdapter, _ = adapter.(*adapters.WebSocketAdapter)
+ config.Logger.Info("Using WebSocket for sync")
+ } else if config.Env.SyncProtocol == "sse" {
+ sseAdapter, _ = adapter.(*adapters.SSEAdapter)
+ config.Logger.Info("Using SSE for sync")
+ }
+
+ routes.SetupRoutes(r, wsAdapter, sseAdapter)
+
+ addr := fmt.Sprintf(":%d", config.Env.Port)
+ config.Logger.Infof("Server listening on %s", addr)
+ config.Logger.Infof("Environment: %s", config.Env.NodeEnv)
+ config.Logger.Infof("Database: %s", config.Env.DBDialect)
+ config.Logger.Infof("Sync Protocol: %s", config.Env.SyncProtocol)
+
+ go func() {
+ if err := r.Run(addr); err != nil {
+ config.Logger.Fatalf("Failed to start server: %v", err)
+ }
+ }()
+
+ quit := make(chan os.Signal, 1)
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+ <-quit
+
+ config.Logger.Info("Shutting down server...")
+
+ if adapter != nil {
+ if err := adapter.Stop(); err != nil {
+ config.Logger.Errorf("Error stopping sync adapter: %v", err)
+ }
+ }
+
+ config.Logger.Info("Server stopped")
+}
diff --git a/server/go.mod b/server/go.mod
new file mode 100644
index 0000000..548db54
--- /dev/null
+++ b/server/go.mod
@@ -0,0 +1,52 @@
+module sync-player-server
+
+go 1.24.6
+
+require (
+ github.com/gin-contrib/cors v1.7.6
+ github.com/gin-gonic/gin v1.10.1
+ github.com/gorilla/websocket v1.5.1
+ github.com/joho/godotenv v1.5.1
+ github.com/sirupsen/logrus v1.9.3
+ gorm.io/driver/mysql v1.5.2
+ gorm.io/driver/postgres v1.5.4
+ gorm.io/driver/sqlite v1.5.4
+ gorm.io/gorm v1.25.5
+)
+
+require (
+ github.com/bytedance/sonic v1.13.3 // indirect
+ github.com/bytedance/sonic/loader v0.2.4 // indirect
+ github.com/cloudwego/base64x v0.1.5 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.9 // indirect
+ github.com/gin-contrib/sse v1.1.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.26.0 // indirect
+ github.com/go-sql-driver/mysql v1.7.0 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+ github.com/jackc/pgx/v5 v5.4.3 // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.10 // indirect
+ github.com/kr/text v0.2.0 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-sqlite3 v1.14.17 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.3.0 // indirect
+ golang.org/x/arch v0.18.0 // indirect
+ golang.org/x/crypto v0.39.0 // indirect
+ golang.org/x/net v0.41.0 // indirect
+ golang.org/x/sys v0.33.0 // indirect
+ golang.org/x/text v0.26.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/server/go.sum b/server/go.sum
new file mode 100644
index 0000000..24b8810
--- /dev/null
+++ b/server/go.sum
@@ -0,0 +1,124 @@
+github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
+github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
+github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
+github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
+github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
+github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
+github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
+github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
+github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
+github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
+github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
+github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
+golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
+golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
+golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
+golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
+gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
+gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
+gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
+gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
+gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
+gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
+gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
diff --git a/server/internal/config/database.go b/server/internal/config/database.go
new file mode 100644
index 0000000..2362bc1
--- /dev/null
+++ b/server/internal/config/database.go
@@ -0,0 +1,65 @@
+package config
+
+import (
+ "fmt"
+
+ "gorm.io/driver/mysql"
+ "gorm.io/driver/postgres"
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+)
+
+// GetDatabaseDialector returns the appropriate GORM dialector based on DB_DIALECT
+func GetDatabaseDialector() gorm.Dialector {
+ switch Env.DBDialect {
+ case "sqlite":
+ return sqlite.Open(Env.DBStorage)
+
+ case "mysql":
+ dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
+ Env.MySQLUsername,
+ Env.MySQLPassword,
+ Env.MySQLHost,
+ Env.MySQLPort,
+ Env.MySQLDatabase,
+ )
+ if Env.DBEnableSSL {
+ dsn += "&tls=true"
+ }
+ return mysql.Open(dsn)
+
+ case "postgres":
+ dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s",
+ Env.PostgresHost,
+ Env.PostgresPort,
+ Env.PostgresUsername,
+ Env.PostgresPassword,
+ Env.PostgresDatabase,
+ )
+ if Env.DBEnableSSL {
+ dsn += " sslmode=require"
+ } else {
+ dsn += " sslmode=disable"
+ }
+ return postgres.Open(dsn)
+
+ default:
+ panic(fmt.Sprintf("unsupported database dialect: %s", Env.DBDialect))
+ }
+}
+
+// GetGormConfig returns GORM configuration
+func GetGormConfig() *gorm.Config {
+ var logLevel logger.LogLevel
+ if Env.DBLogging {
+ logLevel = logger.Info
+ } else {
+ logLevel = logger.Silent
+ }
+
+ return &gorm.Config{
+ Logger: logger.Default.LogMode(logLevel),
+ NamingStrategy: nil,
+ }
+}
diff --git a/server/internal/config/env.go b/server/internal/config/env.go
new file mode 100644
index 0000000..80218ef
--- /dev/null
+++ b/server/internal/config/env.go
@@ -0,0 +1,158 @@
+package config
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+
+ "github.com/joho/godotenv"
+ "github.com/sirupsen/logrus"
+)
+
+// EnvConfig holds all environment configuration
+type EnvConfig struct {
+ NodeEnv string
+ Port int
+ DBDialect string
+ DBStorage string
+ DBLogging bool
+ LogLevel string
+
+ MySQLHost string
+ MySQLPort int
+ MySQLDatabase string
+ MySQLUsername string
+ MySQLPassword string
+
+ PostgresHost string
+ PostgresPort int
+ PostgresDatabase string
+ PostgresUsername string
+ PostgresPassword string
+
+ DBEnableSSL bool
+ SyncProtocol string
+ CorsAllowOrigins string
+ JWTSecret string
+ JWTExpiryHours int
+}
+
+var Env *EnvConfig
+
+// getEnvValue gets environment variable with default value
+func getEnvValue(key string, defaultValue string) string {
+ value := os.Getenv(key)
+ if value == "" {
+ return defaultValue
+ }
+ return value
+}
+
+// getEnvInt gets environment variable as integer
+func getEnvInt(key string, defaultValue int) int {
+ value := os.Getenv(key)
+ if value == "" {
+ return defaultValue
+ }
+ intValue, err := strconv.Atoi(value)
+ if err != nil {
+ return defaultValue
+ }
+ return intValue
+}
+
+// getEnvBool gets environment variable as boolean
+func getEnvBool(key string, defaultValue bool) bool {
+ value := os.Getenv(key)
+ if value == "" {
+ return defaultValue
+ }
+ return value == "true"
+}
+
+// LoadEnv loads environment variables from .env file
+func LoadEnv() error {
+ // Load .env file (ignore error if file doesn't exist)
+ _ = godotenv.Load()
+
+ Env = &EnvConfig{
+ NodeEnv: getEnvValue("NODE_ENV", "development"),
+ Port: getEnvInt("PORT", 3000),
+ DBDialect: getEnvValue("DB_DIALECT", "sqlite"),
+ DBStorage: getEnvValue("DB_STORAGE", "./data/sync-player.sqlite"),
+ DBLogging: getEnvBool("DB_LOGGING", false),
+ LogLevel: getEnvValue("LOG_LEVEL", "info"),
+
+ MySQLHost: getEnvValue("MYSQL_HOST", "localhost"),
+ MySQLPort: getEnvInt("MYSQL_PORT", 3306),
+ MySQLDatabase: getEnvValue("MYSQL_DATABASE", "sync_player"),
+ MySQLUsername: getEnvValue("MYSQL_USERNAME", "root"),
+ MySQLPassword: getEnvValue("MYSQL_PASSWORD", "password"),
+
+ PostgresHost: getEnvValue("POSTGRES_HOST", "localhost"),
+ PostgresPort: getEnvInt("POSTGRES_PORT", 5432),
+ PostgresDatabase: getEnvValue("POSTGRES_DATABASE", "sync_player"),
+ PostgresUsername: getEnvValue("POSTGRES_USERNAME", "postgres"),
+ PostgresPassword: getEnvValue("POSTGRES_PASSWORD", "password"),
+
+ DBEnableSSL: getEnvBool("DB_ENABLE_SSL", false),
+ SyncProtocol: getEnvValue("SYNC_PROTOCOL", "websocket"),
+ CorsAllowOrigins: getEnvValue("CORS_ALLOW_ORIGINS", "http://localhost:3000,http://localhost:5173,http://localhost:8080,http://127.0.0.1:3000,http://127.0.0.1:5173,http://127.0.0.1:8080"),
+ JWTSecret: getEnvValue("JWT_SECRET", "your-default-secret-key-change-this"),
+ JWTExpiryHours: getEnvInt("JWT_EXPIRY_HOURS", 24),
+ }
+
+ return validateEnv()
+}
+
+// validateEnv validates required environment variables
+func validateEnv() error {
+ logger := logrus.New()
+ logger.SetLevel(logrus.InfoLevel)
+
+ // Validate DB_DIALECT
+ if Env.DBDialect != "sqlite" && Env.DBDialect != "mysql" && Env.DBDialect != "postgres" {
+ logger.Errorf("Unsupported DB_DIALECT: %s", Env.DBDialect)
+ return fmt.Errorf("unsupported DB_DIALECT: %s", Env.DBDialect)
+ }
+
+ // Validate based on dialect
+ if Env.DBDialect == "mysql" {
+ requiredEnvs := map[string]string{
+ "MYSQL_HOST": Env.MySQLHost,
+ "MYSQL_DATABASE": Env.MySQLDatabase,
+ "MYSQL_USERNAME": Env.MySQLUsername,
+ }
+ for key, value := range requiredEnvs {
+ if value == "" {
+ logger.Errorf("Missing required environment variable: %s", key)
+ return fmt.Errorf("missing required environment variable: %s", key)
+ }
+ }
+ } else if Env.DBDialect == "postgres" {
+ requiredEnvs := map[string]string{
+ "POSTGRES_HOST": Env.PostgresHost,
+ "POSTGRES_DATABASE": Env.PostgresDatabase,
+ "POSTGRES_USERNAME": Env.PostgresUsername,
+ }
+ for key, value := range requiredEnvs {
+ if value == "" {
+ logger.Errorf("Missing required environment variable: %s", key)
+ return fmt.Errorf("missing required environment variable: %s", key)
+ }
+ }
+ } else if Env.DBDialect == "sqlite" {
+ if Env.DBStorage == "" {
+ logger.Error("Missing required environment variable: DB_STORAGE")
+ return fmt.Errorf("missing required environment variable: DB_STORAGE")
+ }
+ }
+
+ // Validate SYNC_PROTOCOL
+ if Env.SyncProtocol != "websocket" && Env.SyncProtocol != "sse" {
+ logger.Warnf("Invalid SYNC_PROTOCOL: %s, defaulting to websocket", Env.SyncProtocol)
+ Env.SyncProtocol = "websocket"
+ }
+
+ return nil
+}
diff --git a/server/internal/config/logger.go b/server/internal/config/logger.go
new file mode 100644
index 0000000..3830b9a
--- /dev/null
+++ b/server/internal/config/logger.go
@@ -0,0 +1,80 @@
+package config
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/sirupsen/logrus"
+)
+
+var Logger *logrus.Logger
+
+// SetupLogger initializes the logger with configuration
+func SetupLogger() error {
+ Logger = logrus.New()
+
+ level, err := logrus.ParseLevel(Env.LogLevel)
+ if err != nil {
+ level = logrus.InfoLevel
+ }
+ Logger.SetLevel(level)
+
+ Logger.SetFormatter(&logrus.TextFormatter{
+ FullTimestamp: true,
+ TimestampFormat: "2006-01-02 15:04:05.000",
+ })
+
+ logsDir := "logs"
+ if err := os.MkdirAll(logsDir, 0755); err != nil {
+ return err
+ }
+
+ allLogFile, err := os.OpenFile(
+ filepath.Join(logsDir, "all.log"),
+ os.O_CREATE|os.O_WRONLY|os.O_APPEND,
+ 0666,
+ )
+ if err != nil {
+ return err
+ }
+
+ errorLogFile, err := os.OpenFile(
+ filepath.Join(logsDir, "error.log"),
+ os.O_CREATE|os.O_WRONLY|os.O_APPEND,
+ 0666,
+ )
+ if err != nil {
+ return err
+ }
+
+ Logger.SetOutput(io.MultiWriter(os.Stdout, allLogFile))
+
+ Logger.AddHook(&ErrorFileHook{
+ Writer: errorLogFile,
+ LogLevels: []logrus.Level{logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel},
+ })
+
+ return nil
+}
+
+// ErrorFileHook is a hook that writes error logs to a separate file
+type ErrorFileHook struct {
+ Writer io.Writer
+ LogLevels []logrus.Level
+}
+
+// Levels returns the log levels that this hook should fire for
+func (hook *ErrorFileHook) Levels() []logrus.Level {
+ return hook.LogLevels
+}
+
+// Fire writes the log entry to the error log file
+func (hook *ErrorFileHook) Fire(entry *logrus.Entry) error {
+ line, err := entry.String()
+ if err != nil {
+ return err
+ }
+ _, err = hook.Writer.Write([]byte(line))
+ return err
+}
diff --git a/server/internal/database/connection.go b/server/internal/database/connection.go
new file mode 100644
index 0000000..cf39bc1
--- /dev/null
+++ b/server/internal/database/connection.go
@@ -0,0 +1,55 @@
+package database
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync-player-server/internal/config"
+
+ "gorm.io/gorm"
+)
+
+var DB *gorm.DB
+
+// Connect establishes database connection
+func Connect() error {
+ var err error
+
+ if config.Env.DBDialect == "sqlite" {
+ dataDir := filepath.Dir(config.Env.DBStorage)
+ if err := os.MkdirAll(dataDir, 0755); err != nil {
+ return fmt.Errorf("failed to create data directory: %w", err)
+ }
+ }
+
+ dialector := config.GetDatabaseDialector()
+ gormConfig := config.GetGormConfig()
+
+ DB, err = gorm.Open(dialector, gormConfig)
+ if err != nil {
+ return fmt.Errorf("failed to connect to database: %w", err)
+ }
+
+ db, err := DB.DB()
+ if err != nil {
+ return fmt.Errorf("failed to get database instance: %w", err)
+ }
+
+ db.SetMaxIdleConns(10)
+ db.SetMaxOpenConns(100)
+
+ config.Logger.Info("Successfully connected to database")
+ return nil
+}
+
+// Close closes the database connection
+func Close() error {
+ if DB != nil {
+ db, err := DB.DB()
+ if err != nil {
+ return err
+ }
+ return db.Close()
+ }
+ return nil
+}
diff --git a/server/internal/database/init.go b/server/internal/database/init.go
new file mode 100644
index 0000000..f569c60
--- /dev/null
+++ b/server/internal/database/init.go
@@ -0,0 +1,39 @@
+package database
+
+import (
+ "fmt"
+ "sync-player-server/internal/config"
+ "sync-player-server/internal/models"
+)
+
+// InitDatabase initializes the database and runs migrations
+func InitDatabase() error {
+ if err := Connect(); err != nil {
+ return err
+ }
+
+ if err := AutoMigrate(); err != nil {
+ return err
+ }
+
+ config.Logger.Info("Database initialized successfully")
+ return nil
+}
+
+// AutoMigrate runs database migrations for all models
+func AutoMigrate() error {
+ err := DB.AutoMigrate(
+ &models.User{},
+ &models.Room{},
+ &models.RoomMember{},
+ &models.PlaylistItem{},
+ &models.VideoSource{},
+ &models.RoomPlayStatus{},
+ )
+ if err != nil {
+ return fmt.Errorf("failed to migrate database: %w", err)
+ }
+
+ config.Logger.Info("Database models synced")
+ return nil
+}
diff --git a/server/internal/database/playlist.go b/server/internal/database/playlist.go
new file mode 100644
index 0000000..b7a823d
--- /dev/null
+++ b/server/internal/database/playlist.go
@@ -0,0 +1,184 @@
+package database
+
+import (
+ "sync-player-server/internal/models"
+
+ "gorm.io/gorm"
+)
+
+// VideoSourceInput represents the input for creating a video source
+type VideoSourceInput struct {
+ URL string `json:"url"`
+ Label string `json:"label"`
+}
+
+// AddItemToPlaylist adds an item to the playlist
+func AddItemToPlaylist(roomID uint, title string, sources []VideoSourceInput, tx ...*gorm.DB) (uint, error) {
+ db := getDB(tx...)
+
+ var maxOrderIndex *int
+ db.Model(&models.PlaylistItem{}).
+ Where("room_id = ?", roomID).
+ Select("MAX(order_index)").
+ Scan(&maxOrderIndex)
+
+ orderIndex := 0
+ if maxOrderIndex != nil {
+ orderIndex = *maxOrderIndex + 1
+ }
+
+ playlistItem := &models.PlaylistItem{
+ RoomID: roomID,
+ Title: title,
+ OrderIndex: orderIndex,
+ PlayStatus: models.PlayStatusNew,
+ }
+
+ if err := db.Create(playlistItem).Error; err != nil {
+ return 0, err
+ }
+
+ for _, source := range sources {
+ videoSource := &models.VideoSource{
+ PlaylistItemID: playlistItem.ID,
+ URL: source.URL,
+ Label: source.Label,
+ }
+ if err := db.Create(videoSource).Error; err != nil {
+ return 0, err
+ }
+ }
+
+ return playlistItem.ID, nil
+}
+
+// QueryPlaylistItems retrieves playlist items based on filters
+func QueryPlaylistItems(roomID uint, playlistItemID *uint, playStatus *models.PlayStatus) ([]models.PlaylistItem, error) {
+ var items []models.PlaylistItem
+
+ query := DB.Where("room_id = ?", roomID)
+
+ if playlistItemID != nil {
+ query = query.Where("id = ?", *playlistItemID)
+ }
+
+ if playStatus != nil {
+ query = query.Where("play_status = ?", *playStatus)
+ }
+
+ err := query.Preload("VideoSources").
+ Order("order_index ASC").
+ Find(&items).Error
+
+ if err != nil {
+ return nil, err
+ }
+
+ return items, nil
+}
+
+// DeletePlaylistItem deletes a playlist item and its video sources
+func DeletePlaylistItem(playlistItemID uint) error {
+ return DB.Transaction(func(tx *gorm.DB) error {
+ if err := tx.Where("playlist_item_id = ?", playlistItemID).Delete(&models.VideoSource{}).Error; err != nil {
+ return err
+ }
+
+ if err := tx.Delete(&models.PlaylistItem{}, playlistItemID).Error; err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+// ClearPlaylist clears all playlist items in a room
+func ClearPlaylist(roomID uint) error {
+ return DB.Transaction(func(tx *gorm.DB) error {
+ var playlistItemIDs []uint
+ if err := tx.Model(&models.PlaylistItem{}).
+ Where("room_id = ?", roomID).
+ Pluck("id", &playlistItemIDs).Error; err != nil {
+ return err
+ }
+
+ if len(playlistItemIDs) > 0 {
+ if err := tx.Where("playlist_item_id IN ?", playlistItemIDs).Delete(&models.VideoSource{}).Error; err != nil {
+ return err
+ }
+ }
+
+ if err := tx.Where("room_id = ?", roomID).Delete(&models.PlaylistItem{}).Error; err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
+// UpdatePlaylistItem updates a playlist item
+func UpdatePlaylistItem(playlistItemID uint, title *string, sources []VideoSourceInput, orderIndex *int) error {
+ return DB.Transaction(func(tx *gorm.DB) error {
+ updates := make(map[string]interface{})
+
+ if title != nil {
+ updates["title"] = *title
+ }
+
+ if orderIndex != nil {
+ updates["order_index"] = *orderIndex
+ }
+
+ if len(updates) > 0 {
+ if err := tx.Model(&models.PlaylistItem{}).Where("id = ?", playlistItemID).Updates(updates).Error; err != nil {
+ return err
+ }
+ }
+
+ if sources != nil {
+ if err := tx.Where("playlist_item_id = ?", playlistItemID).Delete(&models.VideoSource{}).Error; err != nil {
+ return err
+ }
+
+ for _, source := range sources {
+ videoSource := &models.VideoSource{
+ PlaylistItemID: playlistItemID,
+ URL: source.URL,
+ Label: source.Label,
+ }
+ if err := tx.Create(videoSource).Error; err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+ })
+}
+
+// OrderIndexUpdate represents an order index update
+type OrderIndexUpdate struct {
+ PlaylistItemID uint `json:"playlistItemId"`
+ OrderIndex int `json:"orderIndex"`
+}
+
+// UpdatePlaylistOrderBatch updates multiple playlist items' order indices in a transaction
+func UpdatePlaylistOrderBatch(updates []OrderIndexUpdate) error {
+ return DB.Transaction(func(tx *gorm.DB) error {
+ for _, update := range updates {
+ if err := tx.Model(&models.PlaylistItem{}).
+ Where("id = ?", update.PlaylistItemID).
+ Update("order_index", update.OrderIndex).Error; err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+
+// UpdatePlayStatus updates the play status of a playlist item
+func UpdatePlayStatus(playlistItemID uint, playStatus models.PlayStatus) error {
+ return DB.Model(&models.PlaylistItem{}).
+ Where("id = ?", playlistItemID).
+ Update("play_status", playStatus).Error
+}
diff --git a/server/internal/database/room.go b/server/internal/database/room.go
new file mode 100644
index 0000000..696fe63
--- /dev/null
+++ b/server/internal/database/room.go
@@ -0,0 +1,49 @@
+package database
+
+import (
+ "sync-player-server/internal/models"
+
+ "gorm.io/gorm"
+)
+
+// CreateRoom creates a new room
+func CreateRoom(name string, password *string, tx ...*gorm.DB) (*models.Room, error) {
+ db := getDB(tx...)
+
+ room := &models.Room{
+ Name: name,
+ PasswordHash: password,
+ }
+
+ if err := db.Create(room).Error; err != nil {
+ return nil, err
+ }
+
+ return room, nil
+}
+
+// GetRoomByID retrieves a room by ID
+func GetRoomByID(id uint) (*models.Room, error) {
+ var room models.Room
+ if err := DB.First(&room, id).Error; err != nil {
+ return nil, err
+ }
+ return &room, nil
+}
+
+// GetRoomByName retrieves a room by name
+func GetRoomByName(name string) (*models.Room, error) {
+ var room models.Room
+ if err := DB.Where("name = ?", name).First(&room).Error; err != nil {
+ return nil, err
+ }
+ return &room, nil
+}
+
+// VerifyRoomPassword verifies if the provided password matches the room's password
+func VerifyRoomPassword(room *models.Room, password string) bool {
+ if room.PasswordHash == nil {
+ return true
+ }
+ return password == *room.PasswordHash
+}
diff --git a/server/internal/database/room_member.go b/server/internal/database/room_member.go
new file mode 100644
index 0000000..2189b37
--- /dev/null
+++ b/server/internal/database/room_member.go
@@ -0,0 +1,82 @@
+package database
+
+import (
+ "sync-player-server/internal/models"
+
+ "gorm.io/gorm"
+)
+
+// AddMemberToRoom adds a member to a room
+func AddMemberToRoom(roomID, userID uint, isAdmin, canGrantAdmin bool, tx ...*gorm.DB) (*models.RoomMember, error) {
+ db := getDB(tx...)
+
+ member := &models.RoomMember{
+ RoomID: roomID,
+ UserID: userID,
+ IsAdmin: isAdmin,
+ CanGrantAdmin: canGrantAdmin,
+ }
+
+ if err := db.Create(member).Error; err != nil {
+ return nil, err
+ }
+
+ return member, nil
+}
+
+// RemoveMemberFromRoom removes a member from a room
+func RemoveMemberFromRoom(roomID, userID uint, tx ...*gorm.DB) error {
+ db := getDB(tx...)
+
+ return db.Where("room_id = ? AND user_id = ?", roomID, userID).Delete(&models.RoomMember{}).Error
+}
+
+// GetRoomMember retrieves a room member
+func GetRoomMember(roomID, userID uint) (*models.RoomMember, error) {
+ var member models.RoomMember
+ if err := DB.Where("room_id = ? AND user_id = ?", roomID, userID).First(&member).Error; err != nil {
+ return nil, err
+ }
+ return &member, nil
+}
+
+// SetMemberOnline updates the online status of a member
+func SetMemberOnline(roomID, userID uint, online bool) error {
+ return DB.Model(&models.RoomMember{}).
+ Where("room_id = ? AND user_id = ?", roomID, userID).
+ Update("online", online).Error
+}
+
+// OnlineUser represents an online user with their info
+type OnlineUser struct {
+ ID uint `json:"id"`
+ Username string `json:"username"`
+ Online bool `json:"online"`
+ IsAdmin bool `json:"isAdmin"`
+}
+
+// GetOnlineUsers retrieves all online users in a room
+func GetOnlineUsers(roomID uint) ([]OnlineUser, error) {
+ var members []models.RoomMember
+ err := DB.Where("room_id = ? AND online = ?", roomID, true).
+ Preload("User").
+ Find(&members).Error
+
+ if err != nil {
+ return nil, err
+ }
+
+ users := make([]OnlineUser, 0, len(members))
+ for _, member := range members {
+ if member.User != nil {
+ users = append(users, OnlineUser{
+ ID: member.UserID,
+ Username: member.User.Username,
+ Online: member.Online,
+ IsAdmin: member.IsAdmin,
+ })
+ }
+ }
+
+ return users, nil
+}
diff --git a/server/internal/database/room_play_status.go b/server/internal/database/room_play_status.go
new file mode 100644
index 0000000..fbcd17f
--- /dev/null
+++ b/server/internal/database/room_play_status.go
@@ -0,0 +1,69 @@
+package database
+
+import (
+ "sync-player-server/internal/models"
+ "time"
+
+ "gorm.io/gorm"
+)
+
+// CreateRoomPlayStatus creates a new room play status
+func CreateRoomPlayStatus(roomID uint, paused bool, playTime float64, timestamp int64, videoID uint, tx ...*gorm.DB) error {
+ db := getDB(tx...)
+
+ status := &models.RoomPlayStatus{
+ RoomID: roomID,
+ Paused: paused,
+ Time: playTime,
+ Timestamp: timestamp,
+ VideoID: videoID,
+ }
+
+ return db.Create(status).Error
+}
+
+// GetRoomPlayStatus retrieves the play status of a room
+func GetRoomPlayStatus(roomID uint) (*models.RoomPlayStatus, error) {
+ var status models.RoomPlayStatus
+ if err := DB.Where("room_id = ?", roomID).First(&status).Error; err != nil {
+ return nil, err
+ }
+ return &status, nil
+}
+
+// UpdateRoomPlayStatus updates the play status of a room
+func UpdateRoomPlayStatus(roomID uint, data map[string]interface{}, tx ...*gorm.DB) error {
+ db := getDB(tx...)
+
+ status, err := GetRoomPlayStatus(roomID)
+ if err != nil {
+ paused := true
+ playTime := 0.0
+ timestamp := time.Now().UnixMilli()
+ videoID := uint(0)
+
+ if v, ok := data["paused"].(bool); ok {
+ paused = v
+ }
+ if v, ok := data["time"].(float64); ok {
+ playTime = v
+ }
+ if v, ok := data["timestamp"].(int64); ok {
+ timestamp = v
+ }
+ if v, ok := data["videoId"].(uint); ok {
+ videoID = v
+ }
+
+ return CreateRoomPlayStatus(roomID, paused, playTime, timestamp, videoID, db)
+ }
+
+ return db.Model(status).Updates(data).Error
+}
+
+// DeleteRoomPlayStatus deletes the play status of a room
+func DeleteRoomPlayStatus(roomID uint, tx ...*gorm.DB) error {
+ db := getDB(tx...)
+
+ return db.Where("room_id = ?", roomID).Delete(&models.RoomPlayStatus{}).Error
+}
diff --git a/server/internal/database/user.go b/server/internal/database/user.go
new file mode 100644
index 0000000..9cd5b07
--- /dev/null
+++ b/server/internal/database/user.go
@@ -0,0 +1,49 @@
+package database
+
+import (
+ "sync-player-server/internal/models"
+
+ "gorm.io/gorm"
+)
+
+// CreateUser creates a new user
+func CreateUser(username string, password *string, tx ...*gorm.DB) (*models.User, error) {
+ db := getDB(tx...)
+
+ user := &models.User{
+ Username: username,
+ PasswordHash: password,
+ }
+
+ if err := db.Create(user).Error; err != nil {
+ return nil, err
+ }
+
+ return user, nil
+}
+
+// GetUserByUsername retrieves a user by username
+func GetUserByUsername(username string) (*models.User, error) {
+ var user models.User
+ if err := DB.Where("username = ?", username).First(&user).Error; err != nil {
+ return nil, err
+ }
+ return &user, nil
+}
+
+// GetUserByID retrieves a user by ID
+func GetUserByID(id uint) (*models.User, error) {
+ var user models.User
+ if err := DB.First(&user, id).Error; err != nil {
+ return nil, err
+ }
+ return &user, nil
+}
+
+// getDB returns the database instance to use (transaction or default)
+func getDB(tx ...*gorm.DB) *gorm.DB {
+ if len(tx) > 0 && tx[0] != nil {
+ return tx[0]
+ }
+ return DB
+}
diff --git a/server/internal/handlers/playlist.go b/server/internal/handlers/playlist.go
new file mode 100644
index 0000000..129be73
--- /dev/null
+++ b/server/internal/handlers/playlist.go
@@ -0,0 +1,250 @@
+package handlers
+
+import (
+ "fmt"
+ "net/http"
+ "sync-player-server/internal/config"
+ "sync-player-server/internal/database"
+ "sync-player-server/internal/middleware"
+ "sync-player-server/internal/models"
+ "sync-player-server/internal/sync"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+// PlaylistAdd adds an item to the playlist
+func PlaylistAdd(c *gin.Context) {
+ var req struct {
+ Title string `json:"title" binding:"required"`
+ Sources []database.VideoSourceInput `json:"sources" binding:"required,min=1,dive"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
+ return
+ }
+
+ userInfo, ok := middleware.GetUserInfo(c)
+ if !ok {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
+ return
+ }
+
+ playlistItemID, err := database.AddItemToPlaylist(userInfo.RoomID, req.Title, req.Sources)
+ if err != nil {
+ config.Logger.Errorf("Failed to add playlist item: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
+ return
+ }
+
+ syncManager := sync.GetSyncManager()
+ if syncManager != nil {
+ syncManager.Broadcast(userInfo.RoomID, sync.SyncMessage{
+ Type: "updatePlaylist",
+ }, []uint{userInfo.UserID})
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "Item added to playlist",
+ "playlistItemId": playlistItemID,
+ })
+}
+
+// PlaylistQuery queries playlist items
+func PlaylistQuery(c *gin.Context) {
+ userInfo, ok := middleware.GetUserInfo(c)
+ if !ok {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
+ return
+ }
+
+ var playlistItemID *uint
+ var playStatus *models.PlayStatus
+
+ if idStr := c.Query("playlistItemId"); idStr != "" {
+ var id uint
+ if _, err := fmt.Sscanf(idStr, "%d", &id); err == nil {
+ playlistItemID = &id
+ }
+ }
+
+ if statusStr := c.Query("playStatus"); statusStr != "" {
+ status := models.PlayStatus(statusStr)
+ playStatus = &status
+ }
+
+ items, err := database.QueryPlaylistItems(userInfo.RoomID, playlistItemID, playStatus)
+ if err != nil {
+ config.Logger.Errorf("Failed to query playlist items: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
+ return
+ }
+
+ // If no playStatus filter is specified, filter out finished items on the server side
+ if playStatus == nil {
+ filteredItems := make([]models.PlaylistItem, 0)
+ for _, item := range items {
+ if item.PlayStatus != models.PlayStatusFinished {
+ filteredItems = append(filteredItems, item)
+ }
+ }
+ c.JSON(http.StatusOK, filteredItems)
+ return
+ }
+
+ c.JSON(http.StatusOK, items)
+}
+
+// PlaylistDelete deletes a playlist item
+func PlaylistDelete(c *gin.Context) {
+ var req struct {
+ PlaylistItemID uint `json:"playlistItemId" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
+ return
+ }
+
+ userInfo, ok := middleware.GetUserInfo(c)
+ if !ok {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
+ return
+ }
+
+ if err := database.DeletePlaylistItem(req.PlaylistItemID); err != nil {
+ config.Logger.Errorf("Failed to delete playlist item: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"message": "Internal server error"})
+ return
+ }
+
+ syncManager := sync.GetSyncManager()
+ if syncManager != nil {
+ syncManager.Broadcast(userInfo.RoomID, sync.SyncMessage{
+ Type: "updatePlaylist",
+ }, []uint{userInfo.UserID})
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Item deleted from playlist"})
+}
+
+// PlaylistClear clears all playlist items
+func PlaylistClear(c *gin.Context) {
+ userInfo, ok := middleware.GetUserInfo(c)
+ if !ok {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
+ return
+ }
+
+ if err := database.ClearPlaylist(userInfo.RoomID); err != nil {
+ config.Logger.Errorf("Failed to clear playlist: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"message": "Internal server error"})
+ return
+ }
+
+ syncManager := sync.GetSyncManager()
+ if syncManager != nil {
+ syncManager.Broadcast(userInfo.RoomID, sync.SyncMessage{
+ Type: "updatePlaylist",
+ }, []uint{userInfo.UserID})
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Playlist cleared"})
+}
+
+// PlaylistUpdateOrder updates playlist order
+func PlaylistUpdateOrder(c *gin.Context) {
+ var req struct {
+ OrderIndexList []database.OrderIndexUpdate `json:"orderIndexList" binding:"required,min=1,dive"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
+ return
+ }
+
+ userInfo, ok := middleware.GetUserInfo(c)
+ if !ok {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
+ return
+ }
+
+ if err := database.UpdatePlaylistOrderBatch(req.OrderIndexList); err != nil {
+ config.Logger.Errorf("Failed to update playlist order: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
+ return
+ }
+
+ syncManager := sync.GetSyncManager()
+ if syncManager != nil {
+ syncManager.Broadcast(userInfo.RoomID, sync.SyncMessage{
+ Type: "updatePlaylist",
+ }, []uint{userInfo.UserID})
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Order updated"})
+}
+
+// PlaylistSwitch switches to a playlist item
+func PlaylistSwitch(c *gin.Context) {
+ var req struct {
+ PlaylistItemID uint `json:"playlistItemId" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
+ return
+ }
+
+ userInfo, ok := middleware.GetUserInfo(c)
+ if !ok {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
+ return
+ }
+
+ // Mark all currently playing items as finished
+ playingStatus := models.PlayStatusPlaying
+ playingItems, err := database.QueryPlaylistItems(userInfo.RoomID, nil, &playingStatus)
+ if err != nil {
+ config.Logger.Errorf("Failed to query playing items: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
+ return
+ }
+
+ for _, item := range playingItems {
+ if err := database.UpdatePlayStatus(item.ID, models.PlayStatusFinished); err != nil {
+ config.Logger.Errorf("Failed to update play status: %v", err)
+ }
+ }
+
+ // Set the requested item to playing
+ if err := database.UpdatePlayStatus(req.PlaylistItemID, models.PlayStatusPlaying); err != nil {
+ config.Logger.Errorf("Failed to update play status: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
+ return
+ }
+
+ // Update or create room play status
+ _, err = database.GetRoomPlayStatus(userInfo.RoomID)
+ if err != nil {
+ database.CreateRoomPlayStatus(userInfo.RoomID, false, 0, time.Now().UnixMilli(), req.PlaylistItemID)
+ } else {
+ database.UpdateRoomPlayStatus(userInfo.RoomID, map[string]any{
+ "paused": false,
+ "time": 0.0,
+ "timestamp": time.Now().UnixMilli(),
+ "video_id": req.PlaylistItemID,
+ })
+ }
+
+ // Always broadcast playlist update to sync all clients
+ syncManager := sync.GetSyncManager()
+ if syncManager != nil {
+ syncManager.Broadcast(userInfo.RoomID, sync.SyncMessage{
+ Type: "updatePlaylist",
+ }, []uint{userInfo.UserID})
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Playlist item switched"})
+}
diff --git a/server/internal/handlers/room.go b/server/internal/handlers/room.go
new file mode 100644
index 0000000..922fb63
--- /dev/null
+++ b/server/internal/handlers/room.go
@@ -0,0 +1,198 @@
+package handlers
+
+import (
+ "fmt"
+ "net/http"
+ "sync-player-server/internal/config"
+ "sync-player-server/internal/database"
+ "sync-player-server/internal/middleware"
+ "sync-player-server/internal/utils"
+
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+// RoomCreate handles room creation
+func RoomCreate(c *gin.Context) {
+ var req struct {
+ Name string `json:"name" binding:"required"`
+ Password string `json:"password"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
+ return
+ }
+
+ existingRoom, err := database.GetRoomByName(req.Name)
+ if err == nil && existingRoom != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "id": existingRoom.ID,
+ "name": existingRoom.Name,
+ "createdTime": existingRoom.CreatedTime,
+ })
+ return
+ }
+
+ var password *string
+ if req.Password != "" {
+ password = &req.Password
+ }
+
+ room, err := database.CreateRoom(req.Name, password)
+ if err != nil {
+ config.Logger.Errorf("Failed to create room: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "id": room.ID,
+ "name": room.Name,
+ "createdTime": room.CreatedTime,
+ })
+}
+
+// RoomQuery handles room query by name
+func RoomQuery(c *gin.Context) {
+ name := c.Query("name")
+ if name == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Room name is required"})
+ return
+ }
+
+ room, err := database.GetRoomByName(name)
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
+ return
+ }
+ config.Logger.Errorf("Failed to query room: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
+ return
+ }
+
+ c.JSON(http.StatusOK, room)
+}
+
+// RoomJoin handles user joining a room
+func RoomJoin(c *gin.Context) {
+ var req struct {
+ RoomID uint `json:"roomId" binding:"required"`
+ UserID uint `json:"userId" binding:"required"`
+ Password string `json:"password"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
+ return
+ }
+
+ err := database.DB.Transaction(func(tx *gorm.DB) error {
+ user, err := database.GetUserByID(req.UserID)
+ if err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
+ return err
+ }
+
+ room, err := database.GetRoomByID(req.RoomID)
+ if err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
+ return err
+ }
+
+ if !database.VerifyRoomPassword(room, req.Password) {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid password"})
+ return gorm.ErrInvalidData
+ }
+
+ existingMember, _ := database.GetRoomMember(req.RoomID, req.UserID)
+ if existingMember != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "User is already in the room"})
+ return gorm.ErrDuplicatedKey
+ }
+
+ newMember, err := database.AddMemberToRoom(req.RoomID, req.UserID, false, false, tx)
+ if err != nil {
+ config.Logger.Errorf("Failed to add member to room: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
+ return err
+ }
+
+ // Generate JWT token
+ token, err := utils.GenerateJWT(user.ID, user.Username, req.RoomID, newMember.IsAdmin)
+ if err != nil {
+ config.Logger.Errorf("Failed to generate JWT token: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate authentication token"})
+ return err
+ }
+
+ // Also set cookie for backward compatibility
+ middleware.SetUserInfoCookie(c, req.RoomID, user.ID)
+
+ c.JSON(http.StatusOK, gin.H{
+ "token": token,
+ "roomId": newMember.RoomID,
+ "userId": newMember.UserID,
+ "isAdmin": newMember.IsAdmin,
+ "canGrantAdmin": newMember.CanGrantAdmin,
+ })
+
+ return nil
+ })
+
+ if err != nil {
+ return
+ }
+}
+
+// RoomLeave handles user leaving a room
+func RoomLeave(c *gin.Context) {
+ var req struct {
+ RoomID uint `json:"roomId" binding:"required"`
+ UserID uint `json:"userId" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
+ return
+ }
+
+ _, err := database.GetRoomMember(req.RoomID, req.UserID)
+ if err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "User is not in the room"})
+ return
+ }
+
+ if err := database.RemoveMemberFromRoom(req.RoomID, req.UserID); err != nil {
+ config.Logger.Errorf("Failed to remove member from room: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Successfully left the room"})
+}
+
+// RoomQueryOnlineUsers handles querying online users in a room
+func RoomQueryOnlineUsers(c *gin.Context) {
+ roomIDStr := c.Query("roomId")
+ if roomIDStr == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Room ID is required"})
+ return
+ }
+
+ var roomID uint
+ if _, err := fmt.Sscanf(roomIDStr, "%d", &roomID); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid room ID"})
+ return
+ }
+
+ users, err := database.GetOnlineUsers(roomID)
+ if err != nil {
+ config.Logger.Errorf("Failed to query online users: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
+ return
+ }
+
+ c.JSON(http.StatusOK, users)
+}
diff --git a/server/internal/handlers/sync_handler.go b/server/internal/handlers/sync_handler.go
new file mode 100644
index 0000000..a0848b6
--- /dev/null
+++ b/server/internal/handlers/sync_handler.go
@@ -0,0 +1,142 @@
+package handlers
+
+import (
+ "net/http"
+ "sync-player-server/internal/config"
+ "sync-player-server/internal/database"
+ "sync-player-server/internal/middleware"
+ "sync-player-server/internal/sync"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+// SyncUpdateTime updates the playback time
+func SyncUpdateTime(c *gin.Context) {
+ var req struct {
+ Time float64 `json:"time" binding:"required"`
+ Timestamp int64 `json:"timestamp" binding:"required"`
+ VideoID uint `json:"videoId" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
+ return
+ }
+
+ userInfo, ok := middleware.GetUserInfo(c)
+ if !ok {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
+ return
+ }
+
+ config.Logger.Infof("sync updateTime: roomId=%d, userId=%d, time=%f, timestamp=%d, videoId=%d",
+ userInfo.RoomID, userInfo.UserID, req.Time, req.Timestamp, req.VideoID)
+
+ _, err := database.GetRoomPlayStatus(userInfo.RoomID)
+ if err != nil {
+ database.CreateRoomPlayStatus(userInfo.RoomID, false, req.Time, req.Timestamp, req.VideoID)
+ } else {
+ database.UpdateRoomPlayStatus(userInfo.RoomID, map[string]interface{}{
+ "paused": false,
+ "time": req.Time,
+ "timestamp": req.Timestamp,
+ "video_id": req.VideoID,
+ })
+ }
+
+ syncManager := sync.GetSyncManager()
+ if syncManager != nil {
+ syncManager.Broadcast(userInfo.RoomID, sync.SyncMessage{
+ Type: "updateTime",
+ Payload: map[string]interface{}{
+ "roomId": userInfo.RoomID,
+ "userId": userInfo.UserID,
+ "paused": false,
+ "time": req.Time,
+ "timestamp": req.Timestamp,
+ "videoId": req.VideoID,
+ },
+ }, []uint{userInfo.UserID})
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Play status updated"})
+}
+
+// SyncQuery queries the playback status
+func SyncQuery(c *gin.Context) {
+ userInfo, ok := middleware.GetUserInfo(c)
+ if !ok {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
+ return
+ }
+
+ config.Logger.Infof("sync query: roomId=%d", userInfo.RoomID)
+
+ playStatus, err := database.GetRoomPlayStatus(userInfo.RoomID)
+ if err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "Play status not found"})
+ return
+ }
+
+ now := time.Now().UnixMilli()
+ timeDiff := now - playStatus.Timestamp
+ if !playStatus.Paused {
+ playStatus.Time += float64(timeDiff) / 1000.0
+ playStatus.Timestamp = now
+ }
+
+ c.JSON(http.StatusOK, playStatus)
+}
+
+// SyncUpdatePause updates the pause status
+func SyncUpdatePause(c *gin.Context) {
+ var req struct {
+ Paused bool `json:"paused"`
+ Timestamp int64 `json:"timestamp" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
+ return
+ }
+
+ userInfo, ok := middleware.GetUserInfo(c)
+ if !ok {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
+ return
+ }
+
+ config.Logger.Infof("sync updatePause: roomId=%d, userId=%d, paused=%t, timestamp=%d",
+ userInfo.RoomID, userInfo.UserID, req.Paused, req.Timestamp)
+
+ _, err := database.GetRoomPlayStatus(userInfo.RoomID)
+ if err != nil {
+ database.CreateRoomPlayStatus(userInfo.RoomID, req.Paused, 0, req.Timestamp, 0)
+ } else {
+ database.UpdateRoomPlayStatus(userInfo.RoomID, map[string]interface{}{
+ "paused": req.Paused,
+ "timestamp": req.Timestamp,
+ })
+ }
+
+ syncManager := sync.GetSyncManager()
+ if syncManager != nil {
+ syncManager.Broadcast(userInfo.RoomID, sync.SyncMessage{
+ Type: "updatePause",
+ Payload: map[string]interface{}{
+ "roomId": userInfo.RoomID,
+ "userId": userInfo.UserID,
+ "paused": req.Paused,
+ "timestamp": req.Timestamp,
+ },
+ }, []uint{userInfo.UserID})
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Play status updated"})
+}
+
+// SyncProtocol returns the sync protocol
+func SyncProtocol(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{"protocol": config.Env.SyncProtocol})
+}
diff --git a/server/internal/handlers/user.go b/server/internal/handlers/user.go
new file mode 100644
index 0000000..18bf085
--- /dev/null
+++ b/server/internal/handlers/user.go
@@ -0,0 +1,73 @@
+package handlers
+
+import (
+ "net/http"
+ "sync-player-server/internal/config"
+ "sync-player-server/internal/database"
+
+ "github.com/gin-gonic/gin"
+ "gorm.io/gorm"
+)
+
+// UserLogin handles user login/registration
+func UserLogin(c *gin.Context) {
+ var req struct {
+ Username string `json:"username" binding:"required"`
+ Password string `json:"password"`
+ }
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
+ return
+ }
+
+ existingUser, err := database.GetUserByUsername(req.Username)
+ if err == nil && existingUser != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "id": existingUser.ID,
+ "username": existingUser.Username,
+ "createdTime": existingUser.CreatedTime,
+ })
+ return
+ }
+
+ var password *string
+ if req.Password != "" {
+ password = &req.Password
+ }
+
+ user, err := database.CreateUser(req.Username, password)
+ if err != nil {
+ config.Logger.Errorf("Failed to create user: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "id": user.ID,
+ "username": user.Username,
+ "createdTime": user.CreatedTime,
+ })
+}
+
+// UserQuery handles user query by username
+func UserQuery(c *gin.Context) {
+ username := c.Query("username")
+ if username == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Username is required"})
+ return
+ }
+
+ user, err := database.GetUserByUsername(username)
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
+ return
+ }
+ config.Logger.Errorf("Failed to query user: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
+ return
+ }
+
+ c.JSON(http.StatusOK, user)
+}
diff --git a/server/internal/middleware/auth.go b/server/internal/middleware/auth.go
new file mode 100644
index 0000000..cf1aced
--- /dev/null
+++ b/server/internal/middleware/auth.go
@@ -0,0 +1,94 @@
+package middleware
+
+import (
+ "net/http"
+ "strings"
+ "sync-player-server/internal/utils"
+
+ "github.com/gin-gonic/gin"
+)
+
+// JWTAuth middleware validates JWT token from Authorization header
+func JWTAuth() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ // Get Authorization header
+ authHeader := c.GetHeader("Authorization")
+ if authHeader == "" {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
+ c.Abort()
+ return
+ }
+
+ // Check if it's a Bearer token
+ parts := strings.SplitN(authHeader, " ", 2)
+ if len(parts) != 2 || parts[0] != "Bearer" {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format. Expected: Bearer "})
+ c.Abort()
+ return
+ }
+
+ tokenString := parts[1]
+
+ // Validate token
+ claims, err := utils.ValidateJWT(tokenString)
+ if err != nil {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
+ c.Abort()
+ return
+ }
+
+ // Set claims in context for use in handlers
+ c.Set("userInfo", UserInfo{
+ RoomID: claims.RoomID,
+ UserID: claims.UserID,
+ })
+ c.Set("userId", claims.UserID)
+ c.Set("roomId", claims.RoomID)
+ c.Set("username", claims.Username)
+ c.Set("role", claims.Role)
+ c.Set("jwtClaims", claims)
+
+ c.Next()
+ }
+}
+
+// OptionalJWTAuth middleware attempts to validate JWT token but doesn't require it
+func OptionalJWTAuth() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ // Get Authorization header
+ authHeader := c.GetHeader("Authorization")
+ if authHeader == "" {
+ c.Next()
+ return
+ }
+
+ // Check if it's a Bearer token
+ parts := strings.SplitN(authHeader, " ", 2)
+ if len(parts) != 2 || parts[0] != "Bearer" {
+ c.Next()
+ return
+ }
+
+ tokenString := parts[1]
+
+ // Validate token
+ claims, err := utils.ValidateJWT(tokenString)
+ if err != nil {
+ c.Next()
+ return
+ }
+
+ // Set claims in context for use in handlers
+ c.Set("userInfo", UserInfo{
+ RoomID: claims.RoomID,
+ UserID: claims.UserID,
+ })
+ c.Set("userId", claims.UserID)
+ c.Set("roomId", claims.RoomID)
+ c.Set("username", claims.Username)
+ c.Set("role", claims.Role)
+ c.Set("jwtClaims", claims)
+
+ c.Next()
+ }
+}
diff --git a/server/internal/middleware/cookie.go b/server/internal/middleware/cookie.go
new file mode 100644
index 0000000..7260256
--- /dev/null
+++ b/server/internal/middleware/cookie.go
@@ -0,0 +1,75 @@
+package middleware
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+)
+
+// UserInfo represents the user information stored in cookie
+type UserInfo struct {
+ RoomID uint `json:"roomId"`
+ UserID uint `json:"userId"`
+}
+
+// ParseUserInfo middleware parses userInfo from cookie and sets it in context
+func ParseUserInfo() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ cookie, err := c.Cookie("userInfo")
+ if err != nil {
+ c.Next()
+ return
+ }
+
+ var userInfo UserInfo
+ if err := json.Unmarshal([]byte(cookie), &userInfo); err != nil {
+ c.Next()
+ return
+ }
+
+ c.Set("userInfo", userInfo)
+ c.Set("roomId", userInfo.RoomID)
+ c.Set("userId", userInfo.UserID)
+ c.Next()
+ }
+}
+
+// RequireAuth middleware requires user to be authenticated
+func RequireAuth() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ _, exists := c.Get("userInfo")
+ if !exists {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
+ c.Abort()
+ return
+ }
+ c.Next()
+ }
+}
+
+// SetUserInfoCookie sets the userInfo cookie
+func SetUserInfoCookie(c *gin.Context, roomID, userID uint) {
+ userInfo := UserInfo{
+ RoomID: roomID,
+ UserID: userID,
+ }
+
+ jsonData, _ := json.Marshal(userInfo)
+ c.SetCookie("userInfo", string(jsonData), 3600*24*7, "/", "", false, false)
+}
+
+// GetUserInfo retrieves user info from context
+func GetUserInfo(c *gin.Context) (*UserInfo, bool) {
+ value, exists := c.Get("userInfo")
+ if !exists {
+ return nil, false
+ }
+
+ userInfo, ok := value.(UserInfo)
+ if !ok {
+ return nil, false
+ }
+
+ return &userInfo, true
+}
diff --git a/server/internal/models/playlist_item.go b/server/internal/models/playlist_item.go
new file mode 100644
index 0000000..4a02de4
--- /dev/null
+++ b/server/internal/models/playlist_item.go
@@ -0,0 +1,36 @@
+package models
+
+import (
+ "time"
+
+ "gorm.io/gorm"
+)
+
+// PlayStatus represents the status of a playlist item
+type PlayStatus string
+
+const (
+ PlayStatusNew PlayStatus = "new"
+ PlayStatusPlaying PlayStatus = "playing"
+ PlayStatusFinished PlayStatus = "finished"
+)
+
+// PlaylistItem represents an item in the playlist
+type PlaylistItem struct {
+ ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
+ RoomID uint `gorm:"not null;index" json:"roomId"`
+ Title string `gorm:"type:varchar(255);not null" json:"title"`
+ OrderIndex int `gorm:"not null" json:"orderIndex"`
+ PlayStatus PlayStatus `gorm:"type:varchar(20);not null;default:'new'" json:"playStatus"`
+ CreatedTime time.Time `gorm:"not null;autoCreateTime:milli" json:"createdTime"`
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+
+ // Associations
+ Room *Room `gorm:"foreignKey:RoomID" json:"room,omitempty"`
+ VideoSources []VideoSource `gorm:"foreignKey:PlaylistItemID" json:"videoSources,omitempty"`
+}
+
+// TableName specifies the table name for PlaylistItem model
+func (PlaylistItem) TableName() string {
+ return "playlist_items"
+}
diff --git a/server/internal/models/room.go b/server/internal/models/room.go
new file mode 100644
index 0000000..f2f2ab6
--- /dev/null
+++ b/server/internal/models/room.go
@@ -0,0 +1,22 @@
+package models
+
+import (
+ "time"
+
+ "gorm.io/gorm"
+)
+
+// Room represents a sync room
+type Room struct {
+ ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
+ Name string `gorm:"type:varchar(100);not null" json:"name"`
+ PasswordHash *string `gorm:"type:varchar(255)" json:"passwordHash,omitempty"`
+ CreatedTime time.Time `gorm:"not null;autoCreateTime:milli" json:"createdTime"`
+ LastActiveTime time.Time `gorm:"not null;autoUpdateTime:milli" json:"lastActiveTime"`
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+}
+
+// TableName specifies the table name for Room model
+func (Room) TableName() string {
+ return "rooms"
+}
diff --git a/server/internal/models/room_member.go b/server/internal/models/room_member.go
new file mode 100644
index 0000000..bdfdb7f
--- /dev/null
+++ b/server/internal/models/room_member.go
@@ -0,0 +1,25 @@
+package models
+
+import (
+ "gorm.io/gorm"
+)
+
+// RoomMember represents the relationship between users and rooms
+type RoomMember struct {
+ ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
+ RoomID uint `gorm:"not null;index" json:"roomId"`
+ UserID uint `gorm:"not null;index" json:"userId"`
+ IsAdmin bool `gorm:"default:false" json:"isAdmin"`
+ CanGrantAdmin bool `gorm:"default:false" json:"canGrantAdmin"`
+ Online bool `gorm:"default:false" json:"online"`
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+
+ // Associations
+ User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
+ Room *Room `gorm:"foreignKey:RoomID" json:"room,omitempty"`
+}
+
+// TableName specifies the table name for RoomMember model
+func (RoomMember) TableName() string {
+ return "room_members"
+}
diff --git a/server/internal/models/room_play_status.go b/server/internal/models/room_play_status.go
new file mode 100644
index 0000000..fca566e
--- /dev/null
+++ b/server/internal/models/room_play_status.go
@@ -0,0 +1,24 @@
+package models
+
+import (
+ "gorm.io/gorm"
+)
+
+// RoomPlayStatus represents the current playback status of a room
+type RoomPlayStatus struct {
+ ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
+ RoomID uint `gorm:"not null;uniqueIndex" json:"roomId"`
+ Paused bool `gorm:"default:false" json:"paused"`
+ Time float64 `gorm:"default:0" json:"time"`
+ Timestamp int64 `gorm:"default:0" json:"timestamp"`
+ VideoID uint `gorm:"default:0" json:"videoId"`
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+
+ // Associations
+ Room *Room `gorm:"foreignKey:RoomID" json:"room,omitempty"`
+}
+
+// TableName specifies the table name for RoomPlayStatus model
+func (RoomPlayStatus) TableName() string {
+ return "room_play_status"
+}
diff --git a/server/internal/models/user.go b/server/internal/models/user.go
new file mode 100644
index 0000000..25edfd6
--- /dev/null
+++ b/server/internal/models/user.go
@@ -0,0 +1,22 @@
+package models
+
+import (
+ "time"
+
+ "gorm.io/gorm"
+)
+
+// User represents a user in the system
+type User struct {
+ ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
+ Username string `gorm:"type:varchar(50);uniqueIndex;not null" json:"username"`
+ PasswordHash *string `gorm:"type:varchar(255)" json:"passwordHash,omitempty"`
+ CreatedTime time.Time `gorm:"not null;autoCreateTime:milli" json:"createdTime"`
+ LastActiveTime time.Time `gorm:"not null;autoUpdateTime:milli" json:"lastActiveTime"`
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+}
+
+// TableName specifies the table name for User model
+func (User) TableName() string {
+ return "users"
+}
diff --git a/server/internal/models/video_source.go b/server/internal/models/video_source.go
new file mode 100644
index 0000000..c63ba9f
--- /dev/null
+++ b/server/internal/models/video_source.go
@@ -0,0 +1,26 @@
+package models
+
+import (
+ "time"
+
+ "gorm.io/gorm"
+)
+
+// VideoSource represents a video source URL for a playlist item
+type VideoSource struct {
+ ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
+ PlaylistItemID uint `gorm:"not null;index" json:"playlistItemId"`
+ URL string `gorm:"type:varchar(255);not null" json:"url"`
+ Label string `gorm:"type:varchar(100)" json:"label"`
+ CreatedTime time.Time `gorm:"not null;autoCreateTime:milli" json:"createdTime"`
+ LastActiveTime time.Time `gorm:"not null;autoUpdateTime:milli" json:"lastActiveTime"`
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+
+ // Associations
+ PlaylistItem *PlaylistItem `gorm:"foreignKey:PlaylistItemID" json:"playlistItem,omitempty"`
+}
+
+// TableName specifies the table name for VideoSource model
+func (VideoSource) TableName() string {
+ return "video_sources"
+}
diff --git a/server/internal/routes/routes.go b/server/internal/routes/routes.go
new file mode 100644
index 0000000..fca581e
--- /dev/null
+++ b/server/internal/routes/routes.go
@@ -0,0 +1,68 @@
+package routes
+
+import (
+ "sync-player-server/internal/handlers"
+ "sync-player-server/internal/middleware"
+ "sync-player-server/internal/sync/adapters"
+
+ "github.com/gin-gonic/gin"
+)
+
+// SetupRoutes configures all routes
+func SetupRoutes(r *gin.Engine, wsAdapter *adapters.WebSocketAdapter, sseAdapter *adapters.SSEAdapter) {
+ // Try JWT first, then fall back to cookie for backward compatibility
+ r.Use(middleware.OptionalJWTAuth())
+ r.Use(middleware.ParseUserInfo())
+
+ r.GET("/health", func(c *gin.Context) {
+ c.JSON(200, gin.H{"status": "ok!"})
+ })
+
+ // API routes group
+ apiGroup := r.Group("/api")
+ {
+ userGroup := apiGroup.Group("/user")
+ {
+ userGroup.POST("/login", handlers.UserLogin)
+ userGroup.GET("/query", handlers.UserQuery)
+ }
+
+ roomGroup := apiGroup.Group("/room")
+ {
+ roomGroup.POST("/create", handlers.RoomCreate)
+ roomGroup.GET("/query", handlers.RoomQuery)
+ roomGroup.POST("/join", handlers.RoomJoin)
+ roomGroup.POST("/leave", handlers.RoomLeave)
+ roomGroup.GET("/queryOnlineUsers", handlers.RoomQueryOnlineUsers)
+ }
+
+ playlistGroup := apiGroup.Group("/playlist")
+ playlistGroup.Use(middleware.RequireAuth())
+ {
+ playlistGroup.POST("/add", handlers.PlaylistAdd)
+ playlistGroup.GET("/query", handlers.PlaylistQuery)
+ playlistGroup.DELETE("/delete", handlers.PlaylistDelete)
+ playlistGroup.DELETE("/clear", handlers.PlaylistClear)
+ playlistGroup.POST("/updateOrder", handlers.PlaylistUpdateOrder)
+ playlistGroup.POST("/switch", handlers.PlaylistSwitch)
+ }
+
+ syncGroup := apiGroup.Group("/sync")
+ syncGroup.Use(middleware.RequireAuth())
+ {
+ syncGroup.POST("/updateTime", handlers.SyncUpdateTime)
+ syncGroup.GET("/query", handlers.SyncQuery)
+ syncGroup.POST("/updatePause", handlers.SyncUpdatePause)
+ syncGroup.GET("/protocol", handlers.SyncProtocol)
+ }
+ }
+
+ // WebSocket and SSE endpoints remain at root level
+ if wsAdapter != nil {
+ r.GET("/ws", wsAdapter.HandleWebSocket)
+ }
+
+ if sseAdapter != nil {
+ r.GET("/sse/connect", sseAdapter.HandleSSEConnect)
+ }
+}
diff --git a/server/internal/sync/adapters/factory.go b/server/internal/sync/adapters/factory.go
new file mode 100644
index 0000000..b229560
--- /dev/null
+++ b/server/internal/sync/adapters/factory.go
@@ -0,0 +1,29 @@
+package adapters
+
+import (
+ "fmt"
+ "sync-player-server/internal/config"
+ "sync-player-server/internal/sync"
+)
+
+// NewAdapter creates a new sync adapter based on the protocol
+func NewAdapter(protocol string) (sync.ISyncAdapter, error) {
+ config.Logger.Infof("Creating adapter for protocol: %s", protocol)
+
+ switch protocol {
+ case "websocket":
+ adapter := NewWebSocketAdapter()
+ if err := adapter.Start(); err != nil {
+ return nil, fmt.Errorf("failed to start WebSocket adapter: %w", err)
+ }
+ return adapter, nil
+ case "sse":
+ adapter := NewSSEAdapter()
+ if err := adapter.Start(); err != nil {
+ return nil, fmt.Errorf("failed to start SSE adapter: %w", err)
+ }
+ return adapter, nil
+ default:
+ return nil, fmt.Errorf("unsupported sync protocol: %s", protocol)
+ }
+}
diff --git a/server/internal/sync/adapters/sse.go b/server/internal/sync/adapters/sse.go
new file mode 100644
index 0000000..8244680
--- /dev/null
+++ b/server/internal/sync/adapters/sse.go
@@ -0,0 +1,240 @@
+package adapters
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+ "sync"
+ "sync-player-server/internal/config"
+ "sync-player-server/internal/database"
+ "sync-player-server/internal/utils"
+ synctypes "sync-player-server/internal/sync"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+// SSEClient represents an SSE client connection
+type SSEClient struct {
+ UserID uint
+ RoomID uint
+ Writer gin.ResponseWriter
+ Flusher http.Flusher
+ Done chan bool
+}
+
+// SSEAdapter implements ISyncAdapter using Server-Sent Events
+type SSEAdapter struct {
+ connections map[uint]map[uint]*SSEClient
+ mu sync.RWMutex
+}
+
+// NewSSEAdapter creates a new SSE adapter
+func NewSSEAdapter() *SSEAdapter {
+ return &SSEAdapter{
+ connections: make(map[uint]map[uint]*SSEClient),
+ }
+}
+
+// HandleSSEConnect handles SSE connection requests
+func (a *SSEAdapter) HandleSSEConnect(c *gin.Context) {
+ var userID, roomID uint
+
+ // Try JWT authentication first
+ token := c.Query("token")
+ if token == "" {
+ // Try from Authorization header
+ authHeader := c.GetHeader("Authorization")
+ if authHeader != "" {
+ parts := strings.SplitN(authHeader, " ", 2)
+ if len(parts) == 2 && parts[0] == "Bearer" {
+ token = parts[1]
+ }
+ }
+ }
+
+ if token != "" {
+ // Use JWT authentication
+ claims, err := utils.ValidateJWT(token)
+ if err != nil {
+ c.JSON(401, gin.H{"error": "Invalid or expired token"})
+ return
+ }
+ userID = claims.UserID
+ roomID = claims.RoomID
+ } else {
+ // Fallback to userId/roomId query parameters for backward compatibility
+ userIDStr := c.Query("userId")
+ roomIDStr := c.Query("roomId")
+
+ if _, err := fmt.Sscanf(userIDStr, "%d", &userID); err != nil {
+ c.JSON(400, gin.H{"error": "Invalid userId"})
+ return
+ }
+ if _, err := fmt.Sscanf(roomIDStr, "%d", &roomID); err != nil {
+ c.JSON(400, gin.H{"error": "Invalid roomId"})
+ return
+ }
+ }
+
+ // Set SSE headers
+ c.Writer.Header().Set("Content-Type", "text/event-stream")
+ c.Writer.Header().Set("Cache-Control", "no-cache")
+ c.Writer.Header().Set("Connection", "keep-alive")
+ c.Writer.Header().Set("X-Accel-Buffering", "no")
+
+ flusher, ok := c.Writer.(http.Flusher)
+ if !ok {
+ c.JSON(500, gin.H{"error": "Streaming not supported"})
+ return
+ }
+
+ client := &SSEClient{
+ UserID: userID,
+ RoomID: roomID,
+ Writer: c.Writer,
+ Flusher: flusher,
+ Done: make(chan bool),
+ }
+
+ // Register client
+ a.mu.Lock()
+ if a.connections[roomID] == nil {
+ a.connections[roomID] = make(map[uint]*SSEClient)
+ }
+ a.connections[roomID][userID] = client
+ a.mu.Unlock()
+
+ database.SetMemberOnline(roomID, userID, true)
+ config.Logger.Infof("User %d connected to room %d using SSE", userID, roomID)
+
+ // Send connected message
+ a.sendEvent(client, "connected", map[string]interface{}{})
+
+ // Start heartbeat
+ ticker := time.NewTicker(30 * time.Second)
+ defer ticker.Stop()
+
+ // Keep connection alive
+ for {
+ select {
+ case <-c.Request.Context().Done():
+ a.handleDisconnect(roomID, userID)
+ return
+ case <-client.Done:
+ a.handleDisconnect(roomID, userID)
+ return
+ case <-ticker.C:
+ // Send heartbeat
+ fmt.Fprintf(client.Writer, ":\n\n")
+ client.Flusher.Flush()
+ }
+ }
+}
+
+func (a *SSEAdapter) sendEvent(client *SSEClient, eventType string, data interface{}) {
+ message := map[string]interface{}{
+ "type": eventType,
+ "data": data,
+ }
+
+ jsonData, err := json.Marshal(message)
+ if err != nil {
+ config.Logger.Errorf("Failed to marshal message: %v", err)
+ return
+ }
+
+ fmt.Fprintf(client.Writer, "data: %s\n\n", jsonData)
+ client.Flusher.Flush()
+}
+
+func (a *SSEAdapter) handleDisconnect(roomID, userID uint) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ if a.connections[roomID] != nil {
+ delete(a.connections[roomID], userID)
+ }
+
+ database.SetMemberOnline(roomID, userID, false)
+ config.Logger.Infof("User %d disconnected from room %d", userID, roomID)
+}
+
+// Broadcast sends a message to all users in a room except excluded users
+func (a *SSEAdapter) Broadcast(roomID uint, message synctypes.SyncMessage, excludedUserIDs []uint) {
+ a.mu.RLock()
+ defer a.mu.RUnlock()
+
+ roomClients := a.connections[roomID]
+ if roomClients == nil {
+ return
+ }
+
+ excludeMap := make(map[uint]bool)
+ for _, id := range excludedUserIDs {
+ excludeMap[id] = true
+ }
+
+ for userID, client := range roomClients {
+ if !excludeMap[userID] {
+ a.sendEvent(client, message.Type, message)
+ }
+ }
+}
+
+// SendToUsers sends a message to specific users
+func (a *SSEAdapter) SendToUsers(roomID uint, userIDs []uint, message synctypes.SyncMessage) {
+ a.mu.RLock()
+ defer a.mu.RUnlock()
+
+ roomClients := a.connections[roomID]
+ if roomClients == nil {
+ return
+ }
+
+ for _, userID := range userIDs {
+ if client, ok := roomClients[userID]; ok {
+ a.sendEvent(client, message.Type, message)
+ }
+ }
+}
+
+// GetUserIDsInRoom returns all user IDs in a room
+func (a *SSEAdapter) GetUserIDsInRoom(roomID uint) []uint {
+ a.mu.RLock()
+ defer a.mu.RUnlock()
+
+ roomClients := a.connections[roomID]
+ if roomClients == nil {
+ return []uint{}
+ }
+
+ userIDs := make([]uint, 0, len(roomClients))
+ for userID := range roomClients {
+ userIDs = append(userIDs, userID)
+ }
+ return userIDs
+}
+
+// Start starts the adapter
+func (a *SSEAdapter) Start() error {
+ config.Logger.Info("SSE adapter started")
+ return nil
+}
+
+// Stop stops the adapter
+func (a *SSEAdapter) Stop() error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ for roomID, clients := range a.connections {
+ for userID, client := range clients {
+ close(client.Done)
+ database.SetMemberOnline(roomID, userID, false)
+ }
+ }
+ a.connections = make(map[uint]map[uint]*SSEClient)
+ config.Logger.Info("SSE adapter stopped")
+ return nil
+}
diff --git a/server/internal/sync/adapters/websocket.go b/server/internal/sync/adapters/websocket.go
new file mode 100644
index 0000000..c291ec7
--- /dev/null
+++ b/server/internal/sync/adapters/websocket.go
@@ -0,0 +1,201 @@
+package adapters
+
+import (
+ "encoding/json"
+ "net/http"
+ "sync"
+ "sync-player-server/internal/config"
+ "sync-player-server/internal/database"
+ "sync-player-server/internal/utils"
+ synctypes "sync-player-server/internal/sync"
+
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
+)
+
+var upgrader = websocket.Upgrader{
+ CheckOrigin: func(r *http.Request) bool {
+ return true
+ },
+}
+
+// WebSocketAdapter implements ISyncAdapter using WebSocket
+type WebSocketAdapter struct {
+ connections map[uint]map[uint]*websocket.Conn
+ mu sync.RWMutex
+}
+
+// NewWebSocketAdapter creates a new WebSocket adapter
+func NewWebSocketAdapter() *WebSocketAdapter {
+ return &WebSocketAdapter{
+ connections: make(map[uint]map[uint]*websocket.Conn),
+ }
+}
+
+// HandleWebSocket handles WebSocket connections
+func (a *WebSocketAdapter) HandleWebSocket(c *gin.Context) {
+ conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
+ if err != nil {
+ config.Logger.Errorf("Failed to upgrade WebSocket: %v", err)
+ return
+ }
+ defer conn.Close()
+
+ config.Logger.Info("New client connected")
+ conn.WriteJSON(map[string]string{"type": "connected"})
+
+ for {
+ _, message, err := conn.ReadMessage()
+ if err != nil {
+ a.handleClose(conn)
+ break
+ }
+
+ a.handleMessage(conn, message)
+ }
+}
+
+func (a *WebSocketAdapter) handleMessage(conn *websocket.Conn, message []byte) {
+ var data struct {
+ Type string `json:"type"`
+ Payload json.RawMessage `json:"payload"`
+ }
+
+ if err := json.Unmarshal(message, &data); err != nil {
+ config.Logger.Errorf("Error parsing message: %v", err)
+ return
+ }
+
+ if data.Type == "auth" {
+ var payload struct {
+ Token string `json:"token"`
+ UserID uint `json:"userId"`
+ RoomID uint `json:"roomId"`
+ }
+ if err := json.Unmarshal(data.Payload, &payload); err == nil {
+ // If token is provided, use JWT authentication
+ if payload.Token != "" {
+ claims, err := utils.ValidateJWT(payload.Token)
+ if err != nil {
+ config.Logger.Errorf("Invalid JWT token: %v", err)
+ conn.WriteJSON(map[string]string{
+ "type": "error",
+ "error": "Invalid or expired token",
+ })
+ return
+ }
+ a.handleAuth(conn, claims.UserID, claims.RoomID)
+ } else {
+ // Fallback to userId/roomId for backward compatibility
+ a.handleAuth(conn, payload.UserID, payload.RoomID)
+ }
+ }
+ }
+}
+
+func (a *WebSocketAdapter) handleAuth(conn *websocket.Conn, userID, roomID uint) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ if a.connections[roomID] == nil {
+ a.connections[roomID] = make(map[uint]*websocket.Conn)
+ }
+ a.connections[roomID][userID] = conn
+
+ database.SetMemberOnline(roomID, userID, true)
+ config.Logger.Infof("User %d connected to room %d", userID, roomID)
+}
+
+func (a *WebSocketAdapter) handleClose(conn *websocket.Conn) {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ for roomID, users := range a.connections {
+ for userID, c := range users {
+ if c == conn {
+ delete(a.connections[roomID], userID)
+ database.SetMemberOnline(roomID, userID, false)
+ config.Logger.Infof("User %d disconnected from room %d", userID, roomID)
+ return
+ }
+ }
+ }
+}
+
+// Broadcast sends a message to all users in a room except excluded users
+func (a *WebSocketAdapter) Broadcast(roomID uint, message synctypes.SyncMessage, excludedUserIDs []uint) {
+ a.mu.RLock()
+ defer a.mu.RUnlock()
+
+ roomConnections := a.connections[roomID]
+ if roomConnections == nil {
+ return
+ }
+
+ excludeMap := make(map[uint]bool)
+ for _, id := range excludedUserIDs {
+ excludeMap[id] = true
+ }
+
+ for userID, conn := range roomConnections {
+ if !excludeMap[userID] {
+ conn.WriteJSON(message)
+ }
+ }
+}
+
+// SendToUsers sends a message to specific users
+func (a *WebSocketAdapter) SendToUsers(roomID uint, userIDs []uint, message synctypes.SyncMessage) {
+ a.mu.RLock()
+ defer a.mu.RUnlock()
+
+ roomConnections := a.connections[roomID]
+ if roomConnections == nil {
+ return
+ }
+
+ for _, userID := range userIDs {
+ if conn, ok := roomConnections[userID]; ok {
+ conn.WriteJSON(message)
+ }
+ }
+}
+
+// GetUserIDsInRoom returns all user IDs in a room
+func (a *WebSocketAdapter) GetUserIDsInRoom(roomID uint) []uint {
+ a.mu.RLock()
+ defer a.mu.RUnlock()
+
+ roomConnections := a.connections[roomID]
+ if roomConnections == nil {
+ return []uint{}
+ }
+
+ userIDs := make([]uint, 0, len(roomConnections))
+ for userID := range roomConnections {
+ userIDs = append(userIDs, userID)
+ }
+ return userIDs
+}
+
+// Start starts the adapter
+func (a *WebSocketAdapter) Start() error {
+ config.Logger.Info("WebSocket adapter started")
+ return nil
+}
+
+// Stop stops the adapter
+func (a *WebSocketAdapter) Stop() error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ for roomID, users := range a.connections {
+ for userID, conn := range users {
+ conn.Close()
+ database.SetMemberOnline(roomID, userID, false)
+ }
+ }
+ a.connections = make(map[uint]map[uint]*websocket.Conn)
+ config.Logger.Info("WebSocket adapter stopped")
+ return nil
+}
diff --git a/server/internal/sync/manager.go b/server/internal/sync/manager.go
new file mode 100644
index 0000000..8ec04f4
--- /dev/null
+++ b/server/internal/sync/manager.go
@@ -0,0 +1,57 @@
+package sync
+
+// SyncManager implements ISyncManager
+type SyncManager struct {
+ adapter ISyncAdapter
+}
+
+var globalSyncManager *SyncManager
+
+// NewSyncManager creates a new sync manager with the given adapter
+func NewSyncManager(adapter ISyncAdapter) *SyncManager {
+ return &SyncManager{
+ adapter: adapter,
+ }
+}
+
+// Broadcast sends a message to all users in a room except excluded users
+func (m *SyncManager) Broadcast(roomID uint, message SyncMessage, excludedUserIDs []uint) {
+ if m.adapter != nil {
+ m.adapter.Broadcast(roomID, message, excludedUserIDs)
+ }
+}
+
+// SendToUsers sends a message to specific users
+func (m *SyncManager) SendToUsers(roomID uint, userIDs []uint, message SyncMessage) {
+ if m.adapter != nil {
+ m.adapter.SendToUsers(roomID, userIDs, message)
+ }
+}
+
+// GetUserIDsInRoom returns all user IDs in a room
+func (m *SyncManager) GetUserIDsInRoom(roomID uint) []uint {
+ if m.adapter != nil {
+ return m.adapter.GetUserIDsInRoom(roomID)
+ }
+ return []uint{}
+}
+
+// GetAdapter returns the underlying adapter (for registering routes)
+func (m *SyncManager) GetAdapter() ISyncAdapter {
+ return m.adapter
+}
+
+// InitSyncManager initializes the global sync manager with the given adapter
+func InitSyncManager(adapter ISyncAdapter) {
+ globalSyncManager = NewSyncManager(adapter)
+}
+
+// GetSyncManager returns the global sync manager
+func GetSyncManager() *SyncManager {
+ return globalSyncManager
+}
+
+// SetAdapter sets the adapter for the sync manager
+func (m *SyncManager) SetAdapter(adapter ISyncAdapter) {
+ m.adapter = adapter
+}
diff --git a/server/internal/sync/types.go b/server/internal/sync/types.go
new file mode 100644
index 0000000..e0aaabb
--- /dev/null
+++ b/server/internal/sync/types.go
@@ -0,0 +1,37 @@
+package sync
+
+// SyncMessage represents a message to be synced across clients
+type SyncMessage struct {
+ Type string `json:"type"`
+ Payload interface{} `json:"payload,omitempty"`
+}
+
+// ISyncAdapter defines the interface for sync adapters (WebSocket/SSE)
+type ISyncAdapter interface {
+ // Broadcast sends a message to all users in a room except excluded users
+ Broadcast(roomID uint, message SyncMessage, excludedUserIDs []uint)
+
+ // SendToUsers sends a message to specific users in a room
+ SendToUsers(roomID uint, userIDs []uint, message SyncMessage)
+
+ // GetUserIDsInRoom returns all user IDs currently connected in a room
+ GetUserIDsInRoom(roomID uint) []uint
+
+ // Start starts the adapter
+ Start() error
+
+ // Stop stops the adapter
+ Stop() error
+}
+
+// ISyncManager defines the interface for the sync manager
+type ISyncManager interface {
+ // Broadcast sends a message to all users in a room except excluded users
+ Broadcast(roomID uint, message SyncMessage, excludedUserIDs []uint)
+
+ // SendToUsers sends a message to specific users in a room
+ SendToUsers(roomID uint, userIDs []uint, message SyncMessage)
+
+ // GetUserIDsInRoom returns all user IDs currently connected in a room
+ GetUserIDsInRoom(roomID uint) []uint
+}
diff --git a/server/internal/utils/jwt.go b/server/internal/utils/jwt.go
new file mode 100644
index 0000000..e9f742d
--- /dev/null
+++ b/server/internal/utils/jwt.go
@@ -0,0 +1,66 @@
+package utils
+
+import (
+ "fmt"
+ "sync-player-server/internal/config"
+ "time"
+
+ "github.com/golang-jwt/jwt/v5"
+)
+
+// JWTClaims represents the claims in the JWT token
+type JWTClaims struct {
+ UserID uint `json:"sub"`
+ Username string `json:"username"`
+ RoomID uint `json:"room_id"`
+ Role string `json:"role"`
+ jwt.RegisteredClaims
+}
+
+// GenerateJWT generates a new JWT token for a user
+func GenerateJWT(userID uint, username string, roomID uint, isAdmin bool) (string, error) {
+ role := "user"
+ if isAdmin {
+ role = "admin"
+ }
+
+ claims := JWTClaims{
+ UserID: userID,
+ Username: username,
+ RoomID: roomID,
+ Role: role,
+ RegisteredClaims: jwt.RegisteredClaims{
+ IssuedAt: jwt.NewNumericDate(time.Now()),
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(config.Env.JWTExpiryHours))),
+ },
+ }
+
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ tokenString, err := token.SignedString([]byte(config.Env.JWTSecret))
+ if err != nil {
+ return "", fmt.Errorf("failed to sign token: %w", err)
+ }
+
+ return tokenString, nil
+}
+
+// ValidateJWT validates a JWT token and returns the claims
+func ValidateJWT(tokenString string) (*JWTClaims, error) {
+ token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
+ // Verify signing method
+ if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+ return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
+ }
+ return []byte(config.Env.JWTSecret), nil
+ })
+
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse token: %w", err)
+ }
+
+ if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
+ return claims, nil
+ }
+
+ return nil, fmt.Errorf("invalid token")
+}
diff --git a/server/package-lock.json b/server/package-lock.json
deleted file mode 100644
index c50d7bc..0000000
--- a/server/package-lock.json
+++ /dev/null
@@ -1,3870 +0,0 @@
-{
- "name": "server",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "dependencies": {
- "@types/dotenv": "^6.1.1",
- "@types/express": "^5.0.0",
- "cookie-parser": "^1.4.7",
- "cors": "^2.8.5",
- "dotenv": "^16.4.5",
- "express": "^4.21.1",
- "mysql2": "^3.11.4",
- "pg": "^8.13.1",
- "pg-hstore": "^2.3.4",
- "sequelize": "^6.37.5",
- "sqlite3": "^5.1.7",
- "winston": "^3.16.0",
- "ws": "^8.18.0"
- },
- "devDependencies": {
- "@types/cookie-parser": "^1.4.7",
- "@types/cors": "^2.8.17",
- "@types/node": "^22.8.6",
- "@types/sequelize": "^4.28.20",
- "@types/winston": "^2.4.4",
- "@types/ws": "^8.5.12",
- "@vercel/ncc": "^0.38.2",
- "nodemon": "^3.1.7",
- "ts-node": "^10.9.2",
- "typescript": "^5.6.3"
- }
- },
- "node_modules/@colors/colors": {
- "version": "1.6.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@colors/colors/-/colors-1.6.0.tgz",
- "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.1.90"
- }
- },
- "node_modules/@cspotcode/source-map-support": {
- "version": "0.8.1",
- "resolved": "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
- "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/trace-mapping": "0.3.9"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@dabh/diagnostics": {
- "version": "2.0.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
- "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
- "license": "MIT",
- "dependencies": {
- "colorspace": "1.1.x",
- "enabled": "2.0.x",
- "kuler": "^2.0.0"
- }
- },
- "node_modules/@gar/promisify": {
- "version": "1.1.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@gar/promisify/-/promisify-1.1.3.tgz",
- "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.9",
- "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
- "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.0.3",
- "@jridgewell/sourcemap-codec": "^1.4.10"
- }
- },
- "node_modules/@npmcli/fs": {
- "version": "1.1.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@npmcli/fs/-/fs-1.1.1.tgz",
- "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "@gar/promisify": "^1.0.1",
- "semver": "^7.3.5"
- }
- },
- "node_modules/@npmcli/move-file": {
- "version": "1.1.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@npmcli/move-file/-/move-file-1.1.2.tgz",
- "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==",
- "deprecated": "This functionality has been moved to @npmcli/fs",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "mkdirp": "^1.0.4",
- "rimraf": "^3.0.2"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@tootallnate/once": {
- "version": "1.1.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@tootallnate/once/-/once-1.1.2.tgz",
- "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/@tsconfig/node10": {
- "version": "1.0.11",
- "resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.11.tgz",
- "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@tsconfig/node12": {
- "version": "1.0.11",
- "resolved": "https://registry.npmmirror.com/@tsconfig/node12/-/node12-1.0.11.tgz",
- "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@tsconfig/node14": {
- "version": "1.0.3",
- "resolved": "https://registry.npmmirror.com/@tsconfig/node14/-/node14-1.0.3.tgz",
- "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@tsconfig/node16": {
- "version": "1.0.4",
- "resolved": "https://registry.npmmirror.com/@tsconfig/node16/-/node16-1.0.4.tgz",
- "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/bluebird": {
- "version": "3.5.42",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/bluebird/-/bluebird-3.5.42.tgz",
- "integrity": "sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/body-parser": {
- "version": "1.19.5",
- "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.5.tgz",
- "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
- "license": "MIT",
- "dependencies": {
- "@types/connect": "*",
- "@types/node": "*"
- }
- },
- "node_modules/@types/connect": {
- "version": "3.4.38",
- "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz",
- "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/continuation-local-storage": {
- "version": "3.2.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/continuation-local-storage/-/continuation-local-storage-3.2.7.tgz",
- "integrity": "sha512-Q7dPOymVpRG5Zpz90/o26+OAqOG2Sw+FED7uQmTrJNCF/JAPTylclZofMxZKd6W7g1BDPmT9/C/jX0ZcSNTQwQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/cookie-parser": {
- "version": "1.4.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/cookie-parser/-/cookie-parser-1.4.7.tgz",
- "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/express": "*"
- }
- },
- "node_modules/@types/cors": {
- "version": "2.8.17",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/cors/-/cors-2.8.17.tgz",
- "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/debug": {
- "version": "4.1.12",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/debug/-/debug-4.1.12.tgz",
- "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
- "license": "MIT",
- "dependencies": {
- "@types/ms": "*"
- }
- },
- "node_modules/@types/dotenv": {
- "version": "6.1.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/dotenv/-/dotenv-6.1.1.tgz",
- "integrity": "sha512-ftQl3DtBvqHl9L16tpqqzA4YzCSXZfi7g8cQceTz5rOlYtk/IZbFjAv3mLOQlNIgOaylCQWQoBdDQHPgEBJPHg==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@types/express": {
- "version": "5.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/express/-/express-5.0.0.tgz",
- "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==",
- "license": "MIT",
- "dependencies": {
- "@types/body-parser": "*",
- "@types/express-serve-static-core": "^5.0.0",
- "@types/qs": "*",
- "@types/serve-static": "*"
- }
- },
- "node_modules/@types/express-serve-static-core": {
- "version": "5.0.1",
- "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz",
- "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*",
- "@types/qs": "*",
- "@types/range-parser": "*",
- "@types/send": "*"
- }
- },
- "node_modules/@types/http-errors": {
- "version": "2.0.4",
- "resolved": "https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.4.tgz",
- "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
- "license": "MIT"
- },
- "node_modules/@types/lodash": {
- "version": "4.17.13",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/lodash/-/lodash-4.17.13.tgz",
- "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/mime": {
- "version": "1.3.5",
- "resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz",
- "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
- "license": "MIT"
- },
- "node_modules/@types/ms": {
- "version": "0.7.34",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/ms/-/ms-0.7.34.tgz",
- "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "22.8.6",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/node/-/node-22.8.6.tgz",
- "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~6.19.8"
- }
- },
- "node_modules/@types/qs": {
- "version": "6.9.16",
- "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.16.tgz",
- "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
- "license": "MIT"
- },
- "node_modules/@types/range-parser": {
- "version": "1.2.7",
- "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz",
- "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
- "license": "MIT"
- },
- "node_modules/@types/send": {
- "version": "0.17.4",
- "resolved": "https://registry.npmmirror.com/@types/send/-/send-0.17.4.tgz",
- "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
- "license": "MIT",
- "dependencies": {
- "@types/mime": "^1",
- "@types/node": "*"
- }
- },
- "node_modules/@types/sequelize": {
- "version": "4.28.20",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/sequelize/-/sequelize-4.28.20.tgz",
- "integrity": "sha512-XaGOKRhdizC87hDgQ0u3btxzbejlF+t6Hhvkek1HyphqCI4y7zVBIVAGmuc4cWJqGpxusZ1RiBToHHnNK/Edlw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/bluebird": "*",
- "@types/continuation-local-storage": "*",
- "@types/lodash": "*",
- "@types/validator": "*"
- }
- },
- "node_modules/@types/serve-static": {
- "version": "1.15.7",
- "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.7.tgz",
- "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
- "license": "MIT",
- "dependencies": {
- "@types/http-errors": "*",
- "@types/node": "*",
- "@types/send": "*"
- }
- },
- "node_modules/@types/triple-beam": {
- "version": "1.3.5",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/triple-beam/-/triple-beam-1.3.5.tgz",
- "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
- "license": "MIT"
- },
- "node_modules/@types/validator": {
- "version": "13.12.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/validator/-/validator-13.12.2.tgz",
- "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==",
- "license": "MIT"
- },
- "node_modules/@types/winston": {
- "version": "2.4.4",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/winston/-/winston-2.4.4.tgz",
- "integrity": "sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw==",
- "deprecated": "This is a stub types definition. winston provides its own type definitions, so you do not need this installed.",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "winston": "*"
- }
- },
- "node_modules/@types/ws": {
- "version": "8.5.12",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/ws/-/ws-8.5.12.tgz",
- "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/@vercel/ncc": {
- "version": "0.38.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/@vercel/ncc/-/ncc-0.38.2.tgz",
- "integrity": "sha512-3yel3jaxUg9pHBv4+KeC9qlbdZPug+UMtUOlhvpDYCMSgcNSrS2Hv1LoqMsOV7hf2lYscx+BESfJOIla1WsmMQ==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "ncc": "dist/ncc/cli.js"
- }
- },
- "node_modules/abbrev": {
- "version": "1.1.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/abbrev/-/abbrev-1.1.1.tgz",
- "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
- "license": "ISC",
- "optional": true
- },
- "node_modules/abort-controller": {
- "version": "3.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/abort-controller/-/abort-controller-3.0.0.tgz",
- "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
- "license": "MIT",
- "dependencies": {
- "event-target-shim": "^5.0.0"
- },
- "engines": {
- "node": ">=6.5"
- }
- },
- "node_modules/accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
- "license": "MIT",
- "dependencies": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/acorn": {
- "version": "8.14.0",
- "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.0.tgz",
- "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/acorn-walk": {
- "version": "8.3.4",
- "resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.4.tgz",
- "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "acorn": "^8.11.0"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/agent-base": {
- "version": "6.0.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/agent-base/-/agent-base-6.0.2.tgz",
- "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "debug": "4"
- },
- "engines": {
- "node": ">= 6.0.0"
- }
- },
- "node_modules/agent-base/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/agent-base/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/agentkeepalive": {
- "version": "4.5.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
- "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "humanize-ms": "^1.2.1"
- },
- "engines": {
- "node": ">= 8.0.0"
- }
- },
- "node_modules/aggregate-error": {
- "version": "3.1.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/aggregate-error/-/aggregate-error-3.1.0.tgz",
- "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "clean-stack": "^2.0.0",
- "indent-string": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/anymatch": {
- "version": "3.1.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/aproba": {
- "version": "2.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/aproba/-/aproba-2.0.0.tgz",
- "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
- "license": "ISC",
- "optional": true
- },
- "node_modules/are-we-there-yet": {
- "version": "3.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
- "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
- "deprecated": "This package is no longer supported.",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "delegates": "^1.0.0",
- "readable-stream": "^3.6.0"
- },
- "engines": {
- "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
- }
- },
- "node_modules/arg": {
- "version": "4.1.3",
- "resolved": "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz",
- "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/array-flatten": {
- "version": "1.1.1",
- "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
- "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
- "license": "MIT"
- },
- "node_modules/async": {
- "version": "3.2.6",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/async/-/async-3.2.6.tgz",
- "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
- "license": "MIT"
- },
- "node_modules/aws-ssl-profiles": {
- "version": "1.1.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
- "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
- "license": "MIT",
- "engines": {
- "node": ">= 6.0.0"
- }
- },
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/base64-js": {
- "version": "1.5.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
- "node_modules/binary-extensions": {
- "version": "2.3.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/binary-extensions/-/binary-extensions-2.3.0.tgz",
- "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/bindings": {
- "version": "1.5.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/bindings/-/bindings-1.5.0.tgz",
- "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
- "license": "MIT",
- "dependencies": {
- "file-uri-to-path": "1.0.0"
- }
- },
- "node_modules/bl": {
- "version": "4.1.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/bl/-/bl-4.1.0.tgz",
- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "license": "MIT",
- "dependencies": {
- "buffer": "^5.5.0",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
- }
- },
- "node_modules/body-parser": {
- "version": "1.20.3",
- "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz",
- "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
- "license": "MIT",
- "dependencies": {
- "bytes": "3.1.2",
- "content-type": "~1.0.5",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "on-finished": "2.4.1",
- "qs": "6.13.0",
- "raw-body": "2.5.2",
- "type-is": "~1.6.18",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/braces": {
- "version": "3.0.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fill-range": "^7.1.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/buffer": {
- "version": "5.7.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
- }
- },
- "node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/cacache": {
- "version": "15.3.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/cacache/-/cacache-15.3.0.tgz",
- "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "@npmcli/fs": "^1.0.0",
- "@npmcli/move-file": "^1.0.1",
- "chownr": "^2.0.0",
- "fs-minipass": "^2.0.0",
- "glob": "^7.1.4",
- "infer-owner": "^1.0.4",
- "lru-cache": "^6.0.0",
- "minipass": "^3.1.1",
- "minipass-collect": "^1.0.2",
- "minipass-flush": "^1.0.5",
- "minipass-pipeline": "^1.2.2",
- "mkdirp": "^1.0.3",
- "p-map": "^4.0.0",
- "promise-inflight": "^1.0.1",
- "rimraf": "^3.0.2",
- "ssri": "^8.0.1",
- "tar": "^6.0.2",
- "unique-filename": "^1.1.1"
- },
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/call-bind": {
- "version": "1.0.7",
- "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.7.tgz",
- "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
- "license": "MIT",
- "dependencies": {
- "es-define-property": "^1.0.0",
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.4",
- "set-function-length": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/chokidar": {
- "version": "3.6.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "anymatch": "~3.1.2",
- "braces": "~3.0.2",
- "glob-parent": "~5.1.2",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.6.0"
- },
- "engines": {
- "node": ">= 8.10.0"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.2"
- }
- },
- "node_modules/chownr": {
- "version": "2.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/chownr/-/chownr-2.0.0.tgz",
- "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
- "license": "ISC",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/clean-stack": {
- "version": "2.2.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/clean-stack/-/clean-stack-2.2.0.tgz",
- "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/color": {
- "version": "3.2.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/color/-/color-3.2.1.tgz",
- "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^1.9.3",
- "color-string": "^1.6.0"
- }
- },
- "node_modules/color-convert": {
- "version": "1.9.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "license": "MIT",
- "dependencies": {
- "color-name": "1.1.3"
- }
- },
- "node_modules/color-name": {
- "version": "1.1.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
- "license": "MIT"
- },
- "node_modules/color-string": {
- "version": "1.9.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/color-string/-/color-string-1.9.1.tgz",
- "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
- "license": "MIT",
- "dependencies": {
- "color-name": "^1.0.0",
- "simple-swizzle": "^0.2.2"
- }
- },
- "node_modules/color-support": {
- "version": "1.1.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/color-support/-/color-support-1.1.3.tgz",
- "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
- "license": "ISC",
- "optional": true,
- "bin": {
- "color-support": "bin.js"
- }
- },
- "node_modules/colorspace": {
- "version": "1.1.4",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/colorspace/-/colorspace-1.1.4.tgz",
- "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
- "license": "MIT",
- "dependencies": {
- "color": "^3.1.3",
- "text-hex": "1.0.x"
- }
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/console-control-strings": {
- "version": "1.1.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/console-control-strings/-/console-control-strings-1.1.0.tgz",
- "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
- "license": "ISC",
- "optional": true
- },
- "node_modules/content-disposition": {
- "version": "0.5.4",
- "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
- "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
- "license": "MIT",
- "dependencies": {
- "safe-buffer": "5.2.1"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/content-type": {
- "version": "1.0.5",
- "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
- "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie": {
- "version": "0.7.1",
- "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz",
- "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie-parser": {
- "version": "1.4.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/cookie-parser/-/cookie-parser-1.4.7.tgz",
- "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
- "license": "MIT",
- "dependencies": {
- "cookie": "0.7.2",
- "cookie-signature": "1.0.6"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/cookie-parser/node_modules/cookie": {
- "version": "0.7.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/cookie/-/cookie-0.7.2.tgz",
- "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie-signature": {
- "version": "1.0.6",
- "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz",
- "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
- "license": "MIT"
- },
- "node_modules/cors": {
- "version": "2.8.5",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/cors/-/cors-2.8.5.tgz",
- "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
- "license": "MIT",
- "dependencies": {
- "object-assign": "^4",
- "vary": "^1"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/create-require": {
- "version": "1.1.1",
- "resolved": "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz",
- "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/decompress-response": {
- "version": "6.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/decompress-response/-/decompress-response-6.0.0.tgz",
- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
- "license": "MIT",
- "dependencies": {
- "mimic-response": "^3.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/deep-extend": {
- "version": "0.6.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/deep-extend/-/deep-extend-0.6.0.tgz",
- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
- "license": "MIT",
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/define-data-property": {
- "version": "1.1.4",
- "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz",
- "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
- "license": "MIT",
- "dependencies": {
- "es-define-property": "^1.0.0",
- "es-errors": "^1.3.0",
- "gopd": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/delegates": {
- "version": "1.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/delegates/-/delegates-1.0.0.tgz",
- "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/denque": {
- "version": "2.1.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/denque/-/denque-2.1.0.tgz",
- "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/destroy": {
- "version": "1.2.0",
- "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
- "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.0.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/detect-libc/-/detect-libc-2.0.3.tgz",
- "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/diff": {
- "version": "4.0.2",
- "resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.2.tgz",
- "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.3.1"
- }
- },
- "node_modules/dotenv": {
- "version": "16.4.5",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/dotenv/-/dotenv-16.4.5.tgz",
- "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://dotenvx.com"
- }
- },
- "node_modules/dottie": {
- "version": "2.0.6",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/dottie/-/dottie-2.0.6.tgz",
- "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==",
- "license": "MIT"
- },
- "node_modules/ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
- "license": "MIT"
- },
- "node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/enabled": {
- "version": "2.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/enabled/-/enabled-2.0.0.tgz",
- "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
- "license": "MIT"
- },
- "node_modules/encodeurl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
- "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/encoding": {
- "version": "0.1.13",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/encoding/-/encoding-0.1.13.tgz",
- "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "iconv-lite": "^0.6.2"
- }
- },
- "node_modules/encoding/node_modules/iconv-lite": {
- "version": "0.6.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/iconv-lite/-/iconv-lite-0.6.3.tgz",
- "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/end-of-stream": {
- "version": "1.4.4",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/end-of-stream/-/end-of-stream-1.4.4.tgz",
- "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
- "license": "MIT",
- "dependencies": {
- "once": "^1.4.0"
- }
- },
- "node_modules/env-paths": {
- "version": "2.2.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/env-paths/-/env-paths-2.2.1.tgz",
- "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/err-code": {
- "version": "2.0.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/err-code/-/err-code-2.0.3.tgz",
- "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/es-define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.0.tgz",
- "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
- "license": "MIT",
- "dependencies": {
- "get-intrinsic": "^1.2.4"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
- "license": "MIT"
- },
- "node_modules/etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
- "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/event-target-shim": {
- "version": "5.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/event-target-shim/-/event-target-shim-5.0.1.tgz",
- "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/events": {
- "version": "3.3.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/events/-/events-3.3.0.tgz",
- "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
- "license": "MIT",
- "engines": {
- "node": ">=0.8.x"
- }
- },
- "node_modules/expand-template": {
- "version": "2.0.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/expand-template/-/expand-template-2.0.3.tgz",
- "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
- "license": "(MIT OR WTFPL)",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/express": {
- "version": "4.21.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/express/-/express-4.21.1.tgz",
- "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
- "license": "MIT",
- "dependencies": {
- "accepts": "~1.3.8",
- "array-flatten": "1.1.1",
- "body-parser": "1.20.3",
- "content-disposition": "0.5.4",
- "content-type": "~1.0.4",
- "cookie": "0.7.1",
- "cookie-signature": "1.0.6",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "finalhandler": "1.3.1",
- "fresh": "0.5.2",
- "http-errors": "2.0.0",
- "merge-descriptors": "1.0.3",
- "methods": "~1.1.2",
- "on-finished": "2.4.1",
- "parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
- "proxy-addr": "~2.0.7",
- "qs": "6.13.0",
- "range-parser": "~1.2.1",
- "safe-buffer": "5.2.1",
- "send": "0.19.0",
- "serve-static": "1.16.2",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "type-is": "~1.6.18",
- "utils-merge": "1.0.1",
- "vary": "~1.1.2"
- },
- "engines": {
- "node": ">= 0.10.0"
- }
- },
- "node_modules/fecha": {
- "version": "4.2.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/fecha/-/fecha-4.2.3.tgz",
- "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
- "license": "MIT"
- },
- "node_modules/file-uri-to-path": {
- "version": "1.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
- "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
- "license": "MIT"
- },
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/finalhandler": {
- "version": "1.3.1",
- "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz",
- "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "2.6.9",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "on-finished": "2.4.1",
- "parseurl": "~1.3.3",
- "statuses": "2.0.1",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/fn.name": {
- "version": "1.1.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/fn.name/-/fn.name-1.1.0.tgz",
- "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
- "license": "MIT"
- },
- "node_modules/forwarded": {
- "version": "0.2.0",
- "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
- "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fresh": {
- "version": "0.5.2",
- "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz",
- "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fs-constants": {
- "version": "1.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/fs-constants/-/fs-constants-1.0.0.tgz",
- "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
- "license": "MIT"
- },
- "node_modules/fs-minipass": {
- "version": "2.1.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/fs-minipass/-/fs-minipass-2.1.0.tgz",
- "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
- "license": "ISC",
- "dependencies": {
- "minipass": "^3.0.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "license": "ISC",
- "optional": true
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/gauge": {
- "version": "4.0.4",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/gauge/-/gauge-4.0.4.tgz",
- "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
- "deprecated": "This package is no longer supported.",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "aproba": "^1.0.3 || ^2.0.0",
- "color-support": "^1.1.3",
- "console-control-strings": "^1.1.0",
- "has-unicode": "^2.0.1",
- "signal-exit": "^3.0.7",
- "string-width": "^4.2.3",
- "strip-ansi": "^6.0.1",
- "wide-align": "^1.1.5"
- },
- "engines": {
- "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
- }
- },
- "node_modules/generate-function": {
- "version": "2.3.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/generate-function/-/generate-function-2.3.1.tgz",
- "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
- "license": "MIT",
- "dependencies": {
- "is-property": "^1.0.2"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.2.4",
- "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
- "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3",
- "hasown": "^2.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/github-from-package": {
- "version": "0.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/github-from-package/-/github-from-package-0.0.0.tgz",
- "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
- "license": "MIT"
- },
- "node_modules/glob": {
- "version": "7.2.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "deprecated": "Glob versions prior to v9 are no longer supported",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- },
- "engines": {
- "node": "*"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/gopd": {
- "version": "1.0.1",
- "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.0.1.tgz",
- "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
- "license": "MIT",
- "dependencies": {
- "get-intrinsic": "^1.1.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "license": "ISC",
- "optional": true
- },
- "node_modules/has-flag": {
- "version": "3.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/has-property-descriptors": {
- "version": "1.0.2",
- "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
- "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
- "license": "MIT",
- "dependencies": {
- "es-define-property": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-proto": {
- "version": "1.0.3",
- "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.0.3.tgz",
- "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.0.3",
- "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz",
- "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-unicode": {
- "version": "2.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/has-unicode/-/has-unicode-2.0.1.tgz",
- "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
- "license": "ISC",
- "optional": true
- },
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/http-cache-semantics": {
- "version": "4.1.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
- "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
- "license": "BSD-2-Clause",
- "optional": true
- },
- "node_modules/http-errors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
- "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
- "license": "MIT",
- "dependencies": {
- "depd": "2.0.0",
- "inherits": "2.0.4",
- "setprototypeof": "1.2.0",
- "statuses": "2.0.1",
- "toidentifier": "1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/http-proxy-agent": {
- "version": "4.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
- "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@tootallnate/once": "1",
- "agent-base": "6",
- "debug": "4"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/http-proxy-agent/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/http-proxy-agent/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/https-proxy-agent": {
- "version": "5.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
- "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "agent-base": "6",
- "debug": "4"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/https-proxy-agent/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/https-proxy-agent/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/humanize-ms": {
- "version": "1.2.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/humanize-ms/-/humanize-ms-1.2.1.tgz",
- "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "ms": "^2.0.0"
- }
- },
- "node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/ieee754": {
- "version": "1.2.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "BSD-3-Clause"
- },
- "node_modules/ignore-by-default": {
- "version": "1.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
- "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=0.8.19"
- }
- },
- "node_modules/indent-string": {
- "version": "4.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/indent-string/-/indent-string-4.0.0.tgz",
- "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/infer-owner": {
- "version": "1.0.4",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/infer-owner/-/infer-owner-1.0.4.tgz",
- "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
- "license": "ISC",
- "optional": true
- },
- "node_modules/inflection": {
- "version": "1.13.4",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/inflection/-/inflection-1.13.4.tgz",
- "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
- "engines": [
- "node >= 0.4.0"
- ],
- "license": "MIT"
- },
- "node_modules/inflight": {
- "version": "1.0.6",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
- "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "license": "ISC"
- },
- "node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "license": "ISC"
- },
- "node_modules/ip-address": {
- "version": "9.0.5",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ip-address/-/ip-address-9.0.5.tgz",
- "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "jsbn": "1.1.0",
- "sprintf-js": "^1.1.3"
- },
- "engines": {
- "node": ">= 12"
- }
- },
- "node_modules/ipaddr.js": {
- "version": "1.9.1",
- "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
- "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/is-arrayish": {
- "version": "0.3.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/is-arrayish/-/is-arrayish-0.3.2.tgz",
- "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
- "license": "MIT"
- },
- "node_modules/is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "binary-extensions": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-lambda": {
- "version": "1.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/is-lambda/-/is-lambda-1.0.1.tgz",
- "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/is-property": {
- "version": "1.0.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/is-property/-/is-property-1.0.2.tgz",
- "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
- "license": "MIT"
- },
- "node_modules/is-stream": {
- "version": "2.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/is-stream/-/is-stream-2.0.1.tgz",
- "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "license": "ISC",
- "optional": true
- },
- "node_modules/jsbn": {
- "version": "1.1.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/jsbn/-/jsbn-1.1.0.tgz",
- "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/kuler": {
- "version": "2.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/kuler/-/kuler-2.0.0.tgz",
- "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
- "license": "MIT"
- },
- "node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "license": "MIT"
- },
- "node_modules/logform": {
- "version": "2.6.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/logform/-/logform-2.6.1.tgz",
- "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==",
- "license": "MIT",
- "dependencies": {
- "@colors/colors": "1.6.0",
- "@types/triple-beam": "^1.3.2",
- "fecha": "^4.2.0",
- "ms": "^2.1.1",
- "safe-stable-stringify": "^2.3.1",
- "triple-beam": "^1.3.0"
- },
- "engines": {
- "node": ">= 12.0.0"
- }
- },
- "node_modules/logform/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/long": {
- "version": "5.2.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/long/-/long-5.2.3.tgz",
- "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
- "license": "Apache-2.0"
- },
- "node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/lru.min": {
- "version": "1.1.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/lru.min/-/lru.min-1.1.1.tgz",
- "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==",
- "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/make-error": {
- "version": "1.3.6",
- "resolved": "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz",
- "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/make-fetch-happen": {
- "version": "9.1.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz",
- "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "agentkeepalive": "^4.1.3",
- "cacache": "^15.2.0",
- "http-cache-semantics": "^4.1.0",
- "http-proxy-agent": "^4.0.1",
- "https-proxy-agent": "^5.0.0",
- "is-lambda": "^1.0.1",
- "lru-cache": "^6.0.0",
- "minipass": "^3.1.3",
- "minipass-collect": "^1.0.2",
- "minipass-fetch": "^1.3.2",
- "minipass-flush": "^1.0.5",
- "minipass-pipeline": "^1.2.4",
- "negotiator": "^0.6.2",
- "promise-retry": "^2.0.1",
- "socks-proxy-agent": "^6.0.0",
- "ssri": "^8.0.0"
- },
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/media-typer": {
- "version": "0.3.0",
- "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/merge-descriptors": {
- "version": "1.0.3",
- "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
- "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/methods": {
- "version": "1.1.2",
- "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
- "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime": {
- "version": "1.6.0",
- "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
- "license": "MIT",
- "bin": {
- "mime": "cli.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mimic-response": {
- "version": "3.1.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/mimic-response/-/mimic-response-3.1.0.tgz",
- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "devOptional": true,
- "license": "ISC",
- "dependencies": {
- "brace-expansion": "^1.1.7"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/minipass": {
- "version": "3.3.6",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/minipass/-/minipass-3.3.6.tgz",
- "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
- "license": "ISC",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/minipass-collect": {
- "version": "1.0.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/minipass-collect/-/minipass-collect-1.0.2.tgz",
- "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "minipass": "^3.0.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/minipass-fetch": {
- "version": "1.4.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/minipass-fetch/-/minipass-fetch-1.4.1.tgz",
- "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "minipass": "^3.1.0",
- "minipass-sized": "^1.0.3",
- "minizlib": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- },
- "optionalDependencies": {
- "encoding": "^0.1.12"
- }
- },
- "node_modules/minipass-flush": {
- "version": "1.0.5",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/minipass-flush/-/minipass-flush-1.0.5.tgz",
- "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "minipass": "^3.0.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/minipass-pipeline": {
- "version": "1.2.4",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
- "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "minipass": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/minipass-sized": {
- "version": "1.0.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/minipass-sized/-/minipass-sized-1.0.3.tgz",
- "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "minipass": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/minizlib": {
- "version": "2.1.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/minizlib/-/minizlib-2.1.2.tgz",
- "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
- "license": "MIT",
- "dependencies": {
- "minipass": "^3.0.0",
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/mkdirp": {
- "version": "1.0.4",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/mkdirp/-/mkdirp-1.0.4.tgz",
- "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
- "license": "MIT",
- "bin": {
- "mkdirp": "bin/cmd.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/mkdirp-classic": {
- "version": "0.5.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
- "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
- "license": "MIT"
- },
- "node_modules/moment": {
- "version": "2.30.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/moment/-/moment-2.30.1.tgz",
- "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/moment-timezone": {
- "version": "0.5.46",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/moment-timezone/-/moment-timezone-0.5.46.tgz",
- "integrity": "sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==",
- "license": "MIT",
- "dependencies": {
- "moment": "^2.29.4"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "license": "MIT"
- },
- "node_modules/mysql2": {
- "version": "3.11.4",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/mysql2/-/mysql2-3.11.4.tgz",
- "integrity": "sha512-Z2o3tY4Z8EvSRDwknaC40MdZ3+m0sKbpnXrShQLdxPrAvcNli7jLrD2Zd2IzsRMw4eK9Yle500FDmlkIqp+krg==",
- "license": "MIT",
- "dependencies": {
- "aws-ssl-profiles": "^1.1.1",
- "denque": "^2.1.0",
- "generate-function": "^2.3.1",
- "iconv-lite": "^0.6.3",
- "long": "^5.2.1",
- "lru.min": "^1.0.0",
- "named-placeholders": "^1.1.3",
- "seq-queue": "^0.0.5",
- "sqlstring": "^2.3.2"
- },
- "engines": {
- "node": ">= 8.0"
- }
- },
- "node_modules/mysql2/node_modules/iconv-lite": {
- "version": "0.6.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/iconv-lite/-/iconv-lite-0.6.3.tgz",
- "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/named-placeholders": {
- "version": "1.1.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/named-placeholders/-/named-placeholders-1.1.3.tgz",
- "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
- "license": "MIT",
- "dependencies": {
- "lru-cache": "^7.14.1"
- },
- "engines": {
- "node": ">=12.0.0"
- }
- },
- "node_modules/named-placeholders/node_modules/lru-cache": {
- "version": "7.18.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/lru-cache/-/lru-cache-7.18.3.tgz",
- "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/napi-build-utils": {
- "version": "1.0.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
- "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==",
- "license": "MIT"
- },
- "node_modules/negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/node-abi": {
- "version": "3.71.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/node-abi/-/node-abi-3.71.0.tgz",
- "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==",
- "license": "MIT",
- "dependencies": {
- "semver": "^7.3.5"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/node-addon-api": {
- "version": "7.1.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/node-addon-api/-/node-addon-api-7.1.1.tgz",
- "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
- "license": "MIT"
- },
- "node_modules/node-gyp": {
- "version": "8.4.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/node-gyp/-/node-gyp-8.4.1.tgz",
- "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "env-paths": "^2.2.0",
- "glob": "^7.1.4",
- "graceful-fs": "^4.2.6",
- "make-fetch-happen": "^9.1.0",
- "nopt": "^5.0.0",
- "npmlog": "^6.0.0",
- "rimraf": "^3.0.2",
- "semver": "^7.3.5",
- "tar": "^6.1.2",
- "which": "^2.0.2"
- },
- "bin": {
- "node-gyp": "bin/node-gyp.js"
- },
- "engines": {
- "node": ">= 10.12.0"
- }
- },
- "node_modules/nodemon": {
- "version": "3.1.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/nodemon/-/nodemon-3.1.7.tgz",
- "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chokidar": "^3.5.2",
- "debug": "^4",
- "ignore-by-default": "^1.0.1",
- "minimatch": "^3.1.2",
- "pstree.remy": "^1.1.8",
- "semver": "^7.5.3",
- "simple-update-notifier": "^2.0.0",
- "supports-color": "^5.5.0",
- "touch": "^3.1.0",
- "undefsafe": "^2.0.5"
- },
- "bin": {
- "nodemon": "bin/nodemon.js"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/nodemon"
- }
- },
- "node_modules/nodemon/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/nodemon/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/nopt": {
- "version": "5.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/nopt/-/nopt-5.0.0.tgz",
- "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "abbrev": "1"
- },
- "bin": {
- "nopt": "bin/nopt.js"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/normalize-path": {
- "version": "3.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/npmlog": {
- "version": "6.0.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/npmlog/-/npmlog-6.0.2.tgz",
- "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
- "deprecated": "This package is no longer supported.",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "are-we-there-yet": "^3.0.0",
- "console-control-strings": "^1.1.0",
- "gauge": "^4.0.3",
- "set-blocking": "^2.0.0"
- },
- "engines": {
- "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
- }
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-inspect": {
- "version": "1.13.2",
- "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.2.tgz",
- "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/on-finished": {
- "version": "2.4.1",
- "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
- "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
- "license": "MIT",
- "dependencies": {
- "ee-first": "1.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "license": "ISC",
- "dependencies": {
- "wrappy": "1"
- }
- },
- "node_modules/one-time": {
- "version": "1.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/one-time/-/one-time-1.0.0.tgz",
- "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
- "license": "MIT",
- "dependencies": {
- "fn.name": "1.x.x"
- }
- },
- "node_modules/p-map": {
- "version": "4.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/p-map/-/p-map-4.0.0.tgz",
- "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "aggregate-error": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/path-to-regexp": {
- "version": "0.1.10",
- "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
- "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
- "license": "MIT"
- },
- "node_modules/pg": {
- "version": "8.13.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/pg/-/pg-8.13.1.tgz",
- "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==",
- "license": "MIT",
- "dependencies": {
- "pg-connection-string": "^2.7.0",
- "pg-pool": "^3.7.0",
- "pg-protocol": "^1.7.0",
- "pg-types": "^2.1.0",
- "pgpass": "1.x"
- },
- "engines": {
- "node": ">= 8.0.0"
- },
- "optionalDependencies": {
- "pg-cloudflare": "^1.1.1"
- },
- "peerDependencies": {
- "pg-native": ">=3.0.1"
- },
- "peerDependenciesMeta": {
- "pg-native": {
- "optional": true
- }
- }
- },
- "node_modules/pg-cloudflare": {
- "version": "1.1.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
- "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/pg-connection-string": {
- "version": "2.7.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/pg-connection-string/-/pg-connection-string-2.7.0.tgz",
- "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==",
- "license": "MIT"
- },
- "node_modules/pg-hstore": {
- "version": "2.3.4",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/pg-hstore/-/pg-hstore-2.3.4.tgz",
- "integrity": "sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==",
- "license": "MIT",
- "dependencies": {
- "underscore": "^1.13.1"
- },
- "engines": {
- "node": ">= 0.8.x"
- }
- },
- "node_modules/pg-int8": {
- "version": "1.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/pg-int8/-/pg-int8-1.0.1.tgz",
- "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
- "license": "ISC",
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/pg-pool": {
- "version": "3.7.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/pg-pool/-/pg-pool-3.7.0.tgz",
- "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==",
- "license": "MIT",
- "peerDependencies": {
- "pg": ">=8.0"
- }
- },
- "node_modules/pg-protocol": {
- "version": "1.7.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/pg-protocol/-/pg-protocol-1.7.0.tgz",
- "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==",
- "license": "MIT"
- },
- "node_modules/pg-types": {
- "version": "2.2.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/pg-types/-/pg-types-2.2.0.tgz",
- "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
- "license": "MIT",
- "dependencies": {
- "pg-int8": "1.0.1",
- "postgres-array": "~2.0.0",
- "postgres-bytea": "~1.0.0",
- "postgres-date": "~1.0.4",
- "postgres-interval": "^1.1.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/pgpass": {
- "version": "1.0.5",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/pgpass/-/pgpass-1.0.5.tgz",
- "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
- "license": "MIT",
- "dependencies": {
- "split2": "^4.1.0"
- }
- },
- "node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/postgres-array": {
- "version": "2.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/postgres-array/-/postgres-array-2.0.0.tgz",
- "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/postgres-bytea": {
- "version": "1.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
- "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/postgres-date": {
- "version": "1.0.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/postgres-date/-/postgres-date-1.0.7.tgz",
- "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/postgres-interval": {
- "version": "1.2.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/postgres-interval/-/postgres-interval-1.2.0.tgz",
- "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
- "license": "MIT",
- "dependencies": {
- "xtend": "^4.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/prebuild-install": {
- "version": "7.1.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/prebuild-install/-/prebuild-install-7.1.2.tgz",
- "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==",
- "license": "MIT",
- "dependencies": {
- "detect-libc": "^2.0.0",
- "expand-template": "^2.0.3",
- "github-from-package": "0.0.0",
- "minimist": "^1.2.3",
- "mkdirp-classic": "^0.5.3",
- "napi-build-utils": "^1.0.1",
- "node-abi": "^3.3.0",
- "pump": "^3.0.0",
- "rc": "^1.2.7",
- "simple-get": "^4.0.0",
- "tar-fs": "^2.0.0",
- "tunnel-agent": "^0.6.0"
- },
- "bin": {
- "prebuild-install": "bin.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/process": {
- "version": "0.11.10",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/process/-/process-0.11.10.tgz",
- "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6.0"
- }
- },
- "node_modules/promise-inflight": {
- "version": "1.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/promise-inflight/-/promise-inflight-1.0.1.tgz",
- "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
- "license": "ISC",
- "optional": true
- },
- "node_modules/promise-retry": {
- "version": "2.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/promise-retry/-/promise-retry-2.0.1.tgz",
- "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "err-code": "^2.0.2",
- "retry": "^0.12.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/proxy-addr": {
- "version": "2.0.7",
- "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
- "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
- "license": "MIT",
- "dependencies": {
- "forwarded": "0.2.0",
- "ipaddr.js": "1.9.1"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/pstree.remy": {
- "version": "1.1.8",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/pstree.remy/-/pstree.remy-1.1.8.tgz",
- "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/pump": {
- "version": "3.0.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/pump/-/pump-3.0.2.tgz",
- "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
- "license": "MIT",
- "dependencies": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- },
- "node_modules/qs": {
- "version": "6.13.0",
- "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz",
- "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "side-channel": "^1.0.6"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/range-parser": {
- "version": "1.2.1",
- "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/raw-body": {
- "version": "2.5.2",
- "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz",
- "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
- "license": "MIT",
- "dependencies": {
- "bytes": "3.1.2",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "unpipe": "1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/rc": {
- "version": "1.2.8",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/rc/-/rc-1.2.8.tgz",
- "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
- "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
- "dependencies": {
- "deep-extend": "^0.6.0",
- "ini": "~1.3.0",
- "minimist": "^1.2.0",
- "strip-json-comments": "~2.0.1"
- },
- "bin": {
- "rc": "cli.js"
- }
- },
- "node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "license": "MIT",
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/readdirp": {
- "version": "3.6.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "picomatch": "^2.2.1"
- },
- "engines": {
- "node": ">=8.10.0"
- }
- },
- "node_modules/retry": {
- "version": "0.12.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/retry/-/retry-0.12.0.tgz",
- "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/retry-as-promised": {
- "version": "7.0.4",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/retry-as-promised/-/retry-as-promised-7.0.4.tgz",
- "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==",
- "license": "MIT"
- },
- "node_modules/rimraf": {
- "version": "3.0.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "deprecated": "Rimraf versions prior to v4 are no longer supported",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "glob": "^7.1.3"
- },
- "bin": {
- "rimraf": "bin.js"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
- "node_modules/safe-stable-stringify": {
- "version": "2.5.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
- "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "license": "MIT"
- },
- "node_modules/semver": {
- "version": "7.6.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/semver/-/semver-7.6.3.tgz",
- "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/send": {
- "version": "0.19.0",
- "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz",
- "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
- "license": "MIT",
- "dependencies": {
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "fresh": "0.5.2",
- "http-errors": "2.0.0",
- "mime": "1.6.0",
- "ms": "2.1.3",
- "on-finished": "2.4.1",
- "range-parser": "~1.2.1",
- "statuses": "2.0.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/send/node_modules/encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/send/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/seq-queue": {
- "version": "0.0.5",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/seq-queue/-/seq-queue-0.0.5.tgz",
- "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
- },
- "node_modules/sequelize": {
- "version": "6.37.5",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/sequelize/-/sequelize-6.37.5.tgz",
- "integrity": "sha512-10WA4poUb3XWnUROThqL2Apq9C2NhyV1xHPMZuybNMCucDsbbFuKg51jhmyvvAUyUqCiimwTZamc3AHhMoBr2Q==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/sequelize"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "@types/debug": "^4.1.8",
- "@types/validator": "^13.7.17",
- "debug": "^4.3.4",
- "dottie": "^2.0.6",
- "inflection": "^1.13.4",
- "lodash": "^4.17.21",
- "moment": "^2.29.4",
- "moment-timezone": "^0.5.43",
- "pg-connection-string": "^2.6.1",
- "retry-as-promised": "^7.0.4",
- "semver": "^7.5.4",
- "sequelize-pool": "^7.1.0",
- "toposort-class": "^1.0.1",
- "uuid": "^8.3.2",
- "validator": "^13.9.0",
- "wkx": "^0.5.0"
- },
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependenciesMeta": {
- "ibm_db": {
- "optional": true
- },
- "mariadb": {
- "optional": true
- },
- "mysql2": {
- "optional": true
- },
- "oracledb": {
- "optional": true
- },
- "pg": {
- "optional": true
- },
- "pg-hstore": {
- "optional": true
- },
- "snowflake-sdk": {
- "optional": true
- },
- "sqlite3": {
- "optional": true
- },
- "tedious": {
- "optional": true
- }
- }
- },
- "node_modules/sequelize-pool": {
- "version": "7.1.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/sequelize-pool/-/sequelize-pool-7.1.0.tgz",
- "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==",
- "license": "MIT",
- "engines": {
- "node": ">= 10.0.0"
- }
- },
- "node_modules/sequelize/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/sequelize/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/serve-static": {
- "version": "1.16.2",
- "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz",
- "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
- "license": "MIT",
- "dependencies": {
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "parseurl": "~1.3.3",
- "send": "0.19.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/set-blocking": {
- "version": "2.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/set-blocking/-/set-blocking-2.0.0.tgz",
- "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
- "license": "ISC",
- "optional": true
- },
- "node_modules/set-function-length": {
- "version": "1.2.2",
- "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz",
- "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
- "license": "MIT",
- "dependencies": {
- "define-data-property": "^1.1.4",
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2",
- "get-intrinsic": "^1.2.4",
- "gopd": "^1.0.1",
- "has-property-descriptors": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/setprototypeof": {
- "version": "1.2.0",
- "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
- "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
- "license": "ISC"
- },
- "node_modules/side-channel": {
- "version": "1.0.6",
- "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.6.tgz",
- "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.7",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.4",
- "object-inspect": "^1.13.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/signal-exit": {
- "version": "3.0.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/signal-exit/-/signal-exit-3.0.7.tgz",
- "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
- "license": "ISC",
- "optional": true
- },
- "node_modules/simple-concat": {
- "version": "1.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/simple-concat/-/simple-concat-1.0.1.tgz",
- "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
- "node_modules/simple-get": {
- "version": "4.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/simple-get/-/simple-get-4.0.1.tgz",
- "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "decompress-response": "^6.0.0",
- "once": "^1.3.1",
- "simple-concat": "^1.0.0"
- }
- },
- "node_modules/simple-swizzle": {
- "version": "0.2.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
- "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
- "license": "MIT",
- "dependencies": {
- "is-arrayish": "^0.3.1"
- }
- },
- "node_modules/simple-update-notifier": {
- "version": "2.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
- "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "semver": "^7.5.3"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/smart-buffer": {
- "version": "4.2.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/smart-buffer/-/smart-buffer-4.2.0.tgz",
- "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">= 6.0.0",
- "npm": ">= 3.0.0"
- }
- },
- "node_modules/socks": {
- "version": "2.8.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/socks/-/socks-2.8.3.tgz",
- "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "ip-address": "^9.0.5",
- "smart-buffer": "^4.2.0"
- },
- "engines": {
- "node": ">= 10.0.0",
- "npm": ">= 3.0.0"
- }
- },
- "node_modules/socks-proxy-agent": {
- "version": "6.2.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz",
- "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "agent-base": "^6.0.2",
- "debug": "^4.3.3",
- "socks": "^2.6.2"
- },
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/socks-proxy-agent/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/socks-proxy-agent/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/split2": {
- "version": "4.2.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/split2/-/split2-4.2.0.tgz",
- "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
- "license": "ISC",
- "engines": {
- "node": ">= 10.x"
- }
- },
- "node_modules/sprintf-js": {
- "version": "1.1.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/sprintf-js/-/sprintf-js-1.1.3.tgz",
- "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
- "license": "BSD-3-Clause",
- "optional": true
- },
- "node_modules/sqlite3": {
- "version": "5.1.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/sqlite3/-/sqlite3-5.1.7.tgz",
- "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==",
- "hasInstallScript": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "bindings": "^1.5.0",
- "node-addon-api": "^7.0.0",
- "prebuild-install": "^7.1.1",
- "tar": "^6.1.11"
- },
- "optionalDependencies": {
- "node-gyp": "8.x"
- },
- "peerDependencies": {
- "node-gyp": "8.x"
- },
- "peerDependenciesMeta": {
- "node-gyp": {
- "optional": true
- }
- }
- },
- "node_modules/sqlstring": {
- "version": "2.3.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/sqlstring/-/sqlstring-2.3.3.tgz",
- "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/ssri": {
- "version": "8.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ssri/-/ssri-8.0.1.tgz",
- "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "minipass": "^3.1.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/stack-trace": {
- "version": "0.0.10",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/stack-trace/-/stack-trace-0.0.10.tgz",
- "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/statuses": {
- "version": "2.0.1",
- "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
- "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/string_decoder": {
- "version": "1.3.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/string_decoder/-/string_decoder-1.3.0.tgz",
- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "license": "MIT",
- "dependencies": {
- "safe-buffer": "~5.2.0"
- }
- },
- "node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "2.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/supports-color": {
- "version": "5.5.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^3.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/tar": {
- "version": "6.2.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/tar/-/tar-6.2.1.tgz",
- "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
- "license": "ISC",
- "dependencies": {
- "chownr": "^2.0.0",
- "fs-minipass": "^2.0.0",
- "minipass": "^5.0.0",
- "minizlib": "^2.1.1",
- "mkdirp": "^1.0.3",
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/tar-fs": {
- "version": "2.1.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/tar-fs/-/tar-fs-2.1.1.tgz",
- "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
- "license": "MIT",
- "dependencies": {
- "chownr": "^1.1.1",
- "mkdirp-classic": "^0.5.2",
- "pump": "^3.0.0",
- "tar-stream": "^2.1.4"
- }
- },
- "node_modules/tar-fs/node_modules/chownr": {
- "version": "1.1.4",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
- "license": "ISC"
- },
- "node_modules/tar-stream": {
- "version": "2.2.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/tar-stream/-/tar-stream-2.2.0.tgz",
- "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
- "license": "MIT",
- "dependencies": {
- "bl": "^4.0.3",
- "end-of-stream": "^1.4.1",
- "fs-constants": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^3.1.1"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/tar/node_modules/minipass": {
- "version": "5.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/minipass/-/minipass-5.0.0.tgz",
- "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
- "license": "ISC",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/text-hex": {
- "version": "1.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/text-hex/-/text-hex-1.0.0.tgz",
- "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
- "license": "MIT"
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/toidentifier": {
- "version": "1.0.1",
- "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
- "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.6"
- }
- },
- "node_modules/toposort-class": {
- "version": "1.0.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/toposort-class/-/toposort-class-1.0.1.tgz",
- "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==",
- "license": "MIT"
- },
- "node_modules/touch": {
- "version": "3.1.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/touch/-/touch-3.1.1.tgz",
- "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "nodetouch": "bin/nodetouch.js"
- }
- },
- "node_modules/triple-beam": {
- "version": "1.4.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/triple-beam/-/triple-beam-1.4.1.tgz",
- "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
- "license": "MIT",
- "engines": {
- "node": ">= 14.0.0"
- }
- },
- "node_modules/ts-node": {
- "version": "10.9.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ts-node/-/ts-node-10.9.2.tgz",
- "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@cspotcode/source-map-support": "^0.8.0",
- "@tsconfig/node10": "^1.0.7",
- "@tsconfig/node12": "^1.0.7",
- "@tsconfig/node14": "^1.0.0",
- "@tsconfig/node16": "^1.0.2",
- "acorn": "^8.4.1",
- "acorn-walk": "^8.1.1",
- "arg": "^4.1.0",
- "create-require": "^1.1.0",
- "diff": "^4.0.1",
- "make-error": "^1.1.1",
- "v8-compile-cache-lib": "^3.0.1",
- "yn": "3.1.1"
- },
- "bin": {
- "ts-node": "dist/bin.js",
- "ts-node-cwd": "dist/bin-cwd.js",
- "ts-node-esm": "dist/bin-esm.js",
- "ts-node-script": "dist/bin-script.js",
- "ts-node-transpile-only": "dist/bin-transpile.js",
- "ts-script": "dist/bin-script-deprecated.js"
- },
- "peerDependencies": {
- "@swc/core": ">=1.2.50",
- "@swc/wasm": ">=1.2.50",
- "@types/node": "*",
- "typescript": ">=2.7"
- },
- "peerDependenciesMeta": {
- "@swc/core": {
- "optional": true
- },
- "@swc/wasm": {
- "optional": true
- }
- }
- },
- "node_modules/tunnel-agent": {
- "version": "0.6.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
- "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
- "license": "Apache-2.0",
- "dependencies": {
- "safe-buffer": "^5.0.1"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/type-is": {
- "version": "1.6.18",
- "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
- "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
- "license": "MIT",
- "dependencies": {
- "media-typer": "0.3.0",
- "mime-types": "~2.1.24"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/typescript": {
- "version": "5.6.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/typescript/-/typescript-5.6.3.tgz",
- "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/undefsafe": {
- "version": "2.0.5",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/undefsafe/-/undefsafe-2.0.5.tgz",
- "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/underscore": {
- "version": "1.13.7",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/underscore/-/underscore-1.13.7.tgz",
- "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
- "license": "MIT"
- },
- "node_modules/undici-types": {
- "version": "6.19.8",
- "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz",
- "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
- "license": "MIT"
- },
- "node_modules/unique-filename": {
- "version": "1.1.1",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/unique-filename/-/unique-filename-1.1.1.tgz",
- "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "unique-slug": "^2.0.0"
- }
- },
- "node_modules/unique-slug": {
- "version": "2.0.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/unique-slug/-/unique-slug-2.0.2.tgz",
- "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "imurmurhash": "^0.1.4"
- }
- },
- "node_modules/unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "license": "MIT"
- },
- "node_modules/utils-merge": {
- "version": "1.0.1",
- "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz",
- "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4.0"
- }
- },
- "node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "license": "MIT",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
- "node_modules/v8-compile-cache-lib": {
- "version": "3.0.1",
- "resolved": "https://registry.npmmirror.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
- "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/validator": {
- "version": "13.12.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/validator/-/validator-13.12.0.tgz",
- "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/vary": {
- "version": "1.1.2",
- "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
- "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/wide-align": {
- "version": "1.1.5",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/wide-align/-/wide-align-1.1.5.tgz",
- "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
- "license": "ISC",
- "optional": true,
- "dependencies": {
- "string-width": "^1.0.2 || 2 || 3 || 4"
- }
- },
- "node_modules/winston": {
- "version": "3.16.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/winston/-/winston-3.16.0.tgz",
- "integrity": "sha512-xz7+cyGN5M+4CmmD4Npq1/4T+UZaz7HaeTlAruFUTjk79CNMq+P6H30vlE4z0qfqJ01VHYQwd7OZo03nYm/+lg==",
- "license": "MIT",
- "dependencies": {
- "@colors/colors": "^1.6.0",
- "@dabh/diagnostics": "^2.0.2",
- "async": "^3.2.3",
- "is-stream": "^2.0.0",
- "logform": "^2.6.0",
- "one-time": "^1.0.0",
- "readable-stream": "^3.4.0",
- "safe-stable-stringify": "^2.3.1",
- "stack-trace": "0.0.x",
- "triple-beam": "^1.3.0",
- "winston-transport": "^4.7.0"
- },
- "engines": {
- "node": ">= 12.0.0"
- }
- },
- "node_modules/winston-transport": {
- "version": "4.8.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/winston-transport/-/winston-transport-4.8.0.tgz",
- "integrity": "sha512-qxSTKswC6llEMZKgCQdaWgDuMJQnhuvF5f2Nk3SNXc4byfQ+voo2mX1Px9dkNOuR8p0KAjfPG29PuYUSIb+vSA==",
- "license": "MIT",
- "dependencies": {
- "logform": "^2.6.1",
- "readable-stream": "^4.5.2",
- "triple-beam": "^1.3.0"
- },
- "engines": {
- "node": ">= 12.0.0"
- }
- },
- "node_modules/winston-transport/node_modules/buffer": {
- "version": "6.0.3",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/buffer/-/buffer-6.0.3.tgz",
- "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.2.1"
- }
- },
- "node_modules/winston-transport/node_modules/readable-stream": {
- "version": "4.5.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/readable-stream/-/readable-stream-4.5.2.tgz",
- "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
- "license": "MIT",
- "dependencies": {
- "abort-controller": "^3.0.0",
- "buffer": "^6.0.3",
- "events": "^3.3.0",
- "process": "^0.11.10",
- "string_decoder": "^1.3.0"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- }
- },
- "node_modules/wkx": {
- "version": "0.5.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/wkx/-/wkx-0.5.0.tgz",
- "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "license": "ISC"
- },
- "node_modules/ws": {
- "version": "8.18.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/ws/-/ws-8.18.0.tgz",
- "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
- "license": "MIT",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
- "node_modules/xtend": {
- "version": "4.0.2",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/xtend/-/xtend-4.0.2.tgz",
- "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4"
- }
- },
- "node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://mirrors.huaweicloud.com/repository/npm/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "license": "ISC"
- },
- "node_modules/yn": {
- "version": "3.1.1",
- "resolved": "https://registry.npmmirror.com/yn/-/yn-3.1.1.tgz",
- "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- }
- }
-}
diff --git a/server/package.json b/server/package.json
deleted file mode 100644
index 144eae3..0000000
--- a/server/package.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "dependencies": {
- "@types/dotenv": "^6.1.1",
- "@types/express": "^5.0.0",
- "cookie-parser": "^1.4.7",
- "cors": "^2.8.5",
- "dotenv": "^16.4.5",
- "express": "^4.21.1",
- "mysql2": "^3.11.4",
- "pg": "^8.13.1",
- "pg-hstore": "^2.3.4",
- "sequelize": "^6.37.5",
- "sqlite3": "^5.1.7",
- "winston": "^3.16.0",
- "ws": "^8.18.0"
- },
- "devDependencies": {
- "@types/cookie-parser": "^1.4.7",
- "@types/cors": "^2.8.17",
- "@types/node": "^22.8.6",
- "@types/sequelize": "^4.28.20",
- "@types/winston": "^2.4.4",
- "@types/ws": "^8.5.12",
- "@vercel/ncc": "^0.38.2",
- "nodemon": "^3.1.7",
- "ts-node": "^10.9.2",
- "typescript": "^5.6.3"
- },
- "scripts": {
- "start": "node dist/app.js",
- "dev": "nodemon src/app.ts",
- "build": "tsc",
- "ncc": "ncc build src/app.ts -o dist"
- }
-}
diff --git a/server/src/app.ts b/server/src/app.ts
deleted file mode 100644
index dabb95c..0000000
--- a/server/src/app.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import express from 'express';
-import { createServer } from 'http';
-import cors from 'cors';
-import cookieParser from 'cookie-parser';
-import { initSyncManager } from './sync/syncManager';
-import router from './routes';
-import { initDatabase } from './db/init';
-import logger from './config/logger';
-import env from './config/env';
-import sseRouter from './sync/adapters/sse';
-import path from 'path';
-
-const app = express();
-const server = createServer(app);
-
-// base middleware
-app.use(express.json());
-app.use(express.urlencoded({ extended: true }));
-app.use(cors({
- credentials: true,
- origin: true // 或者指定具体的前端域名
-}));
-app.use(cookieParser());
-
-// routes
-app.use('/api', router);
-app.use('/sse', sseRouter);
-
-// serve static files for other routes
-app.use(express.static(path.join(__dirname, 'web')));
-
-// init sync manager
-switch (env.SYNC_PROTOCOL) {
- case 'websocket':
- initSyncManager(server, 'websocket');
- break;
- case 'sse':
- initSyncManager(server, 'sse');
- break;
- default:
- throw new Error(`Unsupported sync protocol: ${env.SYNC_PROTOCOL}`);
-}
-
-// init database before start server
-initDatabase().then(() => {
- server.listen(env.PORT, () => {
- logger.info(`Server is running on port ${env.PORT} in ${env.NODE_ENV} mode`);
- });
-}).catch(error => {
- logger.error('Server startup failed:', error);
- process.exit(1);
-});
-
-export { app, server };
\ No newline at end of file
diff --git a/server/src/config/database.ts b/server/src/config/database.ts
deleted file mode 100644
index 27695af..0000000
--- a/server/src/config/database.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Dialect, Options } from 'sequelize';
-import env from './env';
-
-export const dbConfig: Options = {
- dialect: env.DB_DIALECT as Dialect,
- logging: env.DB_LOGGING,
- define: {
- timestamps: true,
- underscored: true
- },
- ...(env.DB_DIALECT === 'sqlite'
- ? {
- storage: env.DB_STORAGE
- }
- : env.DB_DIALECT === 'mysql'
- ? {
- host: env.MYSQL_HOST,
- port: env.MYSQL_PORT,
- database: env.MYSQL_DATABASE,
- username: env.MYSQL_USERNAME,
- password: env.MYSQL_PASSWORD,
- dialectOptions: env.DB_ENABLE_SSL
- ? {
- ssl: {
- require: true,
- rejectUnauthorized: false
- }
- }
- : {}
- }
- : env.DB_DIALECT === 'postgres'
- ? {
- host: env.POSTGRES_HOST,
- port: env.POSTGRES_PORT,
- database: env.POSTGRES_DATABASE,
- username: env.POSTGRES_USERNAME,
- password: env.POSTGRES_PASSWORD,
- dialectOptions: env.DB_ENABLE_SSL
- ? {
- ssl: {
- require: true,
- rejectUnauthorized: false
- }
- }
- : {}
- }
- : {})
-};
\ No newline at end of file
diff --git a/server/src/config/env.ts b/server/src/config/env.ts
deleted file mode 100644
index 7efba7a..0000000
--- a/server/src/config/env.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import dotenv from 'dotenv';
-import path from 'path';
-import logger from './logger';
-
-// load .env file
-dotenv.config({ path: path.resolve(process.cwd(), '.env') });
-
-// define environment variables interface
-interface EnvConfig {
- NODE_ENV: string;
- PORT: number;
- DB_DIALECT: 'sqlite' | 'mysql' | 'postgres';
- DB_STORAGE: string;
- DB_LOGGING: boolean;
- LOG_LEVEL: string;
-
- // MySQL config
- MYSQL_HOST: string;
- MYSQL_PORT: number;
- MYSQL_DATABASE: string;
- MYSQL_USERNAME: string;
- MYSQL_PASSWORD: string;
-
- // PostgreSQL config
- POSTGRES_HOST: string;
- POSTGRES_PORT: number;
- POSTGRES_DATABASE: string;
- POSTGRES_USERNAME: string;
- POSTGRES_PASSWORD: string;
-
- DB_ENABLE_SSL: boolean;
- // WebSocket or SSE
- SYNC_PROTOCOL?: 'websocket' | 'sse';
-}
-
-// get environment variable, use system environment variable first
-const getEnvValue = (key: string, defaultValue?: any): any => {
- return process.env[key] || defaultValue;
-};
-
-// environment variables configuration
-export const env: EnvConfig = {
- NODE_ENV: getEnvValue('NODE_ENV', 'development'),
- PORT: parseInt(getEnvValue('PORT', '3000')),
- DB_DIALECT: getEnvValue('DB_DIALECT', 'sqlite') as 'sqlite' | 'mysql' | 'postgres',
- DB_STORAGE: getEnvValue('DB_STORAGE', './data/sync-player.sqlite'),
- DB_LOGGING: getEnvValue('DB_LOGGING', 'false') === 'true',
- LOG_LEVEL: getEnvValue('LOG_LEVEL', 'info'),
-
- // MySQL config
- MYSQL_HOST: getEnvValue('MYSQL_HOST', 'localhost'),
- MYSQL_PORT: parseInt(getEnvValue('MYSQL_PORT', '3306')),
- MYSQL_DATABASE: getEnvValue('MYSQL_DATABASE', 'sync_player'),
- MYSQL_USERNAME: getEnvValue('MYSQL_USERNAME', 'root'),
- MYSQL_PASSWORD: getEnvValue('MYSQL_PASSWORD', 'password'),
-
- // PostgreSQL config
- POSTGRES_HOST: getEnvValue('POSTGRES_HOST', 'localhost'),
- POSTGRES_PORT: parseInt(getEnvValue('POSTGRES_PORT', '5432')),
- POSTGRES_DATABASE: getEnvValue('POSTGRES_DATABASE', 'sync_player'),
- POSTGRES_USERNAME: getEnvValue('POSTGRES_USERNAME', 'postgres'),
- POSTGRES_PASSWORD: getEnvValue('POSTGRES_PASSWORD', 'password'),
-
- DB_ENABLE_SSL: getEnvValue('DB_ENABLE_SSL', 'false') === 'true',
- // WebSocket or SSE
- SYNC_PROTOCOL: getEnvValue('SYNC_PROTOCOL', 'websocket'),
-};
-
-// validate required environment variables
-const validateEnv = () => {
- const requiredEnvs: Array = [
- 'NODE_ENV',
- 'PORT',
- 'DB_DIALECT',
- ];
-
- // 添加 MySQL 必需的环境变量验证
- if (env.DB_DIALECT === 'mysql') {
- requiredEnvs.push(
- 'MYSQL_HOST',
- 'MYSQL_PORT',
- 'MYSQL_DATABASE',
- 'MYSQL_USERNAME',
- 'MYSQL_PASSWORD'
- );
- } else if (env.DB_DIALECT === 'postgres') {
- requiredEnvs.push(
- 'POSTGRES_HOST',
- 'POSTGRES_PORT',
- 'POSTGRES_DATABASE',
- 'POSTGRES_USERNAME',
- 'POSTGRES_PASSWORD'
- );
- } else if (env.DB_DIALECT === 'sqlite') {
- requiredEnvs.push('DB_STORAGE');
- } else {
- logger.error('Unsupported DB_DIALECT:', env.DB_DIALECT);
- process.exit(1);
- }
-
- const missingEnvs = requiredEnvs.filter(key => !env[key]);
-
- if (missingEnvs.length > 0) {
- logger.error(`Missing required environment variables: ${missingEnvs.join(', ')}`);
- process.exit(1);
- }
-};
-
-validateEnv();
-
-export default env;
\ No newline at end of file
diff --git a/server/src/config/logger.ts b/server/src/config/logger.ts
deleted file mode 100644
index a327f4b..0000000
--- a/server/src/config/logger.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import winston from 'winston';
-import path from 'path';
-import env from './env';
-
-// define log levels
-const levels = {
- error: 0,
- warn: 1,
- info: 2,
- debug: 3,
-};
-
-// create log format
-const logFormat = winston.format.combine(
- winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
- winston.format.printf(({ timestamp, level, message }) => {
- return `[${timestamp}] ${level.toUpperCase()}: ${message}`;
- })
-);
-
-// console format (with color but retains timestamp)
-const consoleFormat = winston.format.combine(
- winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
- winston.format.colorize({ all: true }),
- winston.format.printf(({ timestamp, level, message }) => {
- return `[${timestamp}] ${level}: ${message}`;
- })
-);
-
-// create logger instance
-const logger = winston.createLogger({
- level: env.LOG_LEVEL,
- levels,
- transports: [
- // console output
- new winston.transports.Console({
- format: consoleFormat
- }),
- // error log file
- new winston.transports.File({
- filename: path.join('logs', 'error.log'),
- level: 'error',
- format: logFormat
- }),
- // all log file
- new winston.transports.File({
- filename: path.join('logs', 'all.log'),
- format: logFormat
- }),
- ],
-});
-
-export default logger;
\ No newline at end of file
diff --git a/server/src/db/connection.ts b/server/src/db/connection.ts
deleted file mode 100644
index 868adf7..0000000
--- a/server/src/db/connection.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { Sequelize } from 'sequelize';
-import { dbConfig } from '../config/database';
-
-const sequelize = new Sequelize(dbConfig);
-
-export default sequelize;
\ No newline at end of file
diff --git a/server/src/db/init.ts b/server/src/db/init.ts
deleted file mode 100644
index 32c8532..0000000
--- a/server/src/db/init.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import sequelize from './connection';
-import Room from '../models/Room';
-import User from '../models/User';
-import RoomMember from '../models/RoomMember';
-import PlaylistItem from '../models/PlaylistItem';
-import VideoSource from '../models/VideoSource';
-import logger from '../config/logger';
-
-// set model relations
-Room.belongsToMany(User, { through: RoomMember });
-User.belongsToMany(Room, { through: RoomMember });
-
-// playlist association
-Room.hasMany(PlaylistItem, { foreignKey: 'roomId' });
-PlaylistItem.belongsTo(Room, { foreignKey: 'roomId' });
-
-// video source association
-PlaylistItem.hasMany(VideoSource, { foreignKey: 'playlistItemId' });
-VideoSource.belongsTo(PlaylistItem, { foreignKey: 'playlistItemId' });
-
-export async function initDatabase() {
- try {
- await sequelize.authenticate();
- logger.info('Success to connect database');
-
- // sync all models to database
- await sequelize.sync({ force: false }); // note: don't use force: true in production
- logger.info('Sync database models');
- } catch (error) {
- logger.error('Failed to init database:', error);
- throw error;
- }
-}
\ No newline at end of file
diff --git a/server/src/db/queries/playlist.ts b/server/src/db/queries/playlist.ts
deleted file mode 100644
index b6ee18b..0000000
--- a/server/src/db/queries/playlist.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import PlaylistItem, { PlayStatus } from '../../models/PlaylistItem';
-import VideoSource from '../../models/VideoSource';
-import { Transaction } from 'sequelize';
-
-
-export async function addItemToPlaylist(roomId: number, title: string, urls: string, transaction?: Transaction) {
- const maxOrderIndex = await PlaylistItem.max('orderIndex', { where: { roomId }, transaction }) as number | null;
- const orderIndex = maxOrderIndex ? maxOrderIndex + 1 : 0;
-
- const playlistItem = await PlaylistItem.create({ roomId, title, orderIndex, playStatus: PlayStatus.NEW, createdTime: new Date() }, { transaction });
- const playlistItemId = playlistItem.id;
-
- const urlList = urls.split(',');
- for (const url of urlList) {
- await createVideoSource(playlistItemId, url);
- }
- return playlistItemId;
-}
-
-async function createVideoSource(playlistItemId: number, url: string) {
- return VideoSource.create({ playlistItemId, url });
-}
-
-export async function queryPlaylistItems(roomId: number, playlistItemId?: number, playStatus?: PlayStatus) {
- const whereClause: any = { roomId };
- if (playlistItemId) {
- whereClause.id = playlistItemId;
- }
- if (playStatus) {
- whereClause.playStatus = playStatus;
- }
- const items = await PlaylistItem.findAll({
- where: whereClause,
- include: [VideoSource],
- order: [['orderIndex', 'ASC']]
- });
-
- return items;
-}
-export async function deletePlaylistItem(playlistItemId: number) {
- await PlaylistItem.destroy({ where: { id: playlistItemId } });
- await VideoSource.destroy({ where: { playlistItemId } });
-}
-
-export async function clearPlaylist(roomId: number) {
- const playlistIds = (await PlaylistItem.findAll({ where: { roomId } })).map((item) => item.id);
- playlistIds.forEach(async (playlistItemId) => {
- await VideoSource.destroy({ where: { playlistItemId } });
- });
- await PlaylistItem.destroy({ where: { roomId } });
-}
-
-export async function updatePlaylistItem(playlistItemId: number, title?: string, urls?: string, orderIndex?: number) {
- if (!title && !urls && !orderIndex) {
- return;
- }
- if (orderIndex !== undefined) {
- await PlaylistItem.update({ orderIndex }, { where: { id: playlistItemId } });
- }
- if (title) {
- await PlaylistItem.update({ title }, { where: { id: playlistItemId } });
- }
- if (urls) {
- await VideoSource.destroy({ where: { playlistItemId } }); // delete old video sources
- createVideoSource(playlistItemId, urls);
- }
-}
-
-export async function updatePlayStatus(id: number, playStatus: PlayStatus) {
- await PlaylistItem.update({ playStatus }, { where: { id } });
-}
\ No newline at end of file
diff --git a/server/src/db/queries/room.ts b/server/src/db/queries/room.ts
deleted file mode 100644
index d8decc5..0000000
--- a/server/src/db/queries/room.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import Room from '../../models/Room';
-import { Transaction } from 'sequelize';
-
-export async function createRoom(name: string, password?: string, transaction?: Transaction) {
- // TODO if password is provided, hash it
- const passwordHash = password ? password : null;
-
- return Room.create({
- name,
- passwordHash,
- createdTime: new Date(),
- lastActiveTime: new Date()
- }, { transaction });
-}
-
-export async function getRoomById(id: number) {
- return Room.findByPk(id);
-}
-
-export async function getRoomByName(name: string) {
- return Room.findOne({ where: { name } });
-}
-
-export async function verifyRoomPassword(room: Room, password: string) {
- if (!room.passwordHash) {
- return true;
- }
- return password === room.passwordHash;
-}
diff --git a/server/src/db/queries/roomMember.ts b/server/src/db/queries/roomMember.ts
deleted file mode 100644
index 67428d4..0000000
--- a/server/src/db/queries/roomMember.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import RoomMember from '../../models/RoomMember';
-import User from '../../models/User';
-import { Transaction } from 'sequelize';
-
-export async function addMemberToRoom(
- roomId: number,
- userId: number,
- isAdmin: boolean = false,
- canGrantAdmin: boolean = false,
- transaction?: Transaction
-) {
- return RoomMember.create({
- roomId,
- userId,
- isAdmin,
- canGrantAdmin
- }, { transaction });
-}
-
-export async function removeMemberFromRoom(roomId: number, userId: number, transaction?: Transaction) {
- return RoomMember.destroy({
- where: {
- roomId,
- userId
- },
- transaction
- });
-}
-
-export async function getRoomMember(roomId: number, userId: number) {
- return RoomMember.findOne({
- where: {
- roomId,
- userId
- }
- });
-}
-
-export async function setMemberOnline(roomId: number, userId: number, online: boolean) {
- return RoomMember.update({ online }, {
- where: {
- roomId,
- userId
- }
- });
-}
-
-export async function getOnlineUsers(roomId: number) {
- // join RoomMember with User and select only online users
- // return RoomMember and username, map to UserListItem
- const members = await RoomMember.findAll({
- where: {
- roomId,
- online: true
- },
- include: [{
- model: User,
- attributes: ['username']
- }]
- });
- return members.map((member) => ({
- id: member.userId,
- username: member.User?.username,
- online: member.online,
- isAdmin: member.isAdmin,
- }));
-}
\ No newline at end of file
diff --git a/server/src/db/queries/roomPlayStatus.ts b/server/src/db/queries/roomPlayStatus.ts
deleted file mode 100644
index 7a9bcdb..0000000
--- a/server/src/db/queries/roomPlayStatus.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import RoomPlayStatus from '../../models/RoomPlayStatus';
-import { Transaction } from 'sequelize';
-
-export async function createRoomPlayStatus(
- roomId: number,
- paused: boolean = true,
- time: number = 0,
- timestamp: number = Date.now(),
- videoId: number = 0,
- transaction?: Transaction
-) {
- return RoomPlayStatus.create({
- roomId,
- paused,
- time,
- timestamp,
- videoId
- }, { transaction });
-}
-
-export async function getRoomPlayStatus(roomId: number) {
- return RoomPlayStatus.findOne({
- where: { roomId }
- });
-}
-
-export async function updateRoomPlayStatus(
- roomId: number,
- data: {
- paused?: boolean;
- time?: number;
- timestamp?: number;
- videoId?: number;
- },
- transaction?: Transaction
-) {
- const playStatus = await getRoomPlayStatus(roomId);
- if (!playStatus) {
- return createRoomPlayStatus(
- roomId,
- data.paused,
- data.time,
- data.timestamp,
- data.videoId,
- transaction
- );
- }
-
- return RoomPlayStatus.update(data, {
- where: { roomId },
- transaction
- });
-}
-
-export async function deleteRoomPlayStatus(roomId: number, transaction?: Transaction) {
- return RoomPlayStatus.destroy({
- where: { roomId },
- transaction
- });
-}
diff --git a/server/src/db/queries/user.ts b/server/src/db/queries/user.ts
deleted file mode 100644
index e38280c..0000000
--- a/server/src/db/queries/user.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import User from '../../models/User';
-import { Transaction } from 'sequelize';
-
-export async function createUser(username: string, password?: string, transaction?: Transaction) {
- // TODO: if password is provided, hash it
- const passwordHash = password ? password : null;
-
- return User.create({
- username,
- passwordHash,
- createdTime: new Date(),
- lastActiveTime: new Date()
- }, { transaction });
-}
-
-export async function getUserByUsername(username: string) {
- return User.findOne({ where: { username } });
-}
-
-export async function getUserById(id: number) {
- return User.findByPk(id);
-}
\ No newline at end of file
diff --git a/server/src/models/PlaylistItem.ts b/server/src/models/PlaylistItem.ts
deleted file mode 100644
index 65f8103..0000000
--- a/server/src/models/PlaylistItem.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Model, DataTypes } from 'sequelize';
-import sequelize from '../db/connection';
-import Room from './Room';
-
-export enum PlayStatus {
- NEW = 'new',
- PLAYING = 'playing',
- FINISHED = 'finished'
-}
-
-class PlaylistItem extends Model {
- declare id: number;
- declare roomId: number;
- declare title: string;
- declare orderIndex: number;
- declare playStatus: PlayStatus;
- declare createdTime: Date;
-}
-
-PlaylistItem.init({
- id: {
- type: DataTypes.INTEGER,
- primaryKey: true,
- autoIncrement: true
- },
- roomId: {
- type: DataTypes.INTEGER,
- allowNull: false,
- references: {
- model: Room,
- key: 'id'
- }
- },
- title: {
- type: DataTypes.STRING(255),
- allowNull: false
- },
- orderIndex: {
- type: DataTypes.INTEGER,
- allowNull: false
- },
- playStatus: {
- type: DataTypes.ENUM(...Object.values(PlayStatus)),
- allowNull: false,
- defaultValue: PlayStatus.NEW
- },
- createdTime: {
- type: DataTypes.DATE(3),
- allowNull: false,
- defaultValue: DataTypes.NOW
- }
-}, {
- sequelize,
- modelName: 'PlaylistItem',
- tableName: 'playlist_items',
- timestamps: false
-});
-
-export default PlaylistItem;
\ No newline at end of file
diff --git a/server/src/models/Room.ts b/server/src/models/Room.ts
deleted file mode 100644
index 993a754..0000000
--- a/server/src/models/Room.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Model, DataTypes } from 'sequelize';
-import sequelize from '../db/connection';
-
-class Room extends Model {
- declare id: number;
- declare name: string;
- declare passwordHash: string | null;
- declare createdTime: Date;
- declare lastActiveTime: Date;
-}
-
-Room.init({
- id: {
- type: DataTypes.INTEGER,
- primaryKey: true,
- autoIncrement: true
- },
- name: {
- type: DataTypes.STRING(100),
- allowNull: false
- },
- passwordHash: {
- type: DataTypes.STRING(255),
- allowNull: true
- },
- createdTime: {
- type: DataTypes.DATE(3),
- allowNull: false,
- defaultValue: DataTypes.NOW
- },
- lastActiveTime: {
- type: DataTypes.DATE(3),
- allowNull: false,
- defaultValue: DataTypes.NOW
- }
-}, {
- sequelize,
- modelName: 'Room',
- tableName: 'rooms',
- timestamps: false
-});
-
-export default Room;
\ No newline at end of file
diff --git a/server/src/models/RoomMember.ts b/server/src/models/RoomMember.ts
deleted file mode 100644
index 7c552ad..0000000
--- a/server/src/models/RoomMember.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Model, DataTypes } from 'sequelize';
-import sequelize from '../db/connection';
-import Room from './Room';
-import User from './User';
-
-class RoomMember extends Model {
- declare id: number;
- declare roomId: number;
- declare userId: number;
- declare isAdmin: boolean;
- declare canGrantAdmin: boolean;
- declare online: boolean;
- declare User?: User;
-}
-
-RoomMember.init({
- id: {
- type: DataTypes.INTEGER,
- primaryKey: true,
- autoIncrement: true
- },
- roomId: {
- type: DataTypes.INTEGER,
- allowNull: false,
- references: {
- model: Room,
- key: 'id'
- }
- },
- userId: {
- type: DataTypes.INTEGER,
- allowNull: false,
- references: {
- model: User,
- key: 'id'
- }
- },
- isAdmin: {
- type: DataTypes.BOOLEAN,
- defaultValue: false
- },
- canGrantAdmin: {
- type: DataTypes.BOOLEAN,
- defaultValue: false
- },
- online: {
- type: DataTypes.BOOLEAN,
- defaultValue: false
- }
-}, {
- sequelize,
- modelName: 'RoomMember',
- tableName: 'room_members',
- timestamps: false
-});
-
-RoomMember.belongsTo(User, { foreignKey: 'userId' });
-
-export default RoomMember;
\ No newline at end of file
diff --git a/server/src/models/RoomPlayStatus.ts b/server/src/models/RoomPlayStatus.ts
deleted file mode 100644
index bb1376b..0000000
--- a/server/src/models/RoomPlayStatus.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Model, DataTypes } from 'sequelize';
-import sequelize from '../db/connection';
-import Room from './Room';
-
-class RoomPlayStatus extends Model {
- declare id: number;
- declare roomId: number;
- declare paused: boolean; // true if the video is paused
- declare time: number; // current time in seconds
- declare timestamp: number; // timestamp of the last user action, in milliseconds
- declare videoId: number; // the video id
-}
-
-RoomPlayStatus.init({
- id: {
- type: DataTypes.INTEGER,
- primaryKey: true,
- autoIncrement: true
- },
- roomId: {
- type: DataTypes.INTEGER,
- allowNull: false,
- references: {
- model: Room,
- key: 'id'
- }
- },
- paused: {
- type: DataTypes.BOOLEAN,
- defaultValue: false
- },
- time: {
- type: DataTypes.DOUBLE,
- defaultValue: 0
- },
- timestamp: {
- type: DataTypes.BIGINT,
- defaultValue: 0
- },
- videoId: {
- type: DataTypes.INTEGER,
- defaultValue: 0
- }
-}, {
- sequelize,
- modelName: 'RoomPlayStatus',
- tableName: 'room_play_status',
- timestamps: false
-});
-
-export default RoomPlayStatus;
\ No newline at end of file
diff --git a/server/src/models/User.ts b/server/src/models/User.ts
deleted file mode 100644
index 26c6173..0000000
--- a/server/src/models/User.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Model, DataTypes } from 'sequelize';
-import sequelize from '../db/connection';
-
-class User extends Model {
- declare id: number;
- declare username: string;
- declare passwordHash: string | null;
- declare createdTime: Date;
- declare lastActiveTime: Date;
-}
-
-User.init({
- id: {
- type: DataTypes.INTEGER,
- primaryKey: true,
- autoIncrement: true
- },
- username: {
- type: DataTypes.STRING(50),
- allowNull: false,
- unique: true
- },
- passwordHash: {
- type: DataTypes.STRING(255),
- allowNull: true
- },
- createdTime: {
- type: DataTypes.DATE(3),
- allowNull: false,
- defaultValue: DataTypes.NOW
- },
- lastActiveTime: {
- type: DataTypes.DATE(3),
- allowNull: false,
- defaultValue: DataTypes.NOW
- }
-}, {
- sequelize,
- modelName: 'User',
- tableName: 'users',
- timestamps: false
-});
-
-export default User;
\ No newline at end of file
diff --git a/server/src/models/VideoSource.ts b/server/src/models/VideoSource.ts
deleted file mode 100644
index 733dadd..0000000
--- a/server/src/models/VideoSource.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Model, DataTypes } from 'sequelize';
-import sequelize from '../db/connection';
-import PlaylistItem from './PlaylistItem';
-
-class VideoSource extends Model {
- declare id: number;
- declare playlistItemId: number;
- declare url: string;
- declare label: string;
- declare createdTime: Date;
- declare lastActiveTime: Date;
-}
-
-VideoSource.init({
- id: {
- type: DataTypes.INTEGER,
- primaryKey: true,
- autoIncrement: true
- },
- playlistItemId: {
- type: DataTypes.INTEGER,
- allowNull: false,
- references: {
- model: PlaylistItem,
- key: 'id'
- }
- },
- url: {
- type: DataTypes.STRING(255),
- allowNull: false
- },
- createdTime: {
- type: DataTypes.DATE(3),
- allowNull: false,
- defaultValue: DataTypes.NOW
- },
- lastActiveTime: {
- type: DataTypes.DATE(3),
- allowNull: false,
- defaultValue: DataTypes.NOW
- }
-}, {
- sequelize,
- modelName: 'VideoSource',
- tableName: 'video_sources',
- timestamps: false
-});
-
-export default VideoSource;
\ No newline at end of file
diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts
deleted file mode 100644
index 2b4c18a..0000000
--- a/server/src/routes/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Router } from 'express';
-import roomRouter from './room';
-import userRouter from './user';
-import playlistRouter from './playlist';
-import syncRouter from './sync';
-
-const router = Router();
-
-// health check
-router.get('/health', (req, res) => {
- res.json({ status: 'ok!' });
-});
-
-// register room routes
-router.use('/room', roomRouter);
-router.use('/user', userRouter);
-router.use('/playlist', playlistRouter);
-router.use('/sync', syncRouter);
-export default router;
\ No newline at end of file
diff --git a/server/src/routes/playlist.ts b/server/src/routes/playlist.ts
deleted file mode 100644
index 9a49a9a..0000000
--- a/server/src/routes/playlist.ts
+++ /dev/null
@@ -1,219 +0,0 @@
-import { Router, Request, Response } from 'express';
-import { addItemToPlaylist, queryPlaylistItems, deletePlaylistItem, clearPlaylist, updatePlaylistItem, updatePlayStatus } from '../db/queries/playlist';
-import { getRoomPlayStatus, updateRoomPlayStatus, createRoomPlayStatus } from '../db/queries/roomPlayStatus';
-import { PlayStatus } from '../models/PlaylistItem';
-import logger from '../config/logger';
-import { getSyncManager } from '../sync/syncManager';
-import { SyncMessage } from '../sync/types';
-
-const router = Router();
-
-router.post('/add', async (req: Request, res: Response) => {
- try {
- const { title, urls } = req.body;
-
- const cookiesJson = JSON.parse(req.cookies.userInfo);
- const roomId = cookiesJson.roomId;
- const userId = cookiesJson.userId;
- // validate roomId, title, urls
- if (!roomId || !title || !urls) {
- res.status(400).json({ error: 'Invalid request body' });
- return;
- }
- const playlistItemId = await addItemToPlaylist(roomId, title, urls);
-
- const data: SyncMessage = {
- type: 'updatePlaylist'
- };
- getSyncManager().broadcast(roomId, data, [userId]);
- res.json({
- message: 'Item added to playlist',
- playlistItemId
- });
- }
- catch (error) {
- logger.error('Failed to add playlist item:', error);
- res.status(500).json({ message: 'Internal server error' });
- }
-});
-
-router.get('/query', async (req: Request, res: Response) => {
- try {
- const cookiesJson = JSON.parse(req.cookies.userInfo);
- const roomId = cookiesJson.roomId;
- if (isNaN(roomId)) {
- res.status(400).json({ error: 'Invalid roomId' });
- return;
- }
-
- let playlistItemId: number | undefined;
- let playStatus: PlayStatus | undefined;
- if (req.query.playlistItemId) {
- playlistItemId = parseInt(req.query.playlistItemId as string);
- if (isNaN(playlistItemId)) {
- res.status(400).json({ error: 'Invalid playlistItemId' });
- return;
- }
- }
- if (req.query.playStatus) {
- playStatus = req.query.playStatus as PlayStatus;
- if (!Object.values(PlayStatus).includes(playStatus)) {
- res.status(400).json({ error: 'Invalid playStatus' });
- return;
- }
- }
-
- // if didn't specify playStatus, query PLAYING items and NEW items
- if (!playStatus) {
- const playingItems = await queryPlaylistItems(roomId, undefined, PlayStatus.PLAYING);
- const newItems = await queryPlaylistItems(roomId, undefined, PlayStatus.NEW);
- const items = playingItems.concat(newItems);
- res.json(items);
- }
- else {
- const items = await queryPlaylistItems(roomId, playlistItemId, playStatus);
- res.json(items);
- }
- }
- catch (error) {
- logger.error('Failed to query playlist items:', error);
- res.status(500).json({ message: 'Internal server error' });
- }
-});
-
-router.delete('/delete', async (req: Request, res: Response) => {
- try {
- const { playlistItemId } = req.body;
- const cookiesJson = JSON.parse(req.cookies.userInfo);
- const roomId = cookiesJson.roomId;
- const userId = cookiesJson.userId;
-
- if (!playlistItemId) {
- res.status(400).json({ error: 'Invalid request body' });
- return;
- }
-
- // delete playlist item and its video sources
- await deletePlaylistItem(playlistItemId);
-
- const data: SyncMessage = {
- type: 'updatePlaylist'
- };
- getSyncManager().broadcast(roomId, data, [userId]);
-
- res.json({ message: 'Item deleted from playlist' });
- }
- catch (error) {
- logger.error('Failed to delete playlist item:', error);
- res.status(500).json({ message: 'Internal server error' });
- }
-});
-
-router.delete('/clear', async (req: Request, res: Response) => {
- try {
- const cookiesJson = JSON.parse(req.cookies.userInfo);
- const roomId = cookiesJson.roomId;
- const userId = cookiesJson.userId;
-
- if (isNaN(roomId)) {
- res.status(400).json({ error: 'Invalid roomId' });
- return;
- }
-
- // clear playlist items and their video sources
- await clearPlaylist(roomId);
-
- const data: SyncMessage = {
- type: 'updatePlaylist'
- };
- getSyncManager().broadcast(roomId, data, [userId]);
-
- res.json({ message: 'Playlist cleared' });
- }
- catch (error) {
- logger.error('Failed to clear playlist:', error);
- res.status(500).json({ message: 'Internal server error' });
- }
-});
-
-router.post('/updateOrder', async (req: Request, res: Response) => {
- try {
- const { orderIndexList } = req.body; // array of { playlistItemId: number, orderIndex: number }
- const cookiesJson = JSON.parse(req.cookies.userInfo);
- const roomId = cookiesJson.roomId;
- const userId = cookiesJson.userId;
-
- if (!Array.isArray(orderIndexList)) {
- res.status(400).json({ error: 'Invalid request body' });
- return;
- }
-
- orderIndexList.forEach((item: any) => {
- if (typeof item.playlistItemId !== 'number' || typeof item.orderIndex !== 'number') {
- res.status(400).json({ error: 'Invalid request body' });
- return;
- }
- updatePlaylistItem(item.playlistItemId, undefined, undefined, item.orderIndex); // update orderIndex only
- });
-
-
- const data: SyncMessage = {
- type: 'updatePlaylist'
- };
- getSyncManager().broadcast(roomId, data, [userId]);
-
- res.json({ message: 'Order updated' });
- }
- catch (error) {
- logger.error('Failed to update order:', error);
- res.status(500).json({ message: 'Internal server error' });
- }
-});
-
-router.post('/switch', async (req: Request, res: Response) => {
- try {
- const cookiesJson = JSON.parse(req.cookies.userInfo);
- const roomId = cookiesJson.roomId;
- const userId = cookiesJson.userId;
- const { playlistItemId } = req.body;
-
- let broadcast = true;
- if (!playlistItemId) {
- res.status(400).json({ error: 'Invalid request body' });
- return;
- }
-
- // set all playing items to finished and the new item to playing
- const playingItems = await queryPlaylistItems(roomId, undefined, PlayStatus.PLAYING);
- playingItems.forEach(async (item) => {
- await updatePlayStatus(item.id, PlayStatus.FINISHED);
- if (item.id === playlistItemId) { // FIXME: return here?
- broadcast = false; // no need to broadcast if the new item is already playing
- }
- });
- await updatePlayStatus(playlistItemId, PlayStatus.PLAYING);
-
- // update room play status
- const playStatus = await getRoomPlayStatus(roomId);
- if (playStatus) {
- await updateRoomPlayStatus(roomId, { paused: false, time: 0, timestamp: Date.now(), videoId: playlistItemId });
- }
- else {
- await createRoomPlayStatus(roomId, false, 0, Date.now(), playlistItemId);
- }
-
- if (broadcast) {
- const data: SyncMessage = {
- type: 'updatePlaylist'
- };
- getSyncManager().broadcast(roomId, data, [userId]);
- }
-
- res.json({ message: 'Playlist item switched' });
- } catch (error) {
- logger.error('Failed to switch playlist item:', error);
- res.status(500).json({ message: 'Internal server error' });
- }
-});
-
-export default router;
\ No newline at end of file
diff --git a/server/src/routes/room.ts b/server/src/routes/room.ts
deleted file mode 100644
index 8ad4af1..0000000
--- a/server/src/routes/room.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import { Router, Request, Response } from 'express';
-import { createRoom, getRoomByName, verifyRoomPassword, getRoomById } from '../db/queries/room';
-import { addMemberToRoom, removeMemberFromRoom, getRoomMember, getOnlineUsers } from '../db/queries/roomMember';
-import { getUserById } from '../db/queries/user';
-import sequelize from '../db/connection';
-import logger from '../config/logger';
-
-const router = Router();
-
-
-router.post('/create', async (req: Request, res: Response): Promise => {
- try {
- const { name, password } = req.body;
-
- // check if room name already exists
- const existingRoom = await getRoomByName(name);
- if (existingRoom) {
- res.json({
- id: existingRoom.id,
- name: existingRoom.name,
- createdTime: existingRoom.createdTime
- });
- return;
- }
-
- // create new room
- const room = await createRoom(name, password);
-
- res.json({
- id: room.id,
- name: room.name,
- createdTime: room.createdTime
- });
- } catch (error) {
- logger.error('Failed to create room:', error);
- res.status(500).json({ error: 'Internal server error' });
- }
-});
-
-router.get('/query', async (req: Request, res: Response): Promise => {
- try {
- const { name } = req.query;
- const room = await getRoomByName(name as string);
- if (!room) {
- res.status(404).json({ error: 'Room not found' });
- return;
- }
- res.json(room);
- } catch (error) {
- res.status(500).json({ error: 'Internal server error' });
- }
-});
-
-router.post('/join', async (req: Request, res: Response): Promise => {
- const transaction = await sequelize.transaction();
-
- try {
- const { roomId, userId, password } = req.body;
-
- // check if user exists
- const user = await getUserById(userId);
- if (!user) {
- res.status(404).json({ error: 'User not found' });
- return;
- }
-
- // check if room exists
- const room = await getRoomById(roomId);
- if (!room) {
- res.status(404).json({ error: 'Room not found' });
- return;
- }
-
- // check if password is valid
- const isPasswordValid = await verifyRoomPassword(room, password || '');
- if (!isPasswordValid) {
- res.status(401).json({ error: 'Invalid password' });
- return;
- }
-
- // check if user is already in the room
- const existingMember = await getRoomMember(roomId, userId);
- if (existingMember) {
- res.status(400).json({ error: 'User is already in the room' });
- return;
- }
-
- // add user to room
- const member = await addMemberToRoom(roomId, userId, false, false, transaction);
-
- await transaction.commit();
- // TODO: set cookie
- res.json({
- roomId: member.roomId,
- userId: member.userId,
- isAdmin: member.isAdmin,
- canGrantAdmin: member.canGrantAdmin
- });
- } catch (error) {
- await transaction.rollback();
- logger.error('Failed to join room:', error);
- res.status(500).json({ error: 'Internal server error' });
- }
-});
-
-router.post('/leave', async (req: Request, res: Response): Promise => {
- try {
- const { roomId, userId } = req.body;
-
- // check if user is in the room
- const member = await getRoomMember(roomId, userId);
- if (!member) {
- res.status(404).json({ error: 'User is not in the room' });
- return;
- }
-
- // remove user from room
- try {
- await removeMemberFromRoom(roomId, userId);
- } catch (error) {
- logger.error('Failed to remove member from room:', error);
- res.status(500).json({ error: 'Internal server error' });
- return;
- }
-
- res.json({ message: 'Successfully left the room' });
- } catch (error) {
- logger.error('Failed to leave room:', error);
- res.status(500).json({ error: 'Internal server error' });
- }
-});
-
-router.get('/queryOnlineUsers', async (req: Request, res: Response) => {
- try {
- const roomId = Number(req.query.roomId);
- const members = await getOnlineUsers(roomId);
- res.json(members);
- } catch (error) {
- logger.error('Failed to query online users:', error);
- res.status(500).json({ error: 'Internal server error' });
- }
-});
-
-export default router;
\ No newline at end of file
diff --git a/server/src/routes/sync.ts b/server/src/routes/sync.ts
deleted file mode 100644
index 5b099dd..0000000
--- a/server/src/routes/sync.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { Router, Request, Response } from 'express';
-import logger from '../config/logger';
-import { getSyncManager } from '../sync/syncManager';
-import { getRoomPlayStatus, updateRoomPlayStatus, createRoomPlayStatus } from '../db/queries/roomPlayStatus';
-import env from '../config/env';
-
-const router = Router();
-
-router.post('/updateTime', async (req: Request, res: Response) => {
- try {
- // update playStatus in server
- const cookiesJson = JSON.parse(req.cookies.userInfo);
- const roomId = cookiesJson.roomId;
- const userId = cookiesJson.userId; // TODO: check if the user is admin
- const { time, timestamp, videoId } = req.body;
- logger.info(`sync updateTime: roomId=${roomId}, userId=${userId}, time=${time}, timestamp=${timestamp}, videoId=${videoId}`);
- if (!time || !timestamp || !videoId) {
- res.status(400).json({ error: 'Invalid request body' });
- return;
- }
- const playStatus = await getRoomPlayStatus(roomId);
- if (!playStatus) {
- await createRoomPlayStatus(roomId, false, time, timestamp, videoId);
- } else {
- await updateRoomPlayStatus(roomId, { paused: false, time, timestamp, videoId });
- }
-
- const syncManager = getSyncManager();
- syncManager.broadcast(roomId, {
- type: 'updateTime',
- payload: { roomId, userId, paused: false, time, timestamp, videoId }
- }, [userId]);
-
- res.json({ message: 'Play status updated' });
- } catch (error) {
- logger.error('Failed to update play status:', error);
- res.status(404).json({ error: 'Play status not found' });
- }
-});
-
-router.get('/query', async (req: Request, res: Response) => {
- try {
- const cookiesJson = JSON.parse(req.cookies.userInfo);
- const roomId = cookiesJson.roomId;
- logger.info(`sync query: roomId=${roomId}`);
- if (!roomId) {
- res.status(400).json({ error: 'Invalid roomId' });
- return;
- }
-
- const playStatus = await getRoomPlayStatus(roomId);
- if (!playStatus) {
- res.status(404).json({ error: 'Play status not found' });
- return;
- }
- const now = Date.now();
- const timeDiff = now - playStatus.timestamp;
- if (!playStatus.paused) { // if the video is playing, update the time
- playStatus.time += timeDiff / 1000;
- playStatus.timestamp = now;
- }
- // console.log(playStatus);
- res.json(playStatus);
- } catch (error) {
- logger.error('Failed to query play status:', error);
- res.status(404).json({ error: 'Play status not found' });
- }
-});
-
-router.post('/updatePause', async (req: Request, res: Response) => {
- try {
- const cookiesJson = JSON.parse(req.cookies.userInfo);
- const roomId = cookiesJson.roomId;
- const userId = cookiesJson.userId; // TODO: check if the user is admin
-
- const { paused, timestamp } = req.body;
- logger.info(`sync updatePause: roomId=${roomId}, userId=${userId}, paused=${paused}, timestamp=${timestamp}`);
- if (paused === undefined || !timestamp) {
- res.status(400).json({ error: 'Invalid request body' });
- return;
- }
- const playStatus = await getRoomPlayStatus(roomId);
- if (!playStatus) {
- await createRoomPlayStatus(roomId, paused, 0, timestamp, 0);
- } else {
- await updateRoomPlayStatus(roomId, { paused, timestamp });
- }
-
- const syncManager = getSyncManager();
- syncManager.broadcast(roomId, {
- type: 'updatePause',
- payload: { roomId, userId, paused, timestamp }
- }, [userId]);
-
- res.json({ message: 'Play status updated' });
- } catch (error) {
- logger.error('Failed to update play status:', error);
- res.status(404).json({ error: 'Play status not found' });
- }
-});
-
-router.get('/protocol', async (req: Request, res: Response) => {
- try {
- res.json({ protocol: env.SYNC_PROTOCOL });
- }
- catch (error) {
- logger.error('Failed to get protocol:', error);
- res.status(500).json({ message: 'Internal server error' });
- }
-});
-
-export default router;
\ No newline at end of file
diff --git a/server/src/routes/user.ts b/server/src/routes/user.ts
deleted file mode 100644
index b8dc7b8..0000000
--- a/server/src/routes/user.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Router, Request, Response } from 'express';
-import { createUser, getUserByUsername } from '../db/queries/user';
-import logger from '../config/logger';
-
-const router = Router();
-
-router.post('/login', async (req: Request, res: Response): Promise => {
- try {
- const { username, password } = req.body;
-
- // check if username already exists
- const existingUser = await getUserByUsername(username);
- if (existingUser) {
- res.json({
- id: existingUser.id,
- username: existingUser.username,
- createdTime: existingUser.createdTime
- });
- return;
- }
-
- // create new user
- const user = await createUser(username, password);
-
- res.json({
- id: user.id,
- username: user.username,
- createdTime: user.createdTime
- });
- } catch (error) {
- logger.error('Failed to create user:', error);
- res.status(500).json({ error: 'Internal server error' });
- }
-});
-
-router.get('/query', async (req: Request, res: Response): Promise => {
- try {
- const { username } = req.query;
- const user = await getUserByUsername(username as string);
- if (!user) {
- res.status(404).json({ error: 'User not found' });
- return;
- }
- res.json(user);
- } catch (error) {
- logger.error('Failed to query user:', error);
- res.status(500).json({ error: 'Internal server error' });
- }
-});
-
-export default router;
\ No newline at end of file
diff --git a/server/src/sync/adapters/sse.ts b/server/src/sync/adapters/sse.ts
deleted file mode 100644
index 61417fa..0000000
--- a/server/src/sync/adapters/sse.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-// use sse(Server-Sent Events) to sync data
-import { Router, Request, Response } from 'express';
-import { ISyncAdapter, SyncEventHandler, SyncMessage } from '../types';
-import logger from '../../config/logger';
-import { setMemberOnline } from '../../db/queries/roomMember';
-
-interface SSEClient {
- userId: number;
- roomId: number;
- response: Response;
-}
-
-interface SSEConnectionMap {
- [roomId: number]: {
- [userId: number]: SSEClient;
- };
-}
-
-const connections: SSEConnectionMap = {}; // FIXME: why is this not in the class?
-
-export class SSEAdapter implements ISyncAdapter {
- // private connections: SSEConnectionMap = {}; // FIXME: can't get connections
- private messageHandler: SyncEventHandler | null = null;
- private router: Router;
-
- constructor() {
- this.router = Router();
- this.setupRoutes();
- }
-
- private setupRoutes() {
- this.router.get('/connect', (req: Request, res: Response) => {
- const userId = Number(req.query.userId);
- const roomId = Number(req.query.roomId);
-
- if (!userId || !roomId) {
- res.status(400).json({ error: 'Missing userId or roomId' });
- return;
- }
-
- // set sse headers
- res.writeHead(200, {
- 'Content-Type': 'text/event-stream',
- 'Cache-Control': 'no-cache',
- 'Connection': 'keep-alive'
- });
-
- // create heartbeat to keep connection
- const heartbeat = setInterval(() => {
- res.write(':\n\n');
- }, 1000 * 30);
-
- // save connection
- if (!connections[roomId]) {
- logger.debug(`create ${roomId}`)
- connections[roomId] = {};
- }
- connections[roomId][userId] = { userId, roomId, response: res };
- const keys_in_room = Object.keys(connections[roomId]);
- logger.debug(`room ${roomId} has users: ${keys_in_room}`);
- setMemberOnline(roomId, userId, true)
-
- // send connected message
- this.sendEventToClient(res, 'connected', {});
- logger.info(`User ${userId} connected to room ${roomId} using SSE`);
-
- // handle connection close
- req.on('close', () => {
- clearInterval(heartbeat);
- if (connections[roomId]?.[userId]) {
- delete connections[roomId][userId];
- logger.info(`User ${userId} disconnected from room ${roomId}`);
- }
- setMemberOnline(roomId, userId, false)
- });
- });
- }
-
- private sendEventToClient(res: Response, type: string, data: any) {
- const result = {
- type,
- data
- };
- res.write(`data: ${JSON.stringify(result)}\n\n`);
- }
-
- private getClientsInRoom(roomId: number): SSEClient[] {
- // console.log(connections[roomId]);
- return Object.values(connections[roomId] || {});
- }
-
- broadcast(roomId: number, message: SyncMessage, excludedUserIds: number[] = []): void {
- const roomClients = this.getClientsInRoom(roomId);
- if (!roomClients || roomClients.length === 0) {
- logger.warn(`No clients found in room ${roomId}`);
- return;
- }
- for (const userId in roomClients) {
- if (!excludedUserIds.includes(Number(userId))) {
- logger.debug(`broadcast to ${userId}`);
- const client = roomClients[userId];
- this.sendEventToClient(client.response, message.type, message);
- }
- }
- }
-
- sendToUsers(roomId: number, userIds: number[], message: SyncMessage): void {
- const roomClients = connections[roomId] || {};
- for (const userId of userIds) {
- const client = roomClients[userId];
- if (client) {
- this.sendEventToClient(client.response, message.type, message);
- }
- }
- }
-
- onMessage(handler: SyncEventHandler): void {
- this.messageHandler = handler;
- }
-
- getUserIdsInRoom(roomId: number): number[] {
- return Object.keys(connections[roomId] || {}).map(Number);
- }
-
- getRouter(): Router {
- return this.router;
- }
-}
-
-// create and export router instance
-const sseAdapter = new SSEAdapter();
-export default sseAdapter.getRouter();
\ No newline at end of file
diff --git a/server/src/sync/adapters/websocket.ts b/server/src/sync/adapters/websocket.ts
deleted file mode 100644
index 1fbbc06..0000000
--- a/server/src/sync/adapters/websocket.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { WebSocketServer, WebSocket } from 'ws';
-import { Server } from 'http';
-import { ISyncAdapter, SyncEventHandler, SyncMessage } from '../types';
-import logger from '../../config/logger';
-import { setMemberOnline } from '../../db/queries/roomMember';
-
-interface WebSocketMap {
- [roomId: number]: {
- [userId: number]: WebSocket;
- };
-}
-
-export class WebSocketAdapter implements ISyncAdapter {
- private wss: WebSocketServer;
- private connections: WebSocketMap = {};
- private messageHandler: SyncEventHandler | null = null;
-
- constructor(server: Server) {
- this.wss = new WebSocketServer({ server });
- this.setupWebSocketServer();
- }
-
- private setupWebSocketServer() {
- this.wss.on('connection', (ws: WebSocket) => {
- logger.info('New client connected');
- ws.send(JSON.stringify({ type: 'connected' }));
-
- ws.on('message', (message: Buffer) => {
- this.handleMessage(ws, message);
- });
-
- ws.on('close', () => {
- this.handleClose(ws);
- });
- });
- }
-
- private handleMessage(ws: WebSocket, message: Buffer) {
- try {
- const data = JSON.parse(message.toString());
- if (data.type === 'auth') {
- const { userId, roomId } = data.payload;
- this.handleAuth(ws, userId, roomId);
- }
- if (this.messageHandler) {
- this.messageHandler(data);
- }
- } catch (error) {
- logger.error('Error parsing message:', error);
- }
- }
-
- private handleAuth(ws: WebSocket, userId: number, roomId: number) {
- if (!this.connections[roomId]) {
- this.connections[roomId] = {};
- }
- this.connections[roomId][userId] = ws;
- setMemberOnline(roomId, userId, true);
- logger.info(`User ${userId} connected to room ${roomId}`);
- }
-
- private handleClose(ws: WebSocket) {
- for (const roomId in this.connections) {
- for (const userId in this.connections[roomId]) {
- if (this.connections[roomId][userId] === ws) {
- delete this.connections[roomId][userId];
- setMemberOnline(parseInt(roomId), parseInt(userId), false)
- logger.info(`User ${userId} disconnected from room ${roomId}`);
- return;
- }
- }
- }
- }
-
- broadcast(roomId: number, message: SyncMessage, excludedUserIds: number[] = []): void {
- const roomConnections = this.connections[roomId] || {};
- for (const userId in roomConnections) {
- if (!excludedUserIds.includes(Number(userId))) {
- const ws = roomConnections[userId];
- ws.send(JSON.stringify(message));
- }
- }
- }
-
- sendToUsers(roomId: number, userIds: number[], message: SyncMessage): void {
- const roomConnections = this.connections[roomId] || {};
- for (const userId of userIds) {
- const ws = roomConnections[userId];
- if (ws) {
- ws.send(JSON.stringify(message));
- }
- }
- }
-
- onMessage(handler: SyncEventHandler): void {
- this.messageHandler = handler;
- }
-
- getUserIdsInRoom(roomId: number): number[] {
- return Object.keys(this.connections[roomId] || {}).map(Number);
- }
-}
\ No newline at end of file
diff --git a/server/src/sync/syncManager.ts b/server/src/sync/syncManager.ts
deleted file mode 100644
index d7a5a32..0000000
--- a/server/src/sync/syncManager.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { Server } from 'http';
-import { ISyncManager, ISyncAdapter, SyncMessage } from './types';
-import { WebSocketAdapter } from './adapters/websocket';
-import { SSEAdapter } from './adapters/sse';
-import logger from '../config/logger';
-
-export class SyncManager implements ISyncManager {
- private adapter: ISyncAdapter;
-
- constructor(server: Server, protocol: 'websocket' | 'sse' = 'websocket') {
- this.adapter = this.createAdapter(server, protocol);
-
- this.adapter.onMessage((data) => {
- logger.debug('Received message:', data);
- });
- }
-
- private createAdapter(server: Server, protocol: 'websocket' | 'sse'): ISyncAdapter {
- logger.info(`Creating adapter for protocol: ${protocol}`);
- switch (protocol) {
- case 'websocket':
- return new WebSocketAdapter(server);
- case 'sse':
- return new SSEAdapter();
- default:
- throw new Error(`Unsupported protocol: ${protocol}`);
- }
- }
-
- broadcast(roomId: number, message: SyncMessage, excludedUserIds: number[] = []): void {
- this.adapter.broadcast(roomId, message, excludedUserIds);
- }
-
- sendToUsers(roomId: number, userIds: number[], message: SyncMessage): void {
- this.adapter.sendToUsers(roomId, userIds, message);
- }
-
- getUserIdsInRoom(roomId: number): number[] {
- return this.adapter.getUserIdsInRoom(roomId);
- }
-}
-
-let syncManager: SyncManager;
-
-export function initSyncManager(server: Server, protocol: 'websocket' | 'sse' = 'websocket'): SyncManager {
- syncManager = new SyncManager(server, protocol);
- return syncManager;
-}
-
-export function getSyncManager(): SyncManager {
- if (!syncManager) {
- throw new Error('SyncManager not initialized');
- }
- return syncManager;
-}
\ No newline at end of file
diff --git a/server/src/sync/types.ts b/server/src/sync/types.ts
deleted file mode 100644
index a29fc7d..0000000
--- a/server/src/sync/types.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export interface SyncMessage {
- type: string;
- payload?: any;
-}
-
-export type SyncEventHandler = (data: any) => void;
-
-export interface ISyncAdapter {
- broadcast(roomId: number, message: SyncMessage, excludedUserIds?: number[]): void;
- sendToUsers(roomId: number, userIds: number[], message: SyncMessage): void;
- onMessage(handler: SyncEventHandler): void;
- getUserIdsInRoom(roomId: number): number[];
-}
-
-export interface ISyncManager {
- broadcast(roomId: number, message: SyncMessage, excludedUserIds?: number[]): void;
- sendToUsers(roomId: number, userIds: number[], message: SyncMessage): void;
- getUserIdsInRoom(roomId: number): number[];
-}
\ No newline at end of file
diff --git a/server/src/websocket/index.ts b/server/src/websocket/index.ts
deleted file mode 100644
index 264732b..0000000
--- a/server/src/websocket/index.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { WebSocketServer, WebSocket } from 'ws';
-import { Server } from 'http';
-import logger from '../config/logger';
-import { getRoomPlayStatus, deleteRoomPlayStatus } from '../db/queries/roomPlayStatus';
-import { setMemberOnline } from '../db/queries/roomMember';
-
-interface WebSocketMap {
- [roomId: number]: {
- [userId: number]: WebSocket;
- };
-}
-
-let wss: WebSocketServer;
-const connections: WebSocketMap = {};
-
-export function initWebSocket(server: Server) {
- wss = new WebSocketServer({ server });
-
- wss.on('connection', (ws: WebSocket) => {
- logger.info('New client connected');
-
- ws.send(JSON.stringify({ type: 'connected' }));
- ws.on('message', (message: Buffer) => {
- logger.debug('Received message:', message.toString());
- try {
- const data = JSON.parse(message.toString());
-
- if (data.type === 'auth') { // TODO: Implement real authentication
- const { userId, roomId } = data.payload;
- if (userId && roomId) {
- if (!connections[roomId]) { // Create a new room
- connections[roomId] = {};
- }
- connections[roomId][userId] = ws;
-
- // 广播用户列表更新
- setMemberOnline(parseInt(roomId), parseInt(userId), true)
- const data = {
- type: 'updateUserList',
- roomId: parseInt(roomId),
- }
- broadcast(parseInt(roomId), data);
- }
- logger.info(`User ${userId} connected to room ${roomId} using websocket`);
- }
- else if (data.type === 'ping') {
- ws.send(JSON.stringify({ type: 'pong' }));
- }
- } catch (error) {
- logger.error('Error parsing message:', error);
- }
- });
-
- ws.on('close', () => { // FIXME: a better way to handle this instead searching through all connections
- for (const roomId in connections) {
- for (const userId in connections[roomId]) {
- if (connections[roomId][userId] === ws) {
- delete connections[roomId][userId];
- logger.info(`User ${userId} disconnected`);
- // if the room is empty, delete the room and the play status
- // if (Object.keys(connections[roomId]).length === 0) {
- // delete connections[roomId];
- // deleteRoomPlayStatus(Number(roomId))
- // .then(() => {
- // logger.info(`Room ${roomId} deleted`);
- // })
- // .catch((error) => {
- // logger.error(`Failed to delete room ${roomId}:`, error);
- // });
- // }
-
- // 广播用户列表更新
- setMemberOnline(parseInt(roomId), parseInt(userId), false)
- const data = {
- type: 'updateUserList',
- roomId: parseInt(roomId),
- }
- broadcast(parseInt(roomId), data);
- break;
- }
- }
- }
- });
- });
-
- return wss;
-}
-
-
-export function getWebSocketServer(): WebSocketServer {
- return wss;
-}
-
-export function getUserIdsInRoom(roomId: number): number[] {
- return Object.keys(connections[roomId] || {}).map(Number);
-}
-
-// Broadcast data to all users in a room, except the excluded users
-export function broadcast(roomId: number, data: any, excludedUserIds: number[] = []) {
- const roomConnections = connections[roomId] || {};
- for (const userId in roomConnections) {
- if (!excludedUserIds.includes(Number(userId))) {
- const ws = roomConnections[userId];
- ws.send(JSON.stringify(data));
- }
- }
-}
-
-// Send data to include users in a room
-export function sendToUsers(roomId: number, userIds: number[], data: any) {
- const roomConnections = connections[roomId] || {};
- for (const userId of userIds) {
- const ws = roomConnections[userId];
- if (ws) {
- ws.send(JSON.stringify(data));
- }
- }
-}
\ No newline at end of file
diff --git a/server/tsconfig.json b/server/tsconfig.json
deleted file mode 100644
index e2d9950..0000000
--- a/server/tsconfig.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "compilerOptions": {
- "target": "es6",
- "module": "commonjs",
- "outDir": "./dist",
- "rootDir": "./src",
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true,
- "forceConsistentCasingInFileNames": true,
- "moduleResolution": "node",
- "baseUrl": "./src",
- "paths": {
- "@/*": ["*"]
- }
- },
- "include": ["src/**/*"],
- "exclude": ["node_modules"]
-}