stream day 6

This commit is contained in:
Zoe
2023-01-11 22:29:41 -06:00
parent a30bcfa0a8
commit 21a9b11547
21 changed files with 644 additions and 145 deletions

View File

@@ -3,7 +3,7 @@
<div class="w-full h-[calc(100%-60px)] overflow-y-scroll pb-1" <div class="w-full h-[calc(100%-60px)] overflow-y-scroll pb-1"
id="conversation-pane"> id="conversation-pane">
<div> <div>
<div v-if="!conversation"> <div v-if="conversation.length === 0">
<p>No messages yet</p> <p>No messages yet</p>
</div> </div>
<div v-else <div v-else
@@ -11,10 +11,23 @@
<div class="message-container"> <div class="message-container">
<div> <div>
<div class="message-sender-text"> <div class="message-sender-text">
<p :class="(message.userId == user.id) ? 'message-sender-you' : 'message-sender'"> <p class="mb-1.5 font-semibold">
{{ message.userId }}</p> {{ message.creator.username }}
</p>
<p class="break-words max-w-full">{{ message.body }}</p> <p class="break-words max-w-full">{{ message.body }}</p>
</div> </div>
<div>
<div v-for="invite in message.invites">
<div class="w-6/12 bg-[hsl(223,6.9%,19.8%)] p-4 rounded-md shadow-md mr-2">
<p class="text-sm font-semibold">You've been invited</p>
<span class="text-xl font-bold capitalize">{{ invite.server.name }}</span>
<div class="flex w-full justify-end">
<button @click="joinServer(invite)"
class="font-semibold rounded px-4 py-2 bg-green-700 hover:bg-green-600 transition-colors">Join</button>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -22,7 +35,7 @@
</div> </div>
<div class="conversation-input w-[calc(100vw-88px-240px)]"> <div class="conversation-input w-[calc(100vw-88px-240px)]">
<form @submit.prevent="sendMessage" <form @submit.prevent="sendMessage"
@keydown.enter="sendMessage" @keydown.enter.exact.prevent="sendMessage"
class="relative px-4 w-full"> class="relative px-4 w-full">
<div id="textbox" <div id="textbox"
class="px-4 rounded-md w-full h-[44px] bg-[hsl(218,calc(1*7.9%),27.3%)] placeholder:text-[hsl(218,calc(1*4.6%),46.9%)] flex flex-row"> class="px-4 rounded-md w-full h-[44px] bg-[hsl(218,calc(1*7.9%),27.3%)] placeholder:text-[hsl(218,calc(1*4.6%),46.9%)] flex flex-row">
@@ -55,32 +68,37 @@
<script lang="ts"> <script lang="ts">
import { useGlobalStore } from '~/stores/store'; import { useGlobalStore } from '~/stores/store';
import { io } from 'socket.io-client' import { io } from 'socket.io-client'
import { IChannel, IInviteCode, IMessage } from '~/types';
export default { export default {
props: ['server'], props: ['server'],
data() { data() {
return { return {
user: useGlobalStore().user, user: storeToRefs(useGlobalStore()).user,
messageContent: '', messageContent: '',
conversation: this.server.messages conversation: this.server.messages as IMessage[],
canSendNotifications: false
} }
}, },
mounted() { mounted() {
const route = useRoute() const route = useRoute()
const socket = io(); const socket = io();
const conversationDiv = document.getElementById('conversation-pane'); Notification.requestPermission().then((result) => {
if (!conversationDiv) throw new Error('wtf'); const permission = (result === 'granted') ? true : false
setTimeout(() => { this.canSendNotifications = permission
conversationDiv.scrollTop = conversationDiv.scrollHeight; });
})
const conversationDiv = document.getElementById('conversation-pane');
if (!conversationDiv) throw new Error('conversation div not found')
this.scrollToBottom()
socket.on('connect', () => {
// listen for messages from the server
socket.on(`message-${route.params.id}`, (ev) => { socket.on(`message-${route.params.id}`, (ev) => {
const { message } = ev const { message } = ev
console.log(message.userId, this.user.id, message, this.conversation) if (message.creator.id === this.user.id) return;
if (message.userId == this.user.id) return; if (!document.hasFocus()) {
new Notification(`Message from @${message.creator.username}`, { body: message.body, tag: message.serverId });
}
this.conversation.push(message) this.conversation.push(message)
@@ -88,19 +106,50 @@ export default {
if (!lastElementChild) return; if (!lastElementChild) return;
setTimeout(() => { setTimeout(() => {
console.log(conversationDiv.scrollTop, conversationDiv.scrollHeight, conversationDiv.clientHeight, lastElementChild.clientHeight, (conversationDiv.scrollHeight - conversationDiv.clientHeight) - lastElementChild.clientHeight)
if (conversationDiv.scrollTop + 11.2 < (conversationDiv.scrollHeight - conversationDiv.clientHeight) - lastElementChild.clientHeight) return; if (conversationDiv.scrollTop + 11.2 < (conversationDiv.scrollHeight - conversationDiv.clientHeight) - lastElementChild.clientHeight) return;
conversationDiv.scrollTop = conversationDiv.scrollHeight; conversationDiv.scrollTop = conversationDiv.scrollHeight;
}) })
})
}); });
}, },
// updated() {
// const route = useRoute()
// const socket = io();
// const conversationDiv = document.getElementById('conversation-pane');
// if (!conversationDiv) throw new Error('conversation div not found')
// this.scrollToBottom()
// socket.removeAllListeners('connect')
// socket.on('connect', () => {
// // listen for messages from the server
// socket.on(`message-${route.params.id}`, (ev) => {
// const { message } = ev
// console.log(message.userId, this.user.id, message, this.conversation)
// if (message.userId == this.user.id) return;
// this.conversation.push(message)
// const lastElementChild = conversationDiv.children[0]?.lastElementChild
// if (!lastElementChild) return;
// setTimeout(() => {
// console.log(conversationDiv.scrollTop, conversationDiv.scrollHeight, conversationDiv.clientHeight, lastElementChild.clientHeight, (conversationDiv.scrollHeight - conversationDiv.clientHeight) - lastElementChild.clientHeight)
// if (conversationDiv.scrollTop + 11.2 < (conversationDiv.scrollHeight - conversationDiv.clientHeight) - lastElementChild.clientHeight) return;
// conversationDiv.scrollTop = conversationDiv.scrollHeight;
// })
// })
// });
// },
methods: { methods: {
async sendMessage() { async sendMessage() {
const route = useRoute() const route = useRoute()
if (!this.messageContent) return; if (!this.messageContent) return;
const { message } = await $fetch(`/api/channels/sendMessage`, { method: 'post', body: { body: this.messageContent, channelId: route.params.id } }) const message: IChannel = await $fetch(`/api/channels/sendMessage`, { method: 'post', body: { body: this.messageContent, channelId: route.params.id } })
if (!message) return;
if (this.conversation.includes(message)) return;
this.conversation.push(message) this.conversation.push(message)
this.messageContent = ''; this.messageContent = '';
@@ -110,6 +159,18 @@ export default {
conversationDiv.scrollTop = conversationDiv.scrollHeight; conversationDiv.scrollTop = conversationDiv.scrollHeight;
}) })
}, },
async joinServer(invite: IInviteCode) {
const { server } = await $fetch('/api/guilds/joinGuild', { method: 'POST', body: { inviteId: invite.id } })
if (!server) return;
this.user.servers?.push(server)
},
scrollToBottom() {
const conversationDiv = document.getElementById('conversation-pane');
if (!conversationDiv) throw new Error('wtf');
setTimeout(() => {
conversationDiv.scrollTop = conversationDiv.scrollHeight;
})
}
// resizeTextarea() { // resizeTextarea() {
// const textArea = document.getElementById('messageBox') // const textArea = document.getElementById('messageBox')
// const textBox = document.getElementById('textBox') // const textBox = document.getElementById('textBox')

View File

@@ -15,16 +15,37 @@
<div class="w-full" <div class="w-full"
v-else> v-else>
<div class="flex p-4 border-b border-zinc-600/80"> <div class="flex p-4 border-b border-zinc-600/80">
<h4 class="text-lg font-semibold w-fit "> <h4 class="text-lg font-semibold grid gap-1 grid-cols-[1fr_28px] w-full">
{{ server.name }} <span>{{ server.name }}</span>
<button class="cursor-pointer p-1 hover:backdrop-brightness-110 transition-all">
<span class="h-fit w-[20px]">
<svg xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24">
<path fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m6 9l6 6l6-6" />
</svg>
</span>
</button>
</h4> </h4>
</div> </div>
<div class="flex gap-y-1.5 px-1.5 mt-2 flex-col"> <div class="flex gap-y-1.5 px-1.5 mt-2 flex-col">
<div class="flex text-center hover:bg-zinc-600/70 px-2 py-1.5 w-full transition-colors rounded drop-shadow-sm" <button @click="createInvite"
v-if="userIsOwnerOrAdmin">make invite</button>
<button
class="flex text-center hover:bg-zinc-600/70 px-2 py-1.5 w-full transition-colors rounded drop-shadow-sm gap-1/5 cursor-pointer"
v-for="channel in server.channels" v-for="channel in server.channels"
@click="openChannel(channel.id)"
:key="channel.id"> :key="channel.id">
<svg width="24" <span>
height="24" <svg class="text-zinc-300 my-auto"
width="20"
height="20"
viewBox="0 0 24 24"> viewBox="0 0 24 24">
<path fill="none" <path fill="none"
stroke="currentColor" stroke="currentColor"
@@ -32,8 +53,28 @@
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M5 9h14M5 15h14M11 4L7 20M17 4l-4 16" /> d="M5 9h14M5 15h14M11 4L7 20M17 4l-4 16" />
</svg> {{ channel.name }} </svg>
</div> </span>
<span>{{ channel.name }}</span>
</button>
<button v-if="userIsOwnerOrAdmin"
@click="openCreateChannelModel"
class="flex text-center hover:bg-zinc-600/70 px-2 py-1.5 w-full transition-colors rounded drop-shadow-sm cursor-pointer">
<span>
<svg xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24">
<path fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 5v14m-7-7h14" />
</svg>
</span>
<span>Add channel</span>
</button>
</div> </div>
</div> </div>
@@ -64,10 +105,72 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="createChannelModelOpen"
class="absolute z-10 top-0 bottom-0 left-0 right-0">
<div class="bg-zinc-900/80 w-screen h-screen"
@click="createChannelModelOpen = false">
</div>
<div
class="p-4 z-20 absolute bg-zinc-800 shadow-md rounded-md -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 text-white">
<h2 class="font-semibold text-xl">
Create a channel:
</h2>
<div>
<form @submit.prevent="createChannel"
class="w-3/5">
<input v-model="channelName"
type="text"
class="py-2 px-3 rounded-md mb-2 bg-zinc-700 shadow-md border border-zinc-700/80"
placeholder="Channel name" />
<input type="submit"
class="py-2 px-3 rounded-md bg-zinc-700 shadow-md border border-zinc-700/80" />
</form>
</div>
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { IRole, IServer } from '~/types';
export default { export default {
data() {
return {
createChannelModelOpen: false,
channelName: '',
userIsOwnerOrAdmin: false
}
},
mounted() {
setTimeout(() => {
if (!this.server.roles) {
console.log(this.server)
throw new Error('server must be null');
}
this.userIsOwnerOrAdmin = this.server.roles.find((e: IRole) => e.users.some((el) => el.id === this.user.id)).owner ||
this.server.roles.find((e: IRole) => e.users.some((el) => el.id === this.user.id)).administrator
})
},
methods: {
openCreateChannelModel() {
this.createChannelModelOpen = true;
},
async createChannel() {
const channel = await $fetch(`/api/guilds/${this.server.id}/addChannel`, { method: 'POST', body: { channelName: this.channelName } })
this.server.channels.push(channel)
this.createChannelModelOpen = false;
},
openChannel(id: string) {
const router = useRouter()
router.push({ params: { id } })
},
async createInvite() {
const inviteCode = await $fetch(`/api/guilds/${this.server.id}/createInvite`, { method: 'POST' })
},
},
props: ['server', 'user'] props: ['server', 'user']
} }
</script> </script>

View File

@@ -1,18 +1,25 @@
<template> <template>
<div v-if="user.id" class="flex h-screen max-h-screen text-white"> <Suspense>
<div v-if="user.id"
class="flex h-screen max-h-screen text-white">
<Nav :user="user" /> <Nav :user="user" />
<Sidebar :server="activeServer" <Sidebar :server="activeServer"
:user="user" /> :user="user" />
<div class="w-[calc(100vw-88px-240px)] h-full"> <div class="w-[calc(100vw-88px-240px)] h-full">
<slot /> <slot />
</div> </div>
</div> </div>
<template #fallback>
Loading...
</template>
</Suspense>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Nav, Sidebar } from '~/.nuxt/components' import { Nav, Sidebar } from '~/.nuxt/components'
import { useGlobalStore } from '~/stores/store' import { useGlobalStore } from '~/stores/store'
import { IUser } from '~/types' import { SafeUser } from '~/types'
export default { export default {
data() { data() {
@@ -26,7 +33,7 @@ export default {
const userStore = useGlobalStore() const userStore = useGlobalStore()
const sessionToken = useCookie('sessionToken') const sessionToken = useCookie('sessionToken')
if (userStore.user.id === undefined && sessionToken.value) { if (userStore.user.id === undefined && sessionToken.value) {
const user: IUser = await $fetch('/api/getCurrentUser') const user: SafeUser = await $fetch('/api/getCurrentUser')
if (!user) return; if (!user) return;

View File

@@ -2,6 +2,16 @@
<MessagePane :server="server" /> <MessagePane :server="server" />
</template> </template>
<script async setup lang="ts">
const route = useRoute()
const server: IChannel = await $fetch(`/api/channels/${route.params.id}`)
if (server) {
useGlobalStore().addDM(server);
useGlobalStore().setActive('dms', server.id);
}
</script>
<script lang="ts"> <script lang="ts">
import { useGlobalStore } from '~/stores/store' import { useGlobalStore } from '~/stores/store'
import { IChannel } from '~/types' import { IChannel } from '~/types'
@@ -11,18 +21,6 @@ definePageMeta({
}) })
export default { export default {
async setup() {
const route = useRoute()
const server: IChannel = await $fetch(`/api/channels/${route.params.id}`)
if (!server) return;
useGlobalStore().addDM(server);
useGlobalStore().setActive('dms', server.id);
return {
server,
}
},
async updated() { async updated() {
if (!useGlobalStore().activeServer == this.server) useGlobalStore().setActive('dms', this.server.id) if (!useGlobalStore().activeServer == this.server) useGlobalStore().setActive('dms', this.server.id)
}, },

View File

@@ -7,7 +7,7 @@ const route = useRoute()
const server: IChannel = await $fetch(`/api/channels/${route.params.id}`) const server: IChannel = await $fetch(`/api/channels/${route.params.id}`)
const realServer = useGlobalStore().user.servers.filter((e) => e.channels.some((el) => el.id == route.params.id))[0] const realServer = useGlobalStore().user.servers?.filter((e) => e.channels.some((el) => el.id == route.params.id))[0]
if (realServer) { if (realServer) {
useGlobalStore().addServer(realServer); useGlobalStore().addServer(realServer);

View File

@@ -22,7 +22,7 @@
<script lang="ts"> <script lang="ts">
import { useGlobalStore } from '~/stores/store' import { useGlobalStore } from '~/stores/store'
import { IUser } from '~/types'; import { SafeUser } from '~/types';
definePageMeta({ definePageMeta({
layout: 'clean' layout: 'clean'
@@ -43,7 +43,7 @@ export default {
username: this.username, username: this.username,
password: this.password password: this.password
} }
}) as { userId: string; token: string; user: IUser; } }) as { userId: string; token: string; user: SafeUser; }
const userId = useCookie('userId') const userId = useCookie('userId')
userId.value = user.userId userId.value = user.userId

View File

@@ -32,7 +32,7 @@
<script lang="ts"> <script lang="ts">
import { NuxtLink } from '~/.nuxt/components'; import { NuxtLink } from '~/.nuxt/components';
import { useGlobalStore } from '~/stores/store' import { useGlobalStore } from '~/stores/store'
import { IUser } from '~/types'; import { SafeUser } from '~/types';
definePageMeta({ definePageMeta({
layout: 'clean' layout: 'clean'
@@ -55,7 +55,7 @@ export default {
email: this.email, email: this.email,
password: this.password password: this.password
} }
}) as { userId: string; token: string; user: IUser; } }) as { userId: string; token: string; user: SafeUser; }
const userId = useCookie('userId') const userId = useCookie('userId')
userId.value = user.userId userId.value = user.userId

View File

@@ -13,10 +13,10 @@ model User {
username String @unique username String @unique
passwordhash String passwordhash String
servers Server[] servers Server[]
serverId String?
messages Message[] messages Message[]
session Session[] session Session[]
channels Channel[] channels Channel[]
roles Role[]
} }
model Server { model Server {
@@ -24,6 +24,18 @@ model Server {
name String name String
participants User[] participants User[]
channels Channel[] channels Channel[]
roles Role[]
InviteCode InviteCode[]
}
model Role {
id String @id @default(cuid())
name String
administrator Boolean @default(false)
owner Boolean @default(false)
users User[]
server Server? @relation(fields: [serverId], references: [id])
serverId String?
} }
model Channel { model Channel {
@@ -32,7 +44,7 @@ model Channel {
server Server? @relation(fields: [serverId], references: [id]) server Server? @relation(fields: [serverId], references: [id])
serverId String? serverId String?
messages Message[] messages Message[]
DM Boolean? DM Boolean @default(false)
dmParticipants User[] dmParticipants User[]
} }
@@ -43,6 +55,18 @@ model Message {
creator User @relation(fields: [userId], references: [id]) creator User @relation(fields: [userId], references: [id])
userId String userId String
channelId String channelId String
invites InviteCode[]
}
model InviteCode {
id String @id @default(cuid())
server Server @relation(fields: [serverId], references: [id])
expires Boolean @default(false)
expiryDate DateTime?
maxUses Int @default(0)
serverId String
Message Message? @relation(fields: [messageId], references: [id])
messageId String?
} }
model Session { model Session {

View File

@@ -1,4 +1,4 @@
import { IChannel, IServer, IUser } from '../../../../types' import { IChannel, IServer, SafeUser } from '../../../../types'
import { PrismaClient } from '@prisma/client' import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient() const prisma = new PrismaClient()
@@ -20,11 +20,47 @@ export default defineEventHandler(async (event) => {
where: { where: {
id: event.context.params.id id: event.context.params.id
}, },
include: { select: {
messages: true, id: true,
dmParticipants: true name: true,
server: {
select: {
id: true
} }
}) as IChannel },
messages: {
select: {
id: true,
body: true,
creator: {
select: {
id: true,
username: true
}
},
invites: {
select: {
id: true,
server: {
select: {
id: true,
name: true
}
}
}
}
}
},
DM: true,
dmParticipants: {
select: {
id: true,
username: true
}
},
serverId: true,
}
}) as IChannel | null;
if (!channel) { if (!channel) {
event.node.res.statusCode = 404; event.node.res.statusCode = 404;
@@ -39,11 +75,12 @@ export default defineEventHandler(async (event) => {
id: channel.serverId id: channel.serverId
}, },
include: { include: {
participants: true participants: true,
roles: true
} }
}) as IServer }) as IServer | null;
const userInServer: Array<IUser> = server.participants.filter((e: IUser) => e.id === event.context.user.id) const userInServer: Array<SafeUser> | undefined = server?.participants.filter((e: SafeUser) => e.id === event.context.user.id)
if (!userInServer) { if (!userInServer) {
event.node.res.statusCode = 401; event.node.res.statusCode = 401;

View File

@@ -42,13 +42,25 @@ export default defineEventHandler(async (event) => {
name: 'general', name: 'general',
}, },
] ]
},
roles: {
create: [
{
name: 'owner',
owner: true,
users: {
connect: [{ id: event.context.user.id }]
}
},
]
} }
}, },
include: { include: {
channels: true, channels: true,
participants: true participants: true,
roles: true
} }
}) as IServer }) as IServer;
return server return server
}) })

