first commit
This commit is contained in:
commit
69b647145f
58 files changed
+15796
No files matched your search
@@ -0,0 +1,13 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'],
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
globalForPrisma.prisma = prisma
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { SignJWT, jwtVerify } from 'jose'
|
||||
|
||||
function getSecret() {
|
||||
const config = useRuntimeConfig()
|
||||
return new TextEncoder().encode(config.jwtSecret)
|
||||
}
|
||||
|
||||
export async function signAdminToken(username: string) {
|
||||
return new SignJWT({ role: 'admin', username })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('7d')
|
||||
.sign(getSecret())
|
||||
}
|
||||
|
||||
export async function verifyAdminToken(token: string) {
|
||||
const { payload } = await jwtVerify(token, getSecret())
|
||||
if (payload.role !== 'admin') {
|
||||
throw createError({ statusCode: 401, statusMessage: 'Invalid admin token' })
|
||||
}
|
||||
return payload as { role: string; username: string }
|
||||
}
|
||||
|
||||
export const ADMIN_COOKIE = 'sejong_admin_token'
|
||||
@@ -0,0 +1,42 @@
|
||||
import { SignJWT, jwtVerify } from 'jose'
|
||||
|
||||
export interface RoomAccessClaims {
|
||||
roomId: string
|
||||
clientId: string
|
||||
pwdVersion: number
|
||||
}
|
||||
|
||||
function getSecret() {
|
||||
const config = useRuntimeConfig()
|
||||
return new TextEncoder().encode(config.jwtSecret)
|
||||
}
|
||||
|
||||
export async function signRoomAccessToken(claims: RoomAccessClaims) {
|
||||
const config = useRuntimeConfig()
|
||||
const days = config.roomAccessTtlDays || 7
|
||||
return new SignJWT({ ...claims, typ: 'room_access' })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime(`${days}d`)
|
||||
.sign(getSecret())
|
||||
}
|
||||
|
||||
export async function verifyRoomAccessToken(
|
||||
token: string,
|
||||
expected: { roomId: string; clientId: string; pwdVersion: number },
|
||||
) {
|
||||
const { payload } = await jwtVerify(token, getSecret())
|
||||
if (payload.typ !== 'room_access') {
|
||||
throw new Error('Invalid token type')
|
||||
}
|
||||
if (payload.roomId !== expected.roomId) {
|
||||
throw new Error('Room mismatch')
|
||||
}
|
||||
if (payload.clientId !== expected.clientId) {
|
||||
throw new Error('Client mismatch')
|
||||
}
|
||||
if (Number(payload.pwdVersion) !== expected.pwdVersion) {
|
||||
throw new Error('Password changed')
|
||||
}
|
||||
return payload as unknown as RoomAccessClaims
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function getMaxRoomMembers() {
|
||||
return useRuntimeConfig().maxRoomMembers || 8
|
||||
}
|
||||
|
||||
export function getMaxConcurrentStreams() {
|
||||
return useRuntimeConfig().maxConcurrentStreams || 4
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { ActiveStream, Participant, RoomState, WatchMap } from '~/types/room'
|
||||
import type { Server } from 'socket.io'
|
||||
|
||||
export interface RoomSession {
|
||||
clientId: string
|
||||
socketId: string
|
||||
nickname: string
|
||||
joinedAt: number
|
||||
streamId: string | null
|
||||
}
|
||||
|
||||
export interface RuntimeRoom {
|
||||
roomId: string
|
||||
sessions: Map<string, RoomSession>
|
||||
streams: Map<string, ActiveStream>
|
||||
/** viewerClientId -> Set<streamId> */
|
||||
watchMap: Map<string, Set<string>>
|
||||
}
|
||||
|
||||
const rooms = new Map<string, RuntimeRoom>()
|
||||
let ioRef: Server | null = null
|
||||
|
||||
export function setIO(io: Server) {
|
||||
ioRef = io
|
||||
}
|
||||
|
||||
export function getIO() {
|
||||
return ioRef
|
||||
}
|
||||
|
||||
export function getOrCreateRuntimeRoom(roomId: string): RuntimeRoom {
|
||||
let room = rooms.get(roomId)
|
||||
if (!room) {
|
||||
room = {
|
||||
roomId,
|
||||
sessions: new Map(),
|
||||
streams: new Map(),
|
||||
watchMap: new Map(),
|
||||
}
|
||||
rooms.set(roomId, room)
|
||||
}
|
||||
return room
|
||||
}
|
||||
|
||||
export function getRuntimeRoom(roomId: string) {
|
||||
return rooms.get(roomId)
|
||||
}
|
||||
|
||||
export function deleteRuntimeRoom(roomId: string) {
|
||||
rooms.delete(roomId)
|
||||
}
|
||||
|
||||
export function serializeWatchMap(room: RuntimeRoom): WatchMap {
|
||||
const out: WatchMap = {}
|
||||
for (const [viewerId, streamIds] of room.watchMap) {
|
||||
out[viewerId] = [...streamIds]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function buildRoomState(room: RuntimeRoom): RoomState {
|
||||
const participants: Participant[] = [...room.sessions.values()].map((s) => ({
|
||||
clientId: s.clientId,
|
||||
nickname: s.nickname,
|
||||
joinedAt: s.joinedAt,
|
||||
isStreaming: Boolean(s.streamId),
|
||||
}))
|
||||
|
||||
return {
|
||||
roomId: room.roomId,
|
||||
participants,
|
||||
activeStreams: [...room.streams.values()],
|
||||
watchMap: serializeWatchMap(room),
|
||||
}
|
||||
}
|
||||
|
||||
export function broadcastRoomState(roomId: string) {
|
||||
const room = rooms.get(roomId)
|
||||
const io = ioRef
|
||||
if (!room || !io) return
|
||||
io.to(roomId).emit('room:state', buildRoomState(room))
|
||||
}
|
||||
|
||||
export function forceCloseRoom(roomId: string, reason = 'Room deleted') {
|
||||
const io = ioRef
|
||||
if (io) {
|
||||
io.to(roomId).emit('room:force-close', { reason })
|
||||
io.in(roomId).disconnectSockets(true)
|
||||
}
|
||||
deleteRuntimeRoom(roomId)
|
||||
}
|
||||
|
||||
export function removeViewerWatches(room: RuntimeRoom, clientId: string) {
|
||||
room.watchMap.delete(clientId)
|
||||
for (const [, set] of room.watchMap) {
|
||||
// no-op for stream ownership
|
||||
}
|
||||
}
|
||||
|
||||
export function stopStreamsByClient(room: RuntimeRoom, clientId: string) {
|
||||
const session = room.sessions.get(clientId)
|
||||
if (session?.streamId) {
|
||||
room.streams.delete(session.streamId)
|
||||
for (const [, set] of room.watchMap) {
|
||||
set.delete(session.streamId)
|
||||
}
|
||||
session.streamId = null
|
||||
}
|
||||
}
|
||||
|
||||
export function getMemberCount(roomId: string) {
|
||||
return rooms.get(roomId)?.sessions.size ?? 0
|
||||
}
|
||||
|
||||
export function getStreamCount(roomId: string) {
|
||||
return rooms.get(roomId)?.streams.size ?? 0
|
||||
}
|
||||
Reference in new issue
Block a user