Базовая система входа и система новостей/оповещений

This commit is contained in:
Artem VV 2023-05-19 21:15:25 +07:00
parent 38b3006746
commit 811856fee4
21 changed files with 1400 additions and 109 deletions

View file

@ -0,0 +1,199 @@
---
import Layout from "../layouts/Layout.astro";
import { getSessionUser, getArticle } from "../db";
import Navbar from "../components/Navbar.astro";
import type { news } from "@prisma/client";
const sessId = Astro.cookies.get("session").value!;
const user = await getSessionUser(sessId);
if (user === null || !user.is_admin) {
return Astro.redirect("/login");
}
let article: news | null = null;
const articleId = parseInt(Astro.url.searchParams.get("id") ?? "");
if (articleId) {
article = await getArticle(articleId);
}
---
<Layout title="Создание новости">
<main data-article={article?.message} data-articleId={articleId}>
<Navbar />
<div class="container mt-5" style="max-width: 650px;">
<div class="d-flex">
<button type="button" class="btn btn-sm btn-success flex-fill" id="saveBtn">Сохранить</button>
</div>
<label for="title" class="form-label mt-4">Заголовок</label>
<input type="text" id="title" class="form-control" value={article ? article.title : ""} />
<div class="form-check">
{
article && article.is_alert ? (
<input class="form-check-input" type="checkbox" id="isAlert" checked />
) : (
<input class="form-check-input" type="checkbox" id="isAlert" />
)
}
<label class="form-check-label" for="isAlert">Оповещение</label>
</div>
<div id="editorjs" class="mt-4">
<label for="title">Текст</label>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@latest"></script>
<script is:inline>
async function saveArticle(articleTitle, articleData, articleIsAlert) {
if (!articleTitle || articleData.blocks.length === 0 || (articleIsAlert !== true && articleIsAlert !== false)) {
alert("Проверьте ввод!");
return;
}
const fd = new FormData();
fd.append("title", articleTitle);
fd.append("data", JSON.stringify(articleData));
fd.append("isAlert", JSON.stringify(articleIsAlert));
try {
const articleId = parseInt(document.querySelector("[data-articleid]").dataset["articleid"]);
if (Number.isInteger(articleId)) {
fd.append("isUpdate", JSON.stringify(true));
fd.append("articleId", articleId);
}
} catch (e) {}
try {
const resp = await fetch("/articles/create", {
method: "POST",
body: fd,
});
const json = await resp.json();
if (json.ok) {
location.href = "/";
} else {
throw new Error(json.reason);
}
} catch (e) {
console.group("Ошибка сохранения статьи");
console.error(e);
console.groupEnd();
alert("Не удалось сохранить! \nПроверьте консоль для подробностей");
}
}
document.addEventListener("DOMContentLoaded", () => {
const editor = new EditorJS({
holder: "editorjs",
placeholder: "Начните писать",
tools: {
header: Header,
quote: Quote,
checklist: Checklist,
list: List,
embed: Embed,
image: {
class: ImageTool,
title: "Изображение",
config: {
endpoints: {
byFile: "/uploadFile",
},
},
},
},
i18n: {
messages: {
ui: {
blockTunes: {
toggler: {
"Click to tune": "Нажмите, чтобы настроить",
"or drag to move": "или перетащите",
},
},
inlineToolbar: {
converter: {
"Convert to": "Конвертировать в",
},
},
toolbar: {
toolbox: {
Add: "Добавить",
},
},
},
toolNames: {
Text: "Параграф",
Heading: "Заголовок",
List: "Список",
Warning: "Примечание",
Checklist: "Чеклист",
Quote: "Цитата",
Code: "Код",
Delimiter: "Разделитель",
"Raw HTML": "HTML-фрагмент",
Table: "Таблица",
Link: "Ссылка",
Marker: "Маркер",
Bold: "Полужирный",
Italic: "Курсив",
InlineCode: "Моноширинный",
Image: "Изображение",
},
tools: {
link: {
"Add a link": "Вставьте ссылку",
},
stub: {
"The block can not be displayed correctly.": "Блок не может быть отображен",
},
image: {
"Couldnt upload image. Please try another.": "Не удалось загрузить изображение. Попробуйте ещё раз.",
},
},
blockTunes: {
delete: {
Delete: "Удалить",
},
moveUp: {
"Move up": "Переместить вверх",
},
moveDown: {
"Move down": "Переместить вниз",
},
},
},
},
});
const titleInput = document.getElementById("title");
const isAlertInput = document.getElementById("isAlert");
document.getElementById("saveBtn").addEventListener("click", async () => {
const articleData = await editor.save();
saveArticle(titleInput.value, articleData, isAlertInput.checked);
});
editor.isReady.then(() => {
try {
const article = JSON.parse(document.querySelector("[data-article]").dataset["article"]);
if (article !== null) {
editor.render(article);
}
} catch (e) {}
});
});
</script>
</Layout>

View file