View File

@@ -1,4 +1,4 @@
import { IChannel, IUser } from '~/types' import { IChannel, SafeUser } from '~/types'
import { PrismaClient } from '@prisma/client' import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient() const prisma = new PrismaClient()
@@ -23,13 +23,13 @@ export default defineEventHandler(async (event) => {
where: { where: {
id: partnerId id: partnerId
} }
}) as IUser }) as SafeUser | null;
const user = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
where: { where: {
id: event.context.user.id id: event.context.user.id
} }
}) as IUser }) as SafeUser | null;
if (!partner) { if (!partner) {
event.node.res.statusCode = 400; event.node.res.statusCode = 400;
@@ -38,6 +38,10 @@ export default defineEventHandler(async (event) => {
} }
} }
if (!user) {
throw new Error('user not found?')
}
const preExistingServer = await prisma.channel.findFirst({ const preExistingServer = await prisma.channel.findFirst({
where: { where: {
name: `${user.id}-${partner.id}`, name: `${user.id}-${partner.id}`,

View File

@@ -1,4 +1,4 @@
import { IChannel, IServer, IUser, IMessage } from '~/types' import { IChannel, IServer, SafeUser, IMessage } from '~/types'
import { Server } from 'socket.io' import { Server } from 'socket.io'
import { PrismaClient } from '@prisma/client' import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient() const prisma = new PrismaClient()
@@ -15,7 +15,7 @@ export default defineEventHandler(async (event) => {
} }
} }
const { body, channelId } = await readBody(event) let { body, channelId } = await readBody(event)
if (!body || !channelId) { if (!body || !channelId) {
event.node.res.statusCode = 400; event.node.res.statusCode = 400;
@@ -43,9 +43,9 @@ export default defineEventHandler(async (event) => {
} }
}) as IServer }) as IServer
const userInServer: Array<IUser> = server.participants.filter((e) => e.id === event.context.user.id) const userInServer: SafeUser | undefined = server.participants.find((e: SafeUser) => e.id === event.context.user.id)
if (userInServer.length > 0) { if (!userInServer) {
event.node.res.statusCode = 401; event.node.res.statusCode = 401;
return { return {
message: 'You must be in the server to send a message.' message: 'You must be in the server to send a message.'
@@ -59,9 +59,9 @@ export default defineEventHandler(async (event) => {
} }
} }
} else { } else {
const userInDM: Array<IUser> | undefined = channel.dmParticipants?.filter((e) => e.id === event.context.user.id) const userInDM: SafeUser | undefined = channel.dmParticipants?.find((e) => e.id === event.context.user.id)
if (!userInDM || userInDM.length > 0) { if (!userInDM) {
event.node.res.statusCode = 401; event.node.res.statusCode = 401;
return { return {
message: 'You must be in the DM to send a message.' message: 'You must be in the DM to send a message.'
@@ -69,6 +69,21 @@ export default defineEventHandler(async (event) => {
} }
} }
const matches = body.match(/<&([a-z]|[0-9]){25}>/g);
let invites: { id: string; }[] = [];
if (matches) {
matches.forEach((e: string) => {
if (!e) return
const opBody = body;
body = body.split(e).join('')
if (opBody === body) return;
const id = e.split('<&')[1]?.split('>')[0];
if (!id) return;
invites.push({ id });
});
}
const message = await prisma.message.create({ const message = await prisma.message.create({
data: { data: {
body, body,
@@ -81,16 +96,34 @@ export default defineEventHandler(async (event) => {
connect: { connect: {
id: channelId id: channelId
} }
},
invites: {
connect: invites
} }
}, },
include: { select: {
creator: true id: true,
body: true,
creator: {
select: {
id: true,
username: true,
}
},
invites: {
select: {
id: true,
server: true,
expires: true,
expiryDate: true,
maxUses: true
}
}
} }
}) as IMessage }) as IMessage
global.io.emit(`message-${channel.id}`, { message }); global.io.emit(`message-${channel.id}`, { message });
return { return message
message
}
}) })

View File

@@ -1,5 +1,5 @@
import { PrismaClient } from '@prisma/client' import { PrismaClient } from '@prisma/client'
import { IUser } from '../../types' import { SafeUser } from '../../types'
const prisma = new PrismaClient() const prisma = new PrismaClient()
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
@@ -42,13 +42,24 @@ export default defineEventHandler(async (event) => {
id: true, id: true,
username: true username: true
} }
},
roles: {
select: {
id: true,
name: true,
administrator: true,
owner: true,
users: {
select: {
id: true
}
}
}
} }
}, },
}, },
} }
}) as IUser }) as SafeUser | null;
user.passwordhash = undefined;
return user return user
}) })

