вложения, канальчики, бим-бим + бам-бам
This commit is contained in:
96
server/routes/attachment.ts
Normal file
96
server/routes/attachment.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
|
||||
import * as fs from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import { Type } from 'typebox'
|
||||
|
||||
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||
const uploadDir = path.join(process.cwd(), 'uploads')
|
||||
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true })
|
||||
}
|
||||
|
||||
fastify.post(
|
||||
'/attachment/upload',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Upload attachment',
|
||||
tags: ['Attachment'],
|
||||
operationId: 'attachment.upload',
|
||||
description: 'Pass file to multipart/form-data',
|
||||
response: {
|
||||
200: Type.String({ format: 'uuid', description: 'Attachment UUID' }),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (req, reply) => {
|
||||
const data = await req.file()
|
||||
|
||||
if (!data) {
|
||||
return reply.notAcceptable()
|
||||
}
|
||||
|
||||
const meta = await fastify.prisma.attachment.create({
|
||||
data: {
|
||||
name: data.filename,
|
||||
mimetype: data.mimetype,
|
||||
size: 0,
|
||||
},
|
||||
})
|
||||
|
||||
if (!meta) {
|
||||
return reply.notAcceptable()
|
||||
}
|
||||
|
||||
const filePath = path.join(process.cwd(), 'uploads', meta.id)
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const writeStream = fs.createWriteStream(filePath)
|
||||
data.file.pipe(writeStream)
|
||||
data.file.on('end', resolve)
|
||||
data.file.on('error', reject)
|
||||
})
|
||||
|
||||
return meta.id
|
||||
},
|
||||
)
|
||||
|
||||
fastify.get(
|
||||
'/attachment/:id',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Get attachment',
|
||||
tags: ['Attachment'],
|
||||
operationId: 'attachment.get',
|
||||
params: Type.Object({
|
||||
id: Type.String({ format: 'uuid' }),
|
||||
}),
|
||||
response: {
|
||||
200: Type.Any({ description: 'Attachment content' }),
|
||||
},
|
||||
},
|
||||
config: {
|
||||
skipAuth: true,
|
||||
},
|
||||
},
|
||||
async (req, reply) => {
|
||||
const meta = await fastify.prisma.attachment.findFirst({
|
||||
where: { id: req.params.id },
|
||||
})
|
||||
|
||||
if (!meta) {
|
||||
return reply.notFound('Attachment not found')
|
||||
}
|
||||
|
||||
const filePath = path.join(process.cwd(), 'uploads', meta.id)
|
||||
|
||||
reply.type(meta.mimetype)
|
||||
reply.header('Cache-Control', 'public, max-age=31536000')
|
||||
reply.header('Content-Disposition', `inline; filename="${meta.name}"`)
|
||||
|
||||
return fs.createReadStream(filePath)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export default plugin
|
||||
@@ -1,33 +0,0 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import bcrypt from 'bcrypt'
|
||||
import { z } from 'zod'
|
||||
|
||||
export default function (fastify: FastifyInstance) {
|
||||
fastify.post('/attachments/upload', async (req, reply) => {
|
||||
try {
|
||||
const schema = z.object({
|
||||
file: z.file(),
|
||||
})
|
||||
const input = schema.parse(req.body)
|
||||
|
||||
// const file = req.file({ limits: { } })
|
||||
|
||||
const id = await bcrypt.hash(input.file, 10)
|
||||
|
||||
return {
|
||||
id,
|
||||
}
|
||||
}
|
||||
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 })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,118 +1,146 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
|
||||
import bcrypt from 'bcrypt'
|
||||
import { z } from 'zod'
|
||||
import { auth } from '../auth/lucia.ts'
|
||||
import prisma from '../prisma/client.ts'
|
||||
import { Type } from 'typebox'
|
||||
import { CreateUserSchema, UserSchema } from '../schemas/auth.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({
|
||||
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||
fastify.post(
|
||||
'/auth/register',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Register',
|
||||
tags: ['Auth'],
|
||||
operationId: 'auth.register',
|
||||
body: CreateUserSchema,
|
||||
response: {
|
||||
200: UserSchema,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
skipAuth: true,
|
||||
},
|
||||
},
|
||||
async (req, reply) => {
|
||||
const hashed = await bcrypt.hash(req.body.password, 10)
|
||||
const user = await fastify.prisma.user.create({
|
||||
data: {
|
||||
username: input.username,
|
||||
username: req.body.username,
|
||||
password: hashed,
|
||||
displayName: input.username,
|
||||
displayName: req.body.username,
|
||||
UserPreferences: {
|
||||
create: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const session = await auth.createSession(user.id, {})
|
||||
const cookie = auth.createSessionCookie(session.id)
|
||||
const session = await fastify.lucia.createSession(user.id, {})
|
||||
const cookie = fastify.lucia.createSessionCookie(session.id)
|
||||
|
||||
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
displayName: user.username,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
}
|
||||
}
|
||||
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 },
|
||||
fastify.post(
|
||||
'/auth/login',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Login',
|
||||
tags: ['Auth'],
|
||||
operationId: 'auth.login',
|
||||
body: Type.Object({
|
||||
username: Type.String({ minLength: 1 }),
|
||||
password: Type.String({ minLength: 1 }),
|
||||
}),
|
||||
response: {
|
||||
200: UserSchema,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
skipAuth: true,
|
||||
},
|
||||
},
|
||||
async (req, reply) => {
|
||||
const user = await fastify.prisma.user.findFirst({
|
||||
where: { username: req.body.username },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
displayName: true,
|
||||
createdAt: true,
|
||||
password: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return reply.code(404).send({ error: 'Incorrect username or password' })
|
||||
return reply.notFound('Incorrect username or password')
|
||||
}
|
||||
|
||||
const validPassword = await bcrypt.compare(input.password, user.password)
|
||||
const validPassword = await bcrypt.compare(req.body.password, user.password)
|
||||
if (!validPassword) {
|
||||
return reply.code(404).send({ error: 'Incorrect username or password' })
|
||||
return reply.notFound('Incorrect username or password')
|
||||
}
|
||||
|
||||
const session = await auth.createSession(user.id, {})
|
||||
const cookie = auth.createSessionCookie(session.id)
|
||||
const session = await fastify.lucia.createSession(user.id, {})
|
||||
const cookie = fastify.lucia.createSessionCookie(session.id)
|
||||
|
||||
reply.setCookie(cookie.name, cookie.value, cookie.attributes)
|
||||
|
||||
return {
|
||||
...user,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fastify.get(
|
||||
'/auth/me',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Me',
|
||||
tags: ['Auth'],
|
||||
operationId: 'auth.me',
|
||||
response: {
|
||||
200: UserSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (req) => {
|
||||
const user = req.user!
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
}
|
||||
}
|
||||
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 {
|
||||
fastify.post(
|
||||
'/auth/logout',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Logout',
|
||||
tags: ['Auth'],
|
||||
operationId: 'auth.logout',
|
||||
},
|
||||
},
|
||||
async (req, reply) => {
|
||||
if (req.session)
|
||||
await auth.invalidateSession(req.session.id)
|
||||
await fastify.lucia.invalidateSession(req.session.id)
|
||||
|
||||
const blank = auth.createBlankSessionCookie()
|
||||
const blank = fastify.lucia.createBlankSessionCookie()
|
||||
|
||||
reply.setCookie(blank.name, blank.value, blank.attributes)
|
||||
|
||||
return true
|
||||
}
|
||||
catch (err) {
|
||||
fastify.log.error(err)
|
||||
reply.code(400).send({ error: err.message })
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export default plugin
|
||||
|
||||
111
server/routes/chat.ts
Normal file
111
server/routes/chat.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
|
||||
import { Type } from 'typebox'
|
||||
import { ChatMessageSchema, NewChatMessageSchema } from '../schemas/chat.ts'
|
||||
|
||||
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||
fastify.post(
|
||||
'/chat/send',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Send message',
|
||||
tags: ['Chat'],
|
||||
operationId: 'chat.send',
|
||||
body: NewChatMessageSchema,
|
||||
response: {
|
||||
200: ChatMessageSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (req, reply) => {
|
||||
const user = req.user!
|
||||
|
||||
const message = await fastify.prisma.message.create({
|
||||
data: {
|
||||
text: req.body.text,
|
||||
senderId: user.id,
|
||||
attachments: {
|
||||
create: (req.body.attachments ?? []).map((attachmentId) => {
|
||||
return {
|
||||
attachment: {
|
||||
connect: {
|
||||
id: attachmentId,
|
||||
},
|
||||
},
|
||||
}
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!message) {
|
||||
return reply.unprocessableEntity()
|
||||
}
|
||||
|
||||
const response = {
|
||||
id: message.id,
|
||||
senderId: user.id,
|
||||
text: message.text,
|
||||
createdAt: message.createdAt.toISOString(),
|
||||
updatedAt: message.updatedAt.toISOString(),
|
||||
attachments: req.body.attachments ?? [],
|
||||
}
|
||||
|
||||
fastify.bus.emit('chat:new-message', response)
|
||||
|
||||
return response
|
||||
},
|
||||
)
|
||||
|
||||
fastify.get(
|
||||
'/chat',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Get messages',
|
||||
tags: ['Chat'],
|
||||
operationId: 'chat.messages',
|
||||
querystring: Type.Object({
|
||||
cursor: Type.Optional(Type.String({ format: 'uuid', description: 'Cursor to message' })),
|
||||
limit: Type.Number({ minimum: 1, maximum: 100, default: 10 }),
|
||||
}),
|
||||
response: {
|
||||
200: Type.Object({
|
||||
messages: Type.Array(ChatMessageSchema),
|
||||
nextCursor: Type.Optional(Type.String({ format: 'uuid', description: 'Cursor to last message' })),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line ts/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
async (req) => {
|
||||
const messages = await fastify.prisma.message.findMany({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: req.query.limit + 1,
|
||||
include: { attachments: true },
|
||||
...(req.query.cursor && {
|
||||
cursor: {
|
||||
id: req.query.cursor,
|
||||
},
|
||||
// skip: 1,
|
||||
}),
|
||||
})
|
||||
|
||||
const hasMore = messages.length > req.query.limit
|
||||
const cursorMessage = hasMore ? messages.pop() : undefined
|
||||
|
||||
return {
|
||||
messages: messages.map((message) => {
|
||||
return {
|
||||
...message,
|
||||
createdAt: message.createdAt.toISOString(),
|
||||
updatedAt: message.updatedAt.toISOString(),
|
||||
attachments: message.attachments.map(({ attachmentId }) => attachmentId),
|
||||
}
|
||||
}),
|
||||
nextCursor: cursorMessage?.id,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export default plugin
|
||||
@@ -1,97 +1,158 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import type { Namespace } from '../types/webrtc.ts'
|
||||
import { z } from 'zod'
|
||||
import prisma from '../prisma/client.ts'
|
||||
import { socketToClient } from '../utils/socket-to-client.ts'
|
||||
import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
|
||||
import { Type } from 'typebox'
|
||||
import { UserSchema } from '../schemas/auth.ts'
|
||||
import { UpdateUserPreferencesSchema, UserPreferencesSchema } from '../schemas/user.ts'
|
||||
|
||||
export default function (fastify: FastifyInstance) {
|
||||
fastify.get('/preferences', async (req, reply) => {
|
||||
if (req.user) {
|
||||
return prisma.userPreferences.findFirst({ where: { userId: req.user.id } })
|
||||
}
|
||||
|
||||
reply.code(401).send(false)
|
||||
})
|
||||
|
||||
fastify.patch('/preferences', async (req, reply) => {
|
||||
if (!req.user) {
|
||||
reply.code(401).send(false)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const schema = z.object({
|
||||
toggleInputHotkey: z.string().optional(),
|
||||
toggleOutputHotkey: z.string().optional(),
|
||||
volumes: z.record(z.string(), z.number()).optional(),
|
||||
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
||||
fastify.get(
|
||||
'/user',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Get user',
|
||||
tags: ['User'],
|
||||
operationId: 'user.get',
|
||||
querystring: Type.Partial(Type.Object({
|
||||
username: Type.String(),
|
||||
})),
|
||||
response: {
|
||||
200: UserSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (req, reply) => {
|
||||
const user = await fastify.prisma.user.findFirst({
|
||||
where: { username: req.query.username },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
displayName: true,
|
||||
createdAt: true,
|
||||
},
|
||||
})
|
||||
const input = schema.parse(req.body)
|
||||
|
||||
return prisma.userPreferences.upsert({
|
||||
where: { userId: req.user.id },
|
||||
if (!user) {
|
||||
return reply.notFound('User not found')
|
||||
}
|
||||
|
||||
return {
|
||||
...user,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fastify.get(
|
||||
'/user/preferences',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Get preferences',
|
||||
tags: ['User'],
|
||||
operationId: 'user.getPreferences',
|
||||
response: {
|
||||
200: UserPreferencesSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (req, reply) => {
|
||||
const user = req.user!
|
||||
|
||||
const preferences = await fastify.prisma.userPreferences.upsert({
|
||||
where: { userId: user.id },
|
||||
create: { userId: user.id },
|
||||
update: {},
|
||||
})
|
||||
|
||||
if (!preferences) {
|
||||
return reply.notFound('User preferences not found')
|
||||
}
|
||||
|
||||
return {
|
||||
toggleInputHotkey: preferences.toggleInputHotkey || '',
|
||||
toggleOutputHotkey: preferences.toggleOutputHotkey || '',
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fastify.patch(
|
||||
'/user/preferences',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Update preferences',
|
||||
tags: ['User'],
|
||||
operationId: 'user.updatePreferences',
|
||||
body: UpdateUserPreferencesSchema,
|
||||
},
|
||||
},
|
||||
async (req) => {
|
||||
const user = req.user!
|
||||
|
||||
return fastify.prisma.userPreferences.upsert({
|
||||
where: { userId: user.id },
|
||||
create: {
|
||||
userId: req.user.id,
|
||||
...input,
|
||||
userId: user.id,
|
||||
...req.body,
|
||||
},
|
||||
update: input,
|
||||
update: req.body,
|
||||
})
|
||||
}
|
||||
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.patch(
|
||||
'/profile',
|
||||
{
|
||||
schema: {
|
||||
summary: 'Update profile',
|
||||
tags: ['User'],
|
||||
operationId: 'user.updateProfile',
|
||||
body: Type.Object({
|
||||
displayName: Type.String(),
|
||||
}),
|
||||
response: {
|
||||
200: UserSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (req, reply) => {
|
||||
const user = req.user!
|
||||
|
||||
fastify.patch('/profile', async (req, reply) => {
|
||||
if (!req.user) {
|
||||
reply.code(401).send(false)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const schema = z.object({
|
||||
displayName: z.string().optional(),
|
||||
})
|
||||
const input = schema.parse(req.body)
|
||||
|
||||
const updatedUser = prisma.user.update({
|
||||
where: { id: req.user.id },
|
||||
const updatedUser = await fastify.prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
displayName: input.displayName,
|
||||
displayName: req.body.displayName,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
displayName: true,
|
||||
createdAt: true,
|
||||
},
|
||||
})
|
||||
|
||||
const namespace: Namespace = fastify.io.of('/webrtc')
|
||||
const sockets = await namespace.fetchSockets()
|
||||
|
||||
const found = sockets.find(socket => socket.data.joined && socket.data.userId === req.user!.id)
|
||||
|
||||
if (found) {
|
||||
found.data.displayName = input.displayName
|
||||
namespace.emit('clientChanged', found.id, socketToClient(found))
|
||||
if (!updatedUser) {
|
||||
return reply.notFound('User not found')
|
||||
}
|
||||
|
||||
return updatedUser
|
||||
}
|
||||
catch (err) {
|
||||
fastify.log.error(err)
|
||||
reply.code(400)
|
||||
const response = {
|
||||
...updatedUser,
|
||||
createdAt: updatedUser.createdAt.toISOString(),
|
||||
}
|
||||
|
||||
if (err instanceof z.ZodError) {
|
||||
reply.send({ error: z.prettifyError(err) })
|
||||
}
|
||||
else {
|
||||
reply.send({ error: err.message })
|
||||
}
|
||||
}
|
||||
})
|
||||
fastify.bus.emit('user:profile-updated', response)
|
||||
|
||||
// TODO: подписаться в webrtc
|
||||
// const namespace: Namespace = fastify.io.of('/webrtc')
|
||||
// const sockets = await namespace.fetchSockets()
|
||||
//
|
||||
// const found = sockets.find(socket => socket.data.joined && socket.data.userId === req.user!.id)
|
||||
//
|
||||
// if (found) {
|
||||
// found.data.displayName = req.body.displayName
|
||||
// namespace.emit('clientChanged', found.id, socketToClient(found))
|
||||
// }
|
||||
|
||||
return response
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export default plugin
|
||||
|
||||
Reference in New Issue
Block a user