Добавил управление пользователями в чатах

This commit is contained in:
Artem VV 2023-05-25 01:19:13 +07:00
parent 5ebb8ff2ea
commit 7a468a6337
13 changed files with 399 additions and 28 deletions

View file

@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/node": "^5.1.2", "@astrojs/node": "^5.1.2",
"@material-design-icons/font": "^0.14.8",
"@noble/hashes": "^1.3.0", "@noble/hashes": "^1.3.0",
"@popperjs/core": "^2.11.7", "@popperjs/core": "^2.11.7",
"@prisma/client": "4.14.0", "@prisma/client": "4.14.0",

7
pnpm-lock.yaml generated
View file

@ -4,6 +4,9 @@ dependencies:
'@astrojs/node': '@astrojs/node':
specifier: ^5.1.2 specifier: ^5.1.2
version: 5.1.2(astro@2.4.1) version: 5.1.2(astro@2.4.1)
'@material-design-icons/font':
specifier: ^0.14.8
version: 0.14.8
'@noble/hashes': '@noble/hashes':
specifier: ^1.3.0 specifier: ^1.3.0
version: 1.3.0 version: 1.3.0
@ -618,6 +621,10 @@ packages:
resolution: {integrity: sha512-4/RWEeXDO6bocPONheFe6gX/oQdP/bEpv0oL4HqjPP5DCenBSt0mHgahppY49N0CpsaqffdwPq+TlX9CYOq2Dw==} resolution: {integrity: sha512-4/RWEeXDO6bocPONheFe6gX/oQdP/bEpv0oL4HqjPP5DCenBSt0mHgahppY49N0CpsaqffdwPq+TlX9CYOq2Dw==}
dev: false dev: false
/@material-design-icons/font@0.14.8:
resolution: {integrity: sha512-KQ/zk35qFYF+TLv83z0mt021G+CQhgUds+KZ0f65SR5wTu4UzHsXOrsWRhDwrncSP+BqVnk5xab9brwxUoK0rA==}
dev: false
/@noble/hashes@1.3.0: /@noble/hashes@1.3.0:
resolution: {integrity: sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==} resolution: {integrity: sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==}
dev: false dev: false

View file

@ -75,8 +75,8 @@ model chat_message {
id String @id @default(uuid()) id String @id @default(uuid())
text String text String
user users @relation(fields: [userId], references: [id]) user users @relation(fields: [userId], references: [id], onDelete: Cascade)
chat chat @relation(fields: [chatId], references: [id]) chat chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
sendAt DateTime @default(now()) sendAt DateTime @default(now())
@ -88,7 +88,8 @@ model user_in_chat {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
chatId String chatId String
userId Int userId Int
creator Boolean @default(false)
chat chat @relation(fields: [chatId], references: [id]) chat chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
user users @relation(fields: [userId], references: [id]) user users @relation(fields: [userId], references: [id], onDelete: Cascade)
} }

1
public/assets/css/hystmodal.min.css vendored Normal file
View file