View File

@@ -0,0 +1,67 @@
import { IChannel, IServer, SafeUser } from '~/types'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default defineEventHandler(async (event) => {
if (!event.context.user.authenticated) {
return {
message: 'You must be logged in to view a channel.'
}
}
if (!event.context.params.id) {
event.node.res.statusCode = 400;
return {
message: 'A serverId is required'
}
}
const { channelName } = await readBody(event)
const server = await prisma.server.findFirst({
where: {
id: event.context.params.id
},
include: {
participants: true,
channels: true,
roles: true
}
}) as IServer | null;
if (!server) {
event.node.res.statusCode = 404;
return {
message: `Server with id "${event.context.params.id}" not found`
}
}
const userInServer: Array<SafeUser> = server.participants.filter((e: SafeUser) => e.id === event.context.user.id)
if (!userInServer) {
event.node.res.statusCode = 401;
return {
message: `You must be in the server to access a channel in that server`
}
}
if (server.channels?.find((e) => e.name === channelName)) {
event.node.res.statusCode = 409;
return {
message: `Channel with name "${channelName}" already exists in server with id "event.context.user.id"`
}
}
const channel = await prisma.channel.create({
data: {
name: channelName,
server: {
connect: {
id: server.id
}
}
}
}) as IChannel
return channel
})

