куча говна
All checks were successful
Deploy / deploy (push) Successful in 4m32s

This commit is contained in:
Никита Круглицкий
2025-10-20 00:10:13 +06:00
parent 31460598ba
commit ec67be8aa6
50 changed files with 1616 additions and 1011 deletions

View File

@@ -1,7 +1,29 @@
import { PrismaAdapter } from '@lucia-auth/adapter-prisma'
import { Lucia } from 'lucia'
import prisma from '../prisma/client'
import prisma from '../prisma/client.ts'
export const auth = new Lucia<object, { username: string, displayName: string }>(new PrismaAdapter(prisma.session, prisma.user))
declare module 'lucia' {
interface Register {
Lucia: typeof Lucia
UserId: string
DatabaseUserAttributes: DatabaseUserAttributes
}
}
interface DatabaseUserAttributes {
id: string
displayName: string
username: string
}
export const auth = new Lucia(new PrismaAdapter(prisma.session, prisma.user), {
getUserAttributes: ({ id, displayName, username }) => {
return {
id,
displayName,
username,
}
},
})
export type Auth = typeof auth

View File

@@ -4,17 +4,19 @@
"start": "ts-node --transpile-only server.ts",
"db:deploy": "npx prisma migrate deploy && npx prisma generate"
},
"type": "module",
"packageManager": "yarn@4.10.3",
"dependencies": {
"@fastify/autoload": "^6.3.1",
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.1.0",
"@lucia-auth/adapter-prisma": "^4.0.1",
"@prisma/client": "^6.17.0",
"@trpc/server": "^11.6.0",
"bcrypt": "^6.0.0",
"consola": "^3.4.2",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"fastify": "^5.6.1",
"fastify-plugin": "^5.1.0",
"lucia": "^3.2.2",
"mediasoup": "^3.19.3",
"prisma": "^6.17.0",
@@ -25,8 +27,6 @@
"devDependencies": {
"@antfu/eslint-config": "^5.4.1",
"@types/bcrypt": "^6",
"@types/cookie-parser": "^1",
"@types/express": "^5.0.3",
"@types/ws": "^8",
"eslint": "^9.36.0",
"ts-node": "^10.9.2",

40
server/plugins/auth.ts Normal file
View File

@@ -0,0 +1,40 @@
import type { Session, User } from 'lucia'
import fp from 'fastify-plugin'
import { auth } from '../auth/lucia.ts'
declare module 'fastify' {
interface FastifyRequest {
user: User | null
session: Session | null
}
}
export default fp(async (fastify) => {
fastify.decorateRequest('user', null)
fastify.decorateRequest('session', null)
fastify.addHook('preHandler', async (req, reply) => {
try {
const sessionId = auth.readSessionCookie(req.headers.cookie ?? '')
const { session, user } = await auth.validateSession(sessionId ?? '')
if (session && session.fresh) {
const cookie = auth.createSessionCookie(session.id)
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
}
if (!session) {
const blank = auth.createBlankSessionCookie()
reply.setCookie(blank.name, blank.value, blank.attributes)
}
req.user = user
req.session = session
}
catch {
req.user = null
req.session = null
}
})
})

View File

@@ -0,0 +1,29 @@
import type * as mediasoup from 'mediasoup'
import fp from 'fastify-plugin'
declare module 'fastify' {
interface FastifyInstance {
mediasoupRouter: mediasoup.types.Router
}
}
export default fp<mediasoup.types.RouterOptions>(
async (fastify, opts) => {
const router = await fastify.mediasoupWorker.createRouter(opts)
fastify.decorate('mediasoupRouter', router)
},
{ name: 'mediasoup-router', dependencies: ['mediasoup-worker'] },
)
export const autoConfig: mediasoup.types.RouterOptions = {
mediaCodecs: [
{
kind: 'audio',
mimeType: 'audio/opus',
clockRate: 48000,
channels: 2,
parameters: { useinbandfec: 1, stereo: 1 },
},
],
}

View File

@@ -0,0 +1,23 @@
import { consola } from 'consola'
import fp from 'fastify-plugin'
import * as mediasoup from 'mediasoup'
declare module 'fastify' {
interface FastifyInstance {
mediasoupWorker: mediasoup.types.Worker
}
}
export default fp(
async (fastify) => {
const worker = await mediasoup.createWorker()
worker.on('died', () => {
consola.error('[Mediasoup]', 'Worker died, exiting...')
process.exit(1)
})
fastify.decorate('mediasoupWorker', worker)
},
{ name: 'mediasoup-worker' },
)

39
server/plugins/socket.ts Normal file
View File

@@ -0,0 +1,39 @@
import type { FastifyInstance } from 'fastify'
import type { ServerOptions } from 'socket.io'
import fp from 'fastify-plugin'
import { Server } from 'socket.io'
import registerWebrtcSocket from '../socket/webrtc.ts'
declare module 'fastify' {
interface FastifyInstance {
io: Server
}
}
export default fp<Partial<ServerOptions>>(
async (fastify, opts) => {
fastify.decorate('io', new Server(fastify.server, opts))
fastify.addHook('preClose', () => {
fastify.io.disconnectSockets(true)
})
fastify.addHook('onClose', async (fastify: FastifyInstance) => {
await fastify.io.close()
})
fastify.ready(() => {
registerWebrtcSocket(fastify.io, fastify.mediasoupRouter)
})
},
{ name: 'socket-io', dependencies: ['mediasoup-worker', 'mediasoup-router'] },
)
export const autoConfig: Partial<ServerOptions> = {
path: '/chad/ws',
cors: {
origin: process.env.CORS_ORIGIN || '*',
methods: ['GET', 'POST'],
credentials: true,
},
}

View File

@@ -1,7 +1,7 @@
import { PrismaClient } from '@prisma/client'
const instance = new PrismaClient({
const client = new PrismaClient({
log: ['query', 'error', 'warn'],
})
export default instance
export default client

View File

@@ -5,6 +5,7 @@ datasource db {
generator client {
provider = "prisma-client-js"
// output = "./generated/client"
}
model User {

120
server/routes/auth.ts Normal file
View File

@@ -0,0 +1,120 @@
import type { FastifyInstance } from 'fastify'
import bcrypt from 'bcrypt'
import { z } from 'zod'
import { auth } from '../auth/lucia.ts'
import prisma from '../prisma/client.ts'
export default function (fastify: FastifyInstance) {
fastify.post('/register', async (req, reply) => {
try {
const schema = z.object({
username: z.string().min(1),
password: z.string().min(6),
})
const input = schema.parse(req.body)
const hashed = await bcrypt.hash(input.password, 10)
const user = await prisma.user.create({
data: {
username: input.username,
password: hashed,
displayName: input.username,
},
})
const session = await auth.createSession(user.id, {})
const cookie = auth.createSessionCookie(session.id)
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
return {
id: user.id,
username: user.username,
displayName: user.displayName,
}
}
catch (err) {
fastify.log.error(err)
reply.code(400)
if (err instanceof z.ZodError) {
reply.send({ error: z.prettifyError(err) })
}
else {
reply.send({ error: err.message })
}
}
})
fastify.post('/login', async (req, reply) => {
try {
const schema = z.object({
username: z.string().min(1),
password: z.string(),
})
const input = schema.parse(req.body)
const user = await prisma.user.findFirst({
where: { username: input.username },
})
if (!user) {
return reply.code(404).send({ error: 'Incorrect username or password' })
}
const validPassword = await bcrypt.compare(input.password, user.password)
if (!validPassword) {
return reply.code(404).send({ error: 'Incorrect username or password' })
}
const session = await auth.createSession(user.id, {})
const cookie = auth.createSessionCookie(session.id)
cookie.attributes.secure = false
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
return {
id: user.id,
username: user.username,
displayName: user.displayName,
}
}
catch (err) {
fastify.log.error(err)
reply.code(400)
if (err instanceof z.ZodError) {
reply.send({ error: z.prettifyError(err) })
}
else {
reply.send({ error: err.message })
}
}
})
fastify.get('/me', async (req, reply) => {
if (req.user) {
return req.user
}
reply.code(401).send(false)
})
fastify.post('/logout', async (req, reply) => {
try {
if (req.session)
await auth.invalidateSession(req.session.id)
const blank = auth.createBlankSessionCookie()
reply.setCookie(blank.name, blank.value, blank.attributes)
return true
}
catch (err) {
fastify.log.error(err)
reply.code(400).send({ error: err.message })
}
})
}

View File

@@ -1,61 +1,41 @@
import { createServer as createHttpServer } from 'node:http'
import { createExpressMiddleware } from '@trpc/server/adapters/express'
import { consola } from 'consola'
import cookieParser from 'cookie-parser'
import cors from 'cors'
import express from 'express'
import * as mediasoup from 'mediasoup'
import { Server as SocketServer } from 'socket.io'
import { createContext } from './trpc/context'
import { appRouter } from './trpc/routers'
import webrtcSocket from './webrtc/socket'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import FastifyAutoLoad from '@fastify/autoload'
import FastifyCookie from '@fastify/cookie'
import FastifyCors from '@fastify/cors'
import Fastify from 'fastify'
import prisma from './prisma/client.ts'
(async () => {
const app = express()
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
app.use(cors())
app.use(cookieParser())
app.use(express.json())
const fastify = Fastify({
logger: true,
})
app.use(
'/chad/trpc',
createExpressMiddleware({
router: appRouter,
createContext,
}),
)
fastify.register(FastifyCors)
const server = createHttpServer(app)
fastify.register(FastifyCookie)
const worker = await mediasoup.createWorker()
worker.on('died', () => {
consola.error('[Mediasoup]', 'Worker died, exiting...')
fastify.register(FastifyAutoLoad, {
dir: join(__dirname, 'plugins'),
})
fastify.register(FastifyAutoLoad, {
dir: join(__dirname, 'routes'),
})
;(async () => {
const port = process.env.PORT ? Number(process.env.PORT) : 4000
try {
await fastify.listen({ port })
await prisma.$connect()
fastify.log.info('Testing DB Connection. OK')
}
catch (err) {
fastify.log.error(err)
process.exit(1)
})
const router = await worker.createRouter({
mediaCodecs: [
{
kind: 'audio',
mimeType: 'audio/opus',
clockRate: 48000,
channels: 2,
parameters: { useinbandfec: 1, stereo: 1 },
},
],
})
const io = new SocketServer(server, {
path: '/chad/ws',
cors: {
origin: process.env.CORS_ORIGIN || '*',
},
})
webrtcSocket(io, router)
server.listen(process.env.PORT || 4000, () => {
console.log('✅ Server running')
})
}
})()

View File

@@ -1,10 +1,14 @@
import type { User } from '@prisma/client'
import type { types } from 'mediasoup'
import type { Namespace, RemoteSocket, Socket, Server as SocketServer } from 'socket.io'
import { consola } from 'consola'
import prisma from '../prisma/client.ts'
interface ChadClient {
id: string
username: string
socketId: string
userId: User['id']
username: User['username']
displayName: User['displayName']
inputMuted: boolean
outputMuted: boolean
}
@@ -27,10 +31,9 @@ type EventCallback<T = SuccessCallbackResult> = (result: T | ErrorCallbackResult
interface ClientToServerEvents {
join: (
options: {
username: string
rtpCapabilities: types.RtpCapabilities
},
cb: EventCallback<{ id: string, username: string }[]>
cb: EventCallback<ChadClient[]>
) => void
getRtpCapabilities: (
cb: EventCallback<types.RtpCapabilities>
@@ -88,12 +91,13 @@ interface ClientToServerEvents {
cb: EventCallback
) => void
updateClient: (
options: Partial<Omit<ChadClient, 'id'>>,
cb: EventCallback
options: Partial<Omit<ChadClient, 'socketId' | 'userId'>>,
cb: EventCallback<ChadClient>
) => void
}
interface ServerToClientEvents {
authenticated: () => void
newPeer: (arg: ChadClient) => void
producers: (arg: ProducerShort[]) => void
newConsumer: (
@@ -114,14 +118,16 @@ interface ServerToClientEvents {
consumerPaused: (arg: { consumerId: string }) => void
consumerResumed: (arg: { consumerId: string }) => void
consumerScore: (arg: { consumerId: string, score: types.ConsumerScore }) => void
clientChanged: (clientId: ChadClient['id'], client: ChadClient) => void
clientChanged: (clientId: ChadClient['socketId'], client: ChadClient) => void
}
interface InterServerEvent {}
interface SocketData {
joined: boolean
username: string
userId: User['id']
username: User['username']
displayName: User['displayName']
inputMuted: boolean
outputMuted: boolean
rtpCapabilities: types.RtpCapabilities
@@ -130,6 +136,8 @@ interface SocketData {
consumers: Map<types.Consumer['id'], types.Consumer>
}
type SomeSocket = Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvent, SocketData> | RemoteSocket<ServerToClientEvents, SocketData>
export default function (io: SocketServer, router: types.Router) {
const namespace: Namespace<ClientToServerEvents, ServerToClientEvents, InterServerEvent, SocketData> = io.of('/webrtc')
@@ -138,7 +146,6 @@ export default function (io: SocketServer, router: types.Router) {
socket.data.joined = false
socket.data.username = socket.id
socket.data.inputMuted = false
socket.data.outputMuted = false
@@ -146,24 +153,35 @@ export default function (io: SocketServer, router: types.Router) {
socket.data.producers = new Map()
socket.data.consumers = new Map()
socket.on('join', async ({ username, rtpCapabilities }, cb) => {
prisma.user.findUnique({
where: {
id: socket.handshake.auth.userId,
},
select: {
id: true,
username: true,
displayName: true,
},
}).then(({ id, username, displayName }) => {
socket.data.userId = id
socket.data.username = username
socket.data.displayName = displayName
socket.emit('authenticated')
})
socket.on('join', async ({ rtpCapabilities }, cb) => {
if (socket.data.joined) {
consola.error('[WebRtc]', 'Already joined')
cb({ error: 'Already joined' })
}
socket.data.joined = true
socket.data.username = username
socket.data.rtpCapabilities = rtpCapabilities
const joinedSockets = await getJoinedSockets()
cb(joinedSockets.map((s) => {
return {
id: s.id,
username: s.data.username,
}
}))
cb(joinedSockets.map(socketToClient))
for (const joinedSocket of joinedSockets.filter(joinedSocket => joinedSocket.id !== socket.id)) {
for (const producer of joinedSocket.data.producers.values()) {
@@ -417,9 +435,18 @@ export default function (io: SocketServer, router: types.Router) {
cb({ ok: true })
})
socket.on('updateClient', (updatedClient, cb) => {
if (updatedClient.username) {
socket.data.username = updatedClient.username
socket.on('updateClient', async (updatedClient, cb) => {
if (updatedClient.displayName) {
await prisma.user.update({
where: {
id: socket.data.userId,
},
data: {
displayName: updatedClient.displayName,
},
})
socket.data.displayName = updatedClient.displayName
}
if (updatedClient.inputMuted) {
@@ -430,7 +457,7 @@ export default function (io: SocketServer, router: types.Router) {
socket.data.outputMuted = updatedClient.outputMuted
}
cb({ ok: true })
cb(socketToClient(socket))
namespace.emit('clientChanged', socket.id, socketToClient(socket))
})
@@ -455,8 +482,8 @@ export default function (io: SocketServer, router: types.Router) {
}
async function createConsumer(
consumerSocket: Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvent, SocketData>,
producerSocket: RemoteSocket<ServerToClientEvents, SocketData>,
consumerSocket: SomeSocket,
producerSocket: SomeSocket,
producer: types.Producer,
) {
if (
@@ -554,10 +581,12 @@ export default function (io: SocketServer, router: types.Router) {
}
}
function socketToClient(socket: Socket<ClientToServerEvents, ServerToClientEvents, InterServerEvent, SocketData>): ChadClient {
function socketToClient(socket: SomeSocket): ChadClient {
return {
id: socket.id,
socketId: socket.id,
userId: socket.data.userId,
username: socket.data.username,
displayName: socket.data.displayName,
inputMuted: socket.data.inputMuted,
outputMuted: socket.data.outputMuted,
}

View File

@@ -1,23 +0,0 @@
import type { CreateExpressContextOptions } from '@trpc/server/adapters/express'
import { auth } from '../auth/lucia'
export async function createContext({ res, req }: CreateExpressContextOptions) {
const sessionId = auth.readSessionCookie(req.headers.cookie ?? '')
if (!sessionId)
return { res, req }
const { session, user } = await auth.validateSession(sessionId)
if (session && session.fresh) {
res.appendHeader('Set-Cookie', auth.createSessionCookie(session.id).serialize())
}
if (!session) {
res.appendHeader('Set-Cookie', auth.createBlankSessionCookie().serialize())
}
return { res, req, session, user }
}
export type Context = Awaited<ReturnType<typeof createContext>>

View File

@@ -1,15 +0,0 @@
import type { Context } from './context'
import { initTRPC } from '@trpc/server'
const t = initTRPC.context<Context>().create()
export const router = t.router
export const publicProcedure = t.procedure
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session?.fresh)
throw new Error('UNAUTHORIZED')
return next({ ctx: { ...ctx } })
})

View File

@@ -1,74 +0,0 @@
import { TRPCError } from '@trpc/server'
import bcrypt from 'bcrypt'
import { z } from 'zod'
import { auth } from '../../auth/lucia'
import client from '../../prisma/client'
import { protectedProcedure, publicProcedure, router } from '../router'
export const authRouter = router({
register: publicProcedure
.input(z.object({ username: z.string().min(1), password: z.string().min(6) }))
.mutation(async ({ input, ctx }) => {
const hashed = await bcrypt.hash(input.password, 10)
const user = await client.user.create({
data: {
username: input.username,
password: hashed,
displayName: input.username,
},
})
const session = await auth.createSession(user.id, {})
const cookie = auth.createSessionCookie(session.id)
ctx.res.setHeader('Set-Cookie', cookie.serialize())
return { user }
}),
login: publicProcedure
.input(z.object({ username: z.string().min(1), password: z.string() }))
.mutation(async ({ input, ctx }) => {
const user = await client.user.findFirst({
where: {
username: input.username,
},
})
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Incorrect username or password',
})
}
const validPassword = await bcrypt.compare(input.password, user.password)
if (!validPassword) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Incorrect username or password',
})
}
const session = await auth.createSession(user.id, {})
const cookie = auth.createSessionCookie(session.id)
ctx.res.setHeader('Set-Cookie', cookie.serialize())
return { user }
}),
me: protectedProcedure.query(({ ctx }) => {
return ctx.user
}),
logout: publicProcedure.mutation(async ({ ctx }) => {
if (ctx.session)
await auth.invalidateSession(ctx.session.id)
ctx.res.setHeader('Set-Cookie', auth.createBlankSessionCookie().serialize())
return true
}),
})

View File

@@ -1,10 +0,0 @@
import { router } from '../router'
import { authRouter } from './auth'
// import { webrtcRouter } from './webrtc'
export const appRouter = router({
auth: authRouter,
// webrtc: webrtcRouter,
})
export type AppRouter = typeof appRouter

View File

@@ -1,10 +1,12 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"module": "ESNext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
"skipLibCheck": true,
"allowImportingTsExtensions": true
}
}

File diff suppressed because it is too large Load Diff