diff --git a/player/.env.example b/player/.env.example index e38473f..42f5eb1 100644 --- a/player/.env.example +++ b/player/.env.example @@ -1,5 +1,5 @@ # API配置 -VITE_API_BASE_URL=http://localhost:3000 +VITE_API_BASE_URL=http://localhost:3000/api VITE_WS_BASE_URL=ws://localhost:3000 # 调试配置 diff --git a/player/src/components/AddPlaylist.vue b/player/src/components/AddPlaylist.vue index 971a05f..bb7a4a4 100644 --- a/player/src/components/AddPlaylist.vue +++ b/player/src/components/AddPlaylist.vue @@ -49,7 +49,7 @@ const playlistStore = usePlaylistStore(); const title = ref(''); const url = ref(''); -function addVideo() { +async function addVideo() { logger.info('Add video'); if (!title.value || !url.value) { logger.error('Title or url is empty'); @@ -59,7 +59,24 @@ function addVideo() { logger.error('RoomId is empty'); return; } - playlistStore.addVideo(userStore.roomId, title.value, url.value); + + try { + // 将 URL 字符串转换为 sources 数组格式 + const sources = [{ + url: url.value, + label: '默认' + }]; + + await playlistStore.addVideo(userStore.roomId, title.value, sources); + + // 清空输入框 + title.value = ''; + url.value = ''; + + logger.info('Video added successfully'); + } catch (error) { + logger.error('Failed to add video:', error); + } } onMounted(() => { logger.info('AddPlaylist mounted'); diff --git a/player/src/components/Playing.vue b/player/src/components/Playing.vue index d179095..3fcda9f 100644 --- a/player/src/components/Playing.vue +++ b/player/src/components/Playing.vue @@ -18,9 +18,9 @@ - @@ -111,15 +111,15 @@ function onFileSelected(event: Event) { // 计算当前视频源 const currentSource = computed(() => { - return playlistStore.currentVideoItem?.VideoSources[0]?.url; + return playlistStore.currentVideoItem?.videoSources[0]?.url; }); // 监听当前视频变化,更新选中的源 // watch(() => playlistStore.currentVideoId, (newId) => { // if (newId && playlistStore.currentVideoItem) { // selectedSourceIndex.value = '0'; -// if (playlistStore.currentVideoItem.VideoSources.length > 0) { -// playerStore.updateSource(playlistStore.currentVideoItem.VideoSources[0].url); +// if (playlistStore.currentVideoItem.videoSources.length > 0) { +// playerStore.updateSource(playlistStore.currentVideoItem.videoSources[0].url); // } // } // }, { immediate: true }); @@ -127,8 +127,8 @@ const currentSource = computed(() => { // 监听选中源的变化 watch(selectedSourceIndex, (newIndex) => { const index = parseInt(newIndex); - if (playlistStore.currentVideoItem?.VideoSources[index]) { - const newSource = playlistStore.currentVideoItem.VideoSources[index].url; + if (playlistStore.currentVideoItem?.videoSources[index]) { + const newSource = playlistStore.currentVideoItem.videoSources[index].url; logger.info('选中的视频源索引:', index, '源:', newSource); playerStore.updateSource(newSource); } diff --git a/player/src/components/Playlist.vue b/player/src/components/Playlist.vue index 5fa8cfb..8a4415f 100644 --- a/player/src/components/Playlist.vue +++ b/player/src/components/Playlist.vue @@ -1,51 +1,73 @@ \ 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"] -}