View File

@@ -0,0 +1,66 @@
import { IChannel, IInviteCode, IServer, SafeUser } from '~/types'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default defineEventHandler(async (event) => {
if (!event.context.user.authenticated) {
return {
message: 'You must be logged in to view a channel.'
}
}
if (!event.context.params.id) {
event.node.res.statusCode = 400;
return {
message: 'A serverId is required'
}
}
let body = await readBody(event)
let expires = false;
// if (body.expiryDate) {
// expires = true;
// body.expiryDate = new Date(body.expiryDate).getUTCDate()
// }
const server = await prisma.server.findFirst({
where: {
id: event.context.params.id
},
include: {
participants: true,
channels: true,
roles: true
}
}) as IServer | null;
if (!server) {
event.node.res.statusCode = 404;
return {
message: `Server with id "${event.context.params.id}" not found`
}
}
const userInServerAndPermited: Array<SafeUser> = server.participants.filter((e: SafeUser) => e.id === event.context.user.id && e.roles?.some((el) => el.administer === true || el.owner === false))
if (!userInServerAndPermited) {
event.node.res.statusCode = 401;
return {
message: `You must be in the server or an admin/owner of that server to make an invite code`
}
}
const inviteCode = await prisma.inviteCode.create({
data: {
server: {
connect: {
id: server.id
}
},
maxUses: 0
}
}) as IInviteCode
return inviteCode
})