@ -0,0 +1,42 @@
import type { APIContext } from "astro";
import { createArticle, updateArticle } from "../../db";
export async function post({ request, url }: APIContext) {
const response: { ok: boolean; reason?: string } = {
ok: true,
};
try {
const fd = await request.formData();
const title = fd.get("title");
const data = fd.get("data");
const isAlert = fd.get("isAlert");
const isUpdate = JSON.parse(fd.get("isUpdate")?.toString() ?? "false");
const articleId = parseInt(fd.get("articleId")?.toString() ?? "");
if (title === null || data === null || isAlert === null || (isUpdate && Number.isNaN(articleId))) {
throw new Error("Неправильный формат запроса");
}
if (isUpdate) {
const updated = await updateArticle(parseInt(articleId?.toString() ?? ""), title.toString(), data.toString(), JSON.parse(isAlert.toString()));
if (!updated) {
throw new Error("Не удалось создать");
}
} else {
const created = await createArticle(title.toString(), data.toString(), JSON.parse(isAlert.toString()));
if (!created) {
throw new Error("Не удалось создать");
}
}
} catch (e: any) {
response.ok = false;
if (e instanceof Error) {
response.reason = e.message;
} else {
response.reason = e.toString();
}
}
return {
body: JSON.stringify(response),
};
}

View file

@ -0,0 +1,30 @@
import type { APIContext } from "astro";
import { deleteArticle } from "../../db";
export async function post({ request, url }: APIContext) {
const response: { ok: boolean; reason?: string } = {
ok: true,
};
try {
const postId = parseInt(url.searchParams.get("id") ?? "");
if (!postId) {
throw new Error("Неправильный формат запроса");
}
const deleted = await deleteArticle(postId);
if (!deleted) {
throw new Error("Не удалось удалить");
}
} catch (e: any) {
response.ok = false;
if (e instanceof Error) {
response.reason = e.message;
} else {
response.reason = e.toString();
}
}
return {
body: JSON.stringify(response),
};
}

38
src/pages/gate/login.ts Normal file
View file

@ -0,0 +1,38 @@
import type { APIContext } from "astro";
import { createSession, getUserFromAuth } from "../../db";
export async function post({ request, redirect, cookies }: APIContext) {
const response: { ok: boolean; reason?: string } = {
ok: true,
};
const formData = await request.formData();
const login = formData.get("login");
const password = formData.get("password");
if (login === null || password === null) {
response.ok = false;
response.reason = "Не предоставлены данные для входа";
} else {
const user = await getUserFromAuth(login!.toString(), password!.toString());
if (user === null) {
response.ok = false;
response.reason = "Неправильная связка логин/пароль";
} else {
const session = await createSession(user!);
if (session !== null) {
response.ok = true;
cookies.set("session", session.id, {
path: "/",
});
} else {
response.ok = false;
response.reason = "Не удалось создать сессию для пользователя";
}
}
}
return {
body: JSON.stringify(response),
};
}

View file

@ -0,0 +1,29 @@
import type { APIContext } from "astro";
import { createUser } from "../../db";
export async function post({ request }: APIContext) {
const response: { ok: boolean; reason?: string } = {
ok: true,
};
const formData = await request.formData();
const login = formData.get("login");
const password = formData.get("password");
if (login === null || password === null) {
response.ok = false;
response.reason = "Не предоставлены данные для регистрации";
} else {
const user = await createUser(login!.toString(), password!.toString());
if (user === null) {
response.ok = false;
response.reason = "Невозможно зарегистрировать пользователя";
} else {
response.ok = true;
}
}
return {
body: JSON.stringify(response),
};
}

View file

@ -1,81 +1,35 @@
---
import Layout from '../layouts/Layout.astro';
import Card from '../components/Card.astro';
import Layout from "../layouts/Layout.astro";
import type { NavbarItemType } from "astro-bootstrap";
import { getUserSession, getNewsAndAlerts, getSessionUser } from "../db";
import NewsBlock from "../components/NewsBlock.astro";
import Navbar from "../components/Navbar.astro";
if (Astro.cookies.has("session")) {
const sessId = Astro.cookies.get("session").value!;
const dbSess = await getUserSession(sessId);
if (dbSess === null) {
Astro.cookies.delete("session");
return Astro.redirect("/login");
}
} else {
return Astro.redirect("/login");
}
const { news, alerts } = await getNewsAndAlerts();
const items: NavbarItemType[] = [
{ text: "Новости", href: "/" },
{ text: "Расписание", href: "/timetable" },
];
---
<Layout title="Welcome to Astro.">
<main>
<h1>Welcome to <span class="text-gradient">Astro</span></h1>
<p class="instructions">
To get started, open the directory <code>src/pages</code> in your project.<br />
<strong>Code Challenge:</strong> Tweak the "Welcome to Astro" message above.
</p>
<ul role="list" class="link-card-grid">
<Card
href="https://docs.astro.build/"
title="Documentation"
body="Learn how Astro works and explore the official API docs."
/>
<Card
href="https://astro.build/integrations/"
title="Integrations"
body="Supercharge your project with new frameworks and libraries."
/>
<Card
href="https://astro.build/themes/"
title="Themes"
body="Explore a galaxy of community-built starter themes."
/>
<Card
href="https://astro.build/chat/"
title="Community"
body="Come say hi to our amazing Discord community. ❤️"
/>
</ul>
</main>
<Layout title="Новости">
<main>
<Navbar />
<NewsBlock />
</main>
</Layout>
<style>
main {
margin: auto;
padding: 1.5rem;
max-width: 60ch;
}
h1 {
font-size: 3rem;
font-weight: 800;
margin: 0;
}
.text-gradient {
background-image: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-size: 400%;
background-position: 0%;
}
.instructions {
line-height: 1.6;
margin: 1rem 0;
border: 1px solid rgba(var(--accent), 25%);
background-color: white;
padding: 1rem;
border-radius: 0.4rem;
}
.instructions code {
font-size: 0.875em;
font-weight: bold;
background: rgba(var(--accent), 12%);
color: rgb(var(--accent));
border-radius: 4px;
padding: 0.3em 0.45em;
}
.instructions strong {
color: rgb(var(--accent));
}
.link-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(24ch, 1fr));
gap: 1rem;
padding: 0;
}
</style>
<style></style>