@ -0,0 +1 @@
.hystmodal__opened,.hystmodal__shadow{position:fixed;right:0;left:0;overflow:hidden}.hystmodal__shadow{border:none;display:block;width:100%;top:0;bottom:0;pointer-events:none;z-index:98;opacity:0;transition:opacity .15s ease;background-color:#000}.hystmodal__shadow--show{pointer-events:auto;opacity:.6}.hystmodal{position:fixed;top:0;bottom:0;right:0;left:0;overflow:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch;opacity:1;pointer-events:none;display:flex;flex-flow:column nowrap;justify-content:flex-start;z-index:99;visibility:hidden}.hystmodal--active{opacity:1}.hystmodal--active,.hystmodal--moved{pointer-events:auto;visibility:visible}.hystmodal__wrap{flex-shrink:0;flex-grow:0;width:100%;min-height:100%;margin:auto;display:flex;flex-flow:column nowrap;align-items:center;justify-content:center}.hystmodal__window{margin:50px 0;box-sizing:border-box;flex-shrink:0;flex-grow:0;background:#fff;width:600px;max-width:100%;overflow:visible;transition:transform .2s ease 0s,opacity .2s ease 0s;transform:scale(.9);opacity:0}.hystmodal--active .hystmodal__window{transform:scale(1);opacity:1}.hystmodal__close{position:absolute;z-index:10;top:0;right:-40px;display:block;width:30px;height:30px;background-color:transparent;background-position:50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' stroke='%23fff' stroke-linecap='square' stroke-miterlimit='50' stroke-width='2' d='M22 2L2 22'/%3E%3Cpath fill='none' stroke='%23fff' stroke-linecap='square' stroke-miterlimit='50' stroke-width='2' d='M2 2l20 20'/%3E%3C/svg%3E");background-size:100% 100%;border:none;font-size:0;cursor:pointer;outline:none}.hystmodal__close:focus{outline:2px dotted #afb3b9;outline-offset:2px}@media (max-width:767px){.hystmodal__close{top:10px;right:10px;width:24px;height:24px;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' stroke='%23111' stroke-linecap='square' stroke-miterlimit='50' stroke-width='2' d='M22 2L2 22'/%3E%3Cpath fill='none' stroke='%23111' stroke-linecap='square' stroke-miterlimit='50' stroke-width='2' d='M2 2l20 20'/%3E%3C/svg%3E")}.hystmodal__window{margin:0}}

1
public/assets/js/hystmodal.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,7 @@
--- ---
import type { users } from "@prisma/client"; import type { users } from "@prisma/client";
import type { CompositeChat } from "../db"; import type { CompositeChat } from "../db";
import { getUsersNotInChat, isUserChatCreator } from "../db";
export interface Props { export interface Props {
user: users; user: users;
@ -28,14 +29,80 @@ if (chat.messages.length > 0) {
lastMessageAt = chat.messages[chat.messages.length - 1].sendAt; lastMessageAt = chat.messages[chat.messages.length - 1].sendAt;
lastMessageAt.setSeconds(lastMessageAt.getSeconds() + 1); lastMessageAt.setSeconds(lastMessageAt.getSeconds() + 1);
} }
const uics = chat.chat.user_in_chat;
const chatUsers = uics.map((e) => e.user);
const others = await getUsersNotInChat(chatUsers);
const userIsCreator = await isUserChatCreator(user.id, chat.chat.id);
--- ---
<div class="hystmodal" id="settingsModal" aria-hidden="true">
<div class="hystmodal__wrap">
<div class="hystmodal__window card p-2" role="dialog" aria-modal="true">
<button data-hystclose class="hystmodal__close">Закрыть</button>
{
!userIsCreator ? (
<button class="btn btn-sm btn-warning w-100 dlg-btn-dlt-usr" data-userId={`${user.id}`}>Выйти из чата</button>
): (
<button id="btn-delete-chat" class="btn btn-sm btn-danger w-100" data-chatId={`${chat.chat.id}`}>Удалить чат</button>
)
}
<hr>
<h3>Пользователи в чате</h3>
<ul class="list-group">
{
uics.map((e) => (
<li class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex flex-row align-items-center">
{e.user.fullName ?? e.user.login}
<a class="material-icons ms-2 fs-6 text-decoration-none" href={`/user/${e.user.login}`} target="_blank">open_in_new</a>
</div>
{userIsCreator && e.userId !== user.id ? (
<span class="badge bg-danger rounded-pill material-icons dlg-btn-dlt-usr" role="button" data-userId={`${e.userId}`}>
delete
</span>
) : null}
{userIsCreator && e.userId === user.id ? (
<span class="badge bg-success rounded-pill user-select-none material-icons-outlined">
shield
</span>
) : null}
</li>
))
}
</ul>
{userIsCreator ? (
<hr />
<h3>Пользователи не в чате</h3>
<ul class="list-group">
{
others.map((e) => (
<li class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex flex-row align-items-center">
{e.fullName ?? e.login}
<a class="material-icons ms-2 fs-6 text-decoration-none" href={`/user/${e.login}`} target="_blank">open_in_new</a>
</div>
<span class="badge bg-primary rounded-pill material-icons dlg-btn-add-usr" role="button" data-userId={`${e.id}`}>
add
</span>
</li>
))
}
</ul>
) : null}
</div>
</div>
</div>
<div class="col d-flex flex-column" data-chatId={chat.chat.id}> <div class="col d-flex flex-column" data-chatId={chat.chat.id}>
<nav class="navbar navbar-expand-lg navbar-light bg-light"> <nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid"> <div class="container-fluid">
<div> <div>
<p class="mb-1">Чат "{chat.chat.title}"</p> <p class="mb-1">Чат "{chat.chat.title}"</p>
<button data-micromodal-trigger="modal-1">1</button> <div class="d-flex flex-row align-items-center">
<button id="openUsersModalBtn" class="btn btn-sm me-2 material-icons" data-hystmodal="#settingsModal">settings</button>
<small class="text-muted" id="usersPart"> <small class="text-muted" id="usersPart">
Пользователи: { Пользователи: {
chat.chat.user_in_chat chat.chat.user_in_chat
@ -46,6 +113,7 @@ if (chat.messages.length > 0) {
</small> </small>
</div> </div>
</div> </div>
</div>
</nav> </nav>
<div id="chatHolder" class="flex-grow-1" data-lastMessageAt={lastMessageAt}> <div id="chatHolder" class="flex-grow-1" data-lastMessageAt={lastMessageAt}>
{ {
@ -69,7 +137,6 @@ if (chat.messages.length > 0) {
<script> <script>
import type { CompositeChat } from "../db"; import type { CompositeChat } from "../db";
import MicroModal from "micromodal";
const attr = (attrName: string) => document.querySelector(`[${attrName}]`)!.getAttribute(attrName)!; const attr = (attrName: string) => document.querySelector(`[${attrName}]`)!.getAttribute(attrName)!;
const chatId = () => attr("data-chatId"); const chatId = () => attr("data-chatId");
@ -146,6 +213,10 @@ if (chat.messages.length > 0) {
console.error(e); console.error(e);
if (e instanceof Error) { if (e instanceof Error) {
alert(`Ошибка загрузки сообщений: ${e.message}`); alert(`Ошибка загрузки сообщений: ${e.message}`);
if (e.message.startsWith("Нет доступа к чату")) {
location.href = "/chats";
return;
}
} }
} }
} }
@ -170,20 +241,15 @@ if (chat.messages.length > 0) {
console.error(e); console.error(e);
if (e instanceof Error) { if (e instanceof Error) {
alert(`Ошибка отправки сообщения: ${e.message}`); alert(`Ошибка отправки сообщения: ${e.message}`);
if (e.message.startsWith("Нет доступа к чату")) {
location.href = "/chats";
return;
} }
} }
} }
async function showUsers() {
const cid = chatId();
alert(cid);
} }
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
MicroModal.init();
document.getElementById("usersPart")?.addEventListener("click", () => showUsers());
const messageText = document.getElementById("messageText")! as HTMLInputElement; const messageText = document.getElementById("messageText")! as HTMLInputElement;
const sendMessageBtn = document.getElementById("sendMessage")!; const sendMessageBtn = document.getElementById("sendMessage")!;
@ -219,3 +285,117 @@ if (chat.messages.length > 0) {
overflow-y: scroll; overflow-y: scroll;
} }
</style> </style>
<link rel="stylesheet" href="/assets/css/hystmodal.min.css" />
<script is:inline src="/assets/js/hystmodal.min.js"></script>
<script is:inline>
const attr = (attrName) => document.querySelector(`[${attrName}]`).getAttribute(attrName);
const chatId = () => attr("data-chatId");
async function deleteUserFromChat(userId) {
try {
const resp = await fetch("/chatapi/removeUserFromChat", {
method: "POST",
body: JSON.stringify({
chatId: chatId(),
userId
}),
headers: {
"Content-Type": "application/json"
},
});
const json = await resp.json();
if (json.ok) {
location.hash = "chatSettings";
location.reload();
} else {
throw new Error(json.reason);
}
} catch (e) {
console.error(e);
if (e instanceof Error) {
alert(`Ошибка удаления пользователя из чата: ${e.message}`);
}
}
}
async function addUserToChat(userId) {
try {
const resp = await fetch("/chatapi/addUserToChat", {
method: "POST",
body: JSON.stringify({
chatId: chatId(),
userId
}),
headers: {
"Content-Type": "application/json"
},
});
const json = await resp.json();
if (json.ok) {
location.hash = "chatSettings";
location.reload();
} else {
throw new Error(json.reason);
}
} catch (e) {
console.error(e);
if (e instanceof Error) {
alert(`Ошибка добавления пользователя в чат: ${e.message}`);
}
}
}
async function deleteChat() {
try {
const resp = await fetch("/chatapi/deleteChat", {
method: "POST",
body: JSON.stringify({
chatId: chatId()
}),
headers: {
"Content-Type": "application/json"
},
});
const json = await resp.json();
if (json.ok) {
location.href = "/chats"
} else {
throw new Error(json.reason);
}
} catch (e) {
console.error(e);
if (e instanceof Error) {
alert(`Ошибка удаления чата: ${e.message}`);
}
}
}
document.addEventListener("DOMContentLoaded", () => {
const modalsController = new HystModal({
linkAttributeName: "data-hystmodal",
});
if (location.hash === "#chatSettings") {
modalsController.open("#settingsModal");
location.hash = "";
history.pushState("", document.title, window.location.pathname
+ window.location.search);
}
document.querySelectorAll(".dlg-btn-dlt-usr").forEach(e => {
const userId = parseInt(e.getAttribute("data-userId"));
if (!Number.isInteger(userId)) return;
e.addEventListener("click", () => deleteUserFromChat(userId));
});
document.querySelectorAll(".dlg-btn-add-usr").forEach(e => {
const userId = parseInt(e.getAttribute("data-userId"));
if (!Number.isInteger(userId)) return;
e.addEventListener("click", () => addUserToChat(userId));
});
document.getElementById("btn-delete-chat")?.addEventListener("click", () => {
deleteChat();
});
});
</script>

View file

@ -451,7 +451,7 @@ export async function createChatMessage(chatId: string, userId: number, message:
return msg; return msg;
} }
export async function createChat(title: string, userIds: number[]) { export async function createChat(title: string, creatorId: number, userIds: number[]) {
const chat = await client.chat.create({ const chat = await client.chat.create({
data: { data: {
title: title, title: title,
@ -460,6 +460,7 @@ export async function createChat(title: string, userIds: number[]) {
data: userIds.map((x) => { data: userIds.map((x) => {
return { return {
userId: x, userId: x,
creator: x == creatorId,
}; };
}), }),
}, },
@ -498,3 +499,37 @@ export async function removeUserFromChat(userId: number, chatId: string) {
}); });
return chat !== null; return chat !== null;
} }
export async function getUsersNotInChat(chatUsers: users[]) {
const others = await client.users.findMany({
where: {
id: {
notIn: chatUsers.map((e) => e.id),
},
},
});
return others;
}
export async function isUserChatCreator(userId: number, chatId: string) {
const user = await client.user_in_chat.findFirst({
where: {
chatId: chatId,
userId: userId,
creator: true,
},
});
return user !== null;
}
export async function deleteChat(chatId: string) {
const chat = await client.chat.delete({
where: {
id: chatId,
},
});
return chat !== null;
}

View file

@ -26,6 +26,8 @@ const { title } = Astro.props;
</html> </html>
<style is:global> <style is:global>
@import "@material-design-icons/font";
img.rendered-image { img.rendered-image {
max-height: 300px; max-height: 300px;
} }

View file

@ -0,0 +1,46 @@
import type { APIContext } from "astro";
import { getSessionUser, addUserToChat, isUserChatCreator } from "../../db";
import { Prisma } from "@prisma/client";
export async function post({ request, cookies }: APIContext) {
const response: { ok: boolean; reason?: string } = {
ok: true,
};
try {
const sessId = cookies.get("session").value!;
const user = (await getSessionUser(sessId))!;
const json = await request.json();
const chatId = json.chatId as string;
const userId = json.userId;
console.log(chatId, userId, user.id);
if (chatId === null || chatId.length === 0 || !Number.isInteger(userId)) {
throw new Error("Предоставлены некорректные данные для изменения чата");
}
const isCreator = await isUserChatCreator(user.id, chatId);
if (!isCreator) {
throw new Error("Не создатель не может добавлять других пользователей!");
}
const ok = await addUserToChat(userId, chatId);
if (!ok) {
throw new Error("Неизвестная ошибка");
}
response.ok = true;
} catch (e: any) {
response.ok = false;
if (e instanceof Prisma.PrismaClientKnownRequestError) {
response.reason = `Неизвестная ошибка базы данных. Код ${e.code}`;
} else if (e instanceof Error) {
response.reason = e.message.trim();
} else {
response.reason = e.toString().trim();
}
}
return {
body: JSON.stringify(response),
};
}

View file

@ -17,7 +17,7 @@ export async function post({ request, cookies }: APIContext) {
throw new Error("Предоставлены некорректные данные для создания чата"); throw new Error("Предоставлены некорректные данные для создания чата");
} }
await createChat(title.toString(), [...selectedUsers, user.id]); await createChat(title.toString(), user.id, [...selectedUsers, user.id]);
response.ok = true; response.ok = true;
} catch (e: any) { } catch (e: any) {

View file

@ -0,0 +1,44 @@
import type { APIContext } from "astro";
import { getSessionUser, deleteChat, isUserChatCreator } from "../../db";
import { Prisma } from "@prisma/client";
export async function post({ request, cookies }: APIContext) {
const response: { ok: boolean; reason?: string } = {
ok: true,
};
try {
const sessId = cookies.get("session").value!;
const user = (await getSessionUser(sessId))!;
const json = await request.json();
const chatId = json.chatId as string;
if (chatId === null || chatId.length === 0) {
throw new Error("Предоставлены некорректные данные для удаления чата");
}
const isCreator = await isUserChatCreator(user.id, chatId);
if (!isCreator) {
throw new Error("Не создатель не может удалять других пользователей!");
}
const ok = await deleteChat(chatId);
if (!ok) {
throw new Error("Неизвестная ошибка");
}
response.ok = true;
} catch (e: any) {
response.ok = false;
if (e instanceof Prisma.PrismaClientKnownRequestError) {
response.reason = `Неизвестная ошибка базы данных. Код ${e.code}`;
} else if (e instanceof Error) {
response.reason = e.message.trim();
} else {
response.reason = e.toString().trim();
}
}
return {
body: JSON.stringify(response),
};
}

View file

@ -0,0 +1,49 @@
import type { APIContext } from "astro";
import { getSessionUser, removeUserFromChat, isUserChatCreator } from "../../db";
import { Prisma } from "@prisma/client";
export async function post({ request, cookies }: APIContext) {
const response: { ok: boolean; reason?: string } = {
ok: true,
};
try {
const sessId = cookies.get("session").value!;
const user = (await getSessionUser(sessId))!;
const json = await request.json();
const chatId = json.chatId as string;
const userId = json.userId;
if (chatId === null || chatId.length === 0 || !Number.isInteger(userId)) {
throw new Error("Предоставлены некорректные данные для изменения чата");
}
const isCreator = await isUserChatCreator(user.id, chatId);
if (!isCreator && userId !== user.id) {
throw new Error("Не создатель не может удалять других пользователей!");
}
if (isCreator && userId === user.id) {
throw new Error("Нельзя удалить самого себя из чата, если Вы создатель!");
}
const ok = await removeUserFromChat(userId, chatId);
if (!ok) {
throw new Error("Неизвестная ошибка");
}
response.ok = true;
} catch (e: any) {
response.ok = false;
if (e instanceof Prisma.PrismaClientKnownRequestError) {
response.reason = `Неизвестная ошибка базы данных. Код ${e.code}`;
} else if (e instanceof Error) {
response.reason = e.message.trim();
} else {
response.reason = e.toString().trim();
}
}
return {
body: JSON.stringify(response),
};
}

View file

@ -22,6 +22,10 @@ let openChat: CompositeChat | null = null;
if (Astro.url.searchParams.has("openChat")) { if (Astro.url.searchParams.has("openChat")) {
let openChatId = Astro.url.searchParams.get("openChat")!; let openChatId = Astro.url.searchParams.get("openChat")!;
openChat = await getChatMessages(openChatId); openChat = await getChatMessages(openChatId);
if (openChat.chat === null) {
return Astro.redirect("/chats");
}
} }
const sessId = Astro.cookies.get("session").value!; const sessId = Astro.cookies.get("session").value!;