View File

@@ -20,9 +20,10 @@ export default defineEventHandler(async (event) => {
}, },
include: { include: {
participants: true, participants: true,
channels: true channels: true,
roles: true
} }
}) as IServer }) as IServer | null;
if (!server) { if (!server) {
event.node.res.statusCode = 404; event.node.res.statusCode = 404;

View File

@@ -0,0 +1,65 @@
import { IServer } from '~/types'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default defineEventHandler(async (event) => {
if (!event.context.user.authenticated) return {
message: 'You must be logged in to view a channel.'
}
const { inviteId } = await readBody(event);
if (!inviteId) {
event.node.res.statusCode = 400;
return {
message: 'A inviteId is required'
}
}
const invite = await prisma.inviteCode.findFirst({
where: {
id: inviteId
},
include: {
server: true
}
})
if (!invite) {
event.node.res.statusCode = 404;
return {
message: `Invite with id "${inviteId}" not found`
}
}
// TODO: check if invite is valid
const server = await prisma.server.update({
where: {
id: invite.server.id
},
data: {
participants: {
connect: [
{ id: event.context.user.id }
]
}
},
include: {
participants: true,
channels: true,
roles: true
}
}) as IServer
if (!server) {
event.node.res.statusCode = 404;
return {
message: `Channel with id "${event.context.params.id}" not found`
}
}
return {
server
}
})

View File

@@ -1,7 +1,7 @@
import bcryptjs from "bcryptjs"; import bcryptjs from "bcryptjs";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { PrismaClient } from '@prisma/client' import { PrismaClient } from '@prisma/client'
import { IUser } from "../../types"; import { SafeUser } from "~/types";
const prisma = new PrismaClient() const prisma = new PrismaClient()
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
@@ -25,7 +25,7 @@ export default defineEventHandler(async (event) => {
} }
] ]
} }
}) as IUser }) as SafeUser
if (preExistingUser) { if (preExistingUser) {
event.node.res.statusCode = 409; event.node.res.statusCode = 409;
@@ -42,7 +42,7 @@ export default defineEventHandler(async (event) => {
passwordhash, passwordhash,
email: body.email email: body.email
} }
}) as IUser }) as SafeUser
const token = uuidv4() const token = uuidv4()