48
src/pages/login.astro Normal file
View file

@ -0,0 +1,48 @@
---
import Layout from "../layouts/Layout.astro";
import { getUserSession } from "../db";
if (Astro.cookies.has("session")) {
const sessId = Astro.cookies.get("session").value!;
const dbSess = await getUserSession(sessId);
if (dbSess !== null) {
return Astro.redirect("/");
}
Astro.cookies.delete("session");
}
---
<Layout title="Вход">
<main>
<div class="container">
<div class="row justify-content-center mt-5">
<div class="col-lg-4 col-md-6 col-sm-6">
<div class="card shadow">
<div class="card-title text-center border-bottom">
<h2 class="p-3">Вход</h2>
</div>
<div class="card-body">
<form method="POST" action="/gate/login">
<div class="mb-4">
<label for="login" class="form-label">Логин</label>
<input type="text" class="form-control" id="login" id="login" name="login" required />
</div>
<div class="mb-4">
<label for="password" class="form-label">Пароль</label>
<input type="password" class="form-control" id="password" id="login" name="password" required />
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Войти</button>
</div>
</form>
Нет аккаунта? <a href="/register">Зарегистрироваться</a>
</div>
</div>
</div>
</div>
</div>
</main>
</Layout>
<style></style>

8
src/pages/logout.ts Normal file
View file

@ -0,0 +1,8 @@
import type { APIContext } from "astro";
import { destroySession } from "../db";
export async function get({ redirect, cookies }: APIContext) {
const sessionId = cookies.get("session").value ?? "";
await destroySession(sessionId);
return redirect("/");
}

48
src/pages/register.astro Normal file
View file

@ -0,0 +1,48 @@
---
import Layout from "../layouts/Layout.astro";
import { getUserSession } from "../db";
if (Astro.cookies.has("session")) {
const sessId = Astro.cookies.get("session").value!;
const dbSess = await getUserSession(sessId);
if (dbSess !== null) {
return Astro.redirect("/");
}
Astro.cookies.delete("session");
}
---
<Layout title="Регистрация">
<main>
<div class="container">
<div class="row justify-content-center mt-5">
<div class="col-lg-4 col-md-6 col-sm-6">
<div class="card shadow">
<div class="card-title text-center border-bottom">
<h2 class="p-3">Регистрация</h2>
</div>
<div class="card-body">
<form method="POST" action="/gate/register">
<div class="mb-4">
<label for="login" class="form-label">Логин</label>
<input type="text" class="form-control" id="login" id="login" name="login" required />
</div>
<div class="mb-4">
<label for="password" class="form-label">Пароль</label>
<input type="password" class="form-control" id="password" id="login" name="password" required />
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Зарегистрироваться</button>
</div>
</form>
Уже есть аккаунт? <a href="/login">Войти</a>
</div>
</div>
</div>
</div>
</div>
</main>
</Layout>
<style></style>

31
src/pages/uploadFile.ts Normal file
View file

@ -0,0 +1,31 @@
import type { APIContext } from "astro";
import sharp from "sharp";
import { join as joinPath } from "path";
function randomHash() {
return (Math.random() * 1e30).toString(16);
}
export async function post({ request }: APIContext) {
const response: { success: number; file?: { url: string } } = {
success: 1,
};
try {
const fd = await request.formData();
const imageFile = fd.get("image") as Blob;
const fName = `${randomHash()}_${imageFile.name}.jpg`;
await sharp(await imageFile.arrayBuffer())
.jpeg({ mozjpeg: true })
.toFile(joinPath(process.cwd(), "public", "uploads", fName));
response.file = {
url: `/uploads/${fName}`,
};
} catch (e) {
response.success = 0;
}
return {
body: JSON.stringify(response),
};
}