View File

@@ -1,25 +0,0 @@
import { IUser } from '~/types'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default defineEventHandler(async (event) => {
if (!event.context.user.authenticated) {
// event.node.res.statusCode = 401;
return {
message: "Unauthenticated"
}
}
const servers = await prisma.user.findFirst({
where: {
id: event.context.user.id
},
include: {
channels: true
}
}) as IUser
servers.passwordhash = undefined;
return servers
})

View File

@@ -1,12 +1,13 @@
import { IUser, IServer, IChannel } from "../types"; import { Ref } from "vue";
import { SafeUser, IServer, IChannel } from "../types";
export const useGlobalStore = defineStore('global', { export const useGlobalStore = defineStore('global', {
state: () => ({ state: () => ({
activeServer: {} as IServer | Record<string, unknown>, activeServer: {} as IServer,
user: {} as IUser user: {} as SafeUser
}), }),
actions: { actions: {
setUser(user: IUser) { setUser(user: SafeUser) {
this.user = user; this.user = user;
}, },
addServer(server: IServer) { addServer(server: IServer) {
@@ -19,17 +20,25 @@ export const useGlobalStore = defineStore('global', {
}, },
setActive(type: string, serverId: string) { setActive(type: string, serverId: string) {
if (serverId === '@me') { if (serverId === '@me') {
this.activeServer = {} this.activeServer = {} as IServer
return; return;
} }
console.log(this.activeServer)
if (!this.user.channels || !this.user.servers) return; if (!this.user.channels || !this.user.servers) return;
type = (type === 'dm') ? 'channels' : 'servers' type = (type === 'dm') ? 'channels' : 'servers'
if (type !== 'channels' && type !== 'servers') return; if (type !== 'channels' && type !== 'servers') return;
const searchableArray: IChannel[] | IServer[] = this["user"][type] const searchableArray: IChannel[] | IServer[] | undefined = this["user"][type]
this.activeServer = searchableArray.find((e: IServer | IChannel) => e.id === serverId) if (!searchableArray) return;
const activeServer = searchableArray.find((e: IServer | IChannel) => e.id === serverId)
console.log(searchableArray, this["user"], activeServer)
if (!activeServer) return;
this.activeServer = activeServer
console.log(this.activeServer)
}, },
}, },
}) })

View File

@@ -2,25 +2,29 @@ export interface IUser {
id: string; id: string;
email: string; email: string;
username: string; username: string;
passwordhash: string | undefined; passwordhash: string;
servers: Array<IServer>; servers?: Array<IServer>;
serverId?: string; channels?: Array<IChannel>;
channels: Array<IChannel>; roles?: Array<IRole>;
} }
export type SafeUser = Omit<Omit<IUser, 'passwordhash'>, 'email'>
export interface IServer { export interface IServer {
id: string; id: string;
name: string; name: string;
channels: Array<IChannel>; channels?: Array<IChannel>;
participants: Array<IUser>; participants: Array<IUser>;
roles?: Array<IRole>;
inviteCode?: Array<IInviteCode>;
} }
export interface IChannel { export interface IChannel {
id: string; id: string;
name: string; name: string;
server?: IServer; server?: IServer;
messages: Array<unknown> messages?: Array<IMessage>
DM?: boolean; DM: boolean;
dmParticipants?: Array<IUser>; dmParticipants?: Array<IUser>;
serverId: string; serverId: string;
} }
@@ -28,8 +32,30 @@ export interface IChannel {
export interface IMessage { export interface IMessage {
id: string; id: string;
body: string; body: string;
creator: IUser; creator: SafeUser;
channel: IChannel; channel?: IChannel;
userId: string; userId: string;
channelId: string; channelId: string;
invites?: IInviteCode[];
}
export interface IInviteCode {
id: string;
server: IServer;
expires: boolean;
expiryDate: Date;
maxUses: number;
serverId: string;
Message: IMessage;
messageId: string;
}
export interface IRole {
id: string;
name: string;
administer: boolean;
owner: boolean;
users: IUser[];
server?: IServer;
serverId?: string;
} }