Базовая система входа и система новостей/оповещений
This commit is contained in:
parent
38b3006746
commit
811856fee4
21 changed files with 1400 additions and 109 deletions
32
src/components/Navbar.astro
Normal file
32
src/components/Navbar.astro
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
const items = [
|
||||
{
|
||||
href: "/",
|
||||
title: "Новости",
|
||||
},
|
||||
{
|
||||
href: "/timetable",
|
||||
title: "Расписание",
|
||||
},
|
||||
{
|
||||
href: "/logout",
|
||||
title: "Выйти",
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<nav class="navbar navbar-expand-lg bg-primary text-white">
|
||||
<div class="container">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0 text-white">
|
||||
{
|
||||
items.map((e) => (
|
||||
<li class="nav-item">
|
||||
<a href={e.href} class="active nav-link text-white" aria-current="page">
|
||||
{e.title}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
128
src/components/NewsBlock.astro
Normal file
128
src/components/NewsBlock.astro
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
import { getNewsAndAlerts, getSessionUser } from "../db";
|
||||
|
||||
const { news, alerts } = await getNewsAndAlerts();
|
||||
|
||||
const sessId = Astro.cookies.get("session").value!;
|
||||
const user = (await getSessionUser(sessId))!;
|
||||
|
||||
const formatDate = (dt: Date) => {
|
||||
return (
|
||||
("0" + dt.getDay()).substr(-2) +
|
||||
"." +
|
||||
("0" + (0 + dt.getMonth())).substr(-2) +
|
||||
"." +
|
||||
dt.getFullYear() +
|
||||
" в " +
|
||||
dt.getHours() +
|
||||
":" +
|
||||
("0" + dt.getMinutes()).substr(-2)
|
||||
);
|
||||
};
|
||||
---
|
||||
|
||||
<div class="container">
|
||||
{user.is_admin ? (
|
||||
<a class="btn btn-sm btn-primary d-block mt-2" href="/articleEditor">
|
||||
Новая статья
|
||||
</a>
|
||||
<hr />
|
||||
) : null}
|
||||
{
|
||||
alerts.map((article, idx) => (
|
||||
<div class:list={[idx === 0 ? "mt-2" : ""]}>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h4 class="alert-heading">{article.title}</h4>
|
||||
<h6>{formatDate(article.created_at)}</h6>
|
||||
<p data-article={article.message} />
|
||||
{user.is_admin ? (
|
||||
<hr />
|
||||
<div class="d-flex gap-3">
|
||||
<button class="btn btn-sm btn-danger flex-grow-1" onclick={`deleteArticle(${article.id})`}>
|
||||
Удалить
|
||||
</button>
|
||||
<a class="btn btn-sm btn-warning flex-grow-1" href={`/articleEditor?id=${article.id}`}>
|
||||
Редактировать
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{ alerts.length > 0 ? <hr /> : null }
|
||||
{ news.length > 0 ?
|
||||
news.map((article, idx) => (
|
||||
<div class:list={[idx === 0 ? "mt-2" : ""]}>
|
||||
<div class="alert alert-info" role="alert">
|
||||
<h4 class="alert-heading">{article.title}</h4>
|
||||
<h6>{formatDate(article.created_at)}</h6>
|
||||
<p data-article={article.message} />
|
||||
{user.is_admin ? (
|
||||
<hr />
|
||||
<div class="d-flex gap-3">
|
||||
<button class="btn btn-sm btn-danger flex-grow-1" onclick={`deleteArticle(${article.id})`}>
|
||||
Удалить
|
||||
</button>
|
||||
<a class="btn btn-sm btn-warning flex-grow-1" href={`/articleEditor?id=${article.id}`}>
|
||||
Редактировать
|
||||
</a>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)) :
|
||||
<div class="mt-2">
|
||||
<div class="alert alert-success" role="alert">
|
||||
<h4 class="alert-heading m-0">Новостей нет</h4>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{user.is_admin ?
|
||||
<script is:inline>
|
||||
async function deleteArticle(articleId) {
|
||||
try {
|
||||
const resp = await fetch(`/articles/delete?id=${articleId}`, {
|
||||
method: "POST"
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (json.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
throw new Error(json.reason)
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
}
|
||||
</script> : null}
|
||||
<script src ="https://cdn.jsdelivr.net/npm/editorjs-html@latest/build/edjsHTML.js"> </script>
|
||||
<script is:inline>
|
||||
function checklist(data) {
|
||||
const items = data.data.items.reduce(
|
||||
(acc, item) => acc + `<div class="form-check"><input class="form-check-input" type="checkbox"${item.checked ? " checked" : ""} disabled/><label class="form-check-label">${item.text}</label></div>`,
|
||||
""
|
||||
);
|
||||
return `<div>${items}</div>`;
|
||||
}
|
||||
function quote(data) {
|
||||
const {text, caption} = data.data;
|
||||
if (caption && caption.length > 0) {
|
||||
return `<figure><blockquote class="blockquote"><p>${text}</p></blockquote><figcaption class="blockquote-footer">${caption}</figcaption></figure>`;
|
||||
}
|
||||
return `<blockquote class="blockquote"><p>${text}</p></blockquote>`;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const edjsParser = edjsHTML({ checklist, quote });
|
||||
document.querySelectorAll("[data-article]").forEach(e => {
|
||||
e.innerHTML = edjsParser.parse(JSON.parse(e.dataset["article"])).reduce((a,b) => a+b, "");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style is:inline>
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
0
src/data/user.ts
Normal file
0
src/data/user.ts
Normal file
146
src/db.ts
Normal file
146
src/db.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { PrismaClient, users } from "@prisma/client";
|
||||
import { blake3 } from "@noble/hashes/blake3";
|
||||
import { bytesToHex as toHex } from "@noble/hashes/utils";
|
||||
|
||||
const client = new PrismaClient();
|
||||
|
||||
export async function getSessionUser(sessionId: string) {
|
||||
const dbSession = await client.user_session.findFirst({
|
||||
where: {
|
||||
id: sessionId,
|
||||
},
|
||||
});
|
||||
const user = await client.users.findFirst({
|
||||
where: {
|
||||
id: dbSession!.usersId,
|
||||
},
|
||||
});
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function getUserSession(sessionId: string) {
|
||||
const dbSession = await client.user_session.findFirst({
|
||||
where: {
|
||||
id: sessionId,
|
||||
},
|
||||
});
|
||||
return dbSession;
|
||||
}
|
||||
|
||||
export async function destroySession(sessionId: string) {
|
||||
try {
|
||||
await client.user_session.delete({
|
||||
where: {
|
||||
id: sessionId,
|
||||
},
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export async function isThereAnyAdmins() {
|
||||
const user = await client.users.findMany({
|
||||
where: {
|
||||
is_admin: true,
|
||||
},
|
||||
});
|
||||
return user.length > 0;
|
||||
}
|
||||
|
||||
export async function getUserFromAuth(login: string, password: string) {
|
||||
const user = await client.users.findFirst({
|
||||
where: {
|
||||
login: login,
|
||||
pass: toHex(blake3(password)),
|
||||
},
|
||||
});
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function createUser(login: string, password: string) {
|
||||
const anyAdmins = await isThereAnyAdmins();
|
||||
const user = await client.users.create({
|
||||
data: {
|
||||
login: login,
|
||||
pass: toHex(blake3(password)),
|
||||
is_admin: !anyAdmins,
|
||||
},
|
||||
});
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function createSession(user: users) {
|
||||
const session = await client.user_session.create({
|
||||
data: {
|
||||
usersId: user!.id,
|
||||
},
|
||||
});
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function getNewsAndAlerts() {
|
||||
const allNews = await client.news.findMany({
|
||||
orderBy: {
|
||||
created_at: "desc",
|
||||
},
|
||||
});
|
||||
const news = [];
|
||||
const alerts = [];
|
||||
for (let n of allNews) {
|
||||
if (n.is_alert) {
|
||||
alerts.push(n);
|
||||
} else {
|
||||
news.push(n);
|
||||
}
|
||||
}
|
||||
return {
|
||||
news,
|
||||
alerts,
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteArticle(articleId: number) {
|
||||
const result = await client.news.delete({
|
||||
where: {
|
||||
id: articleId,
|
||||
},
|
||||
});
|
||||
|
||||
return result !== null;
|
||||
}
|
||||
|
||||
export async function createArticle(title: string, data: string, isAlert: boolean) {
|
||||
const article = await client.news.create({
|
||||
data: {
|
||||
title: title,
|
||||
message: data,
|
||||
is_alert: isAlert,
|
||||
},
|
||||
});
|
||||
|
||||
return article !== null;
|
||||
}
|
||||
|
||||
export async function updateArticle(id: number, title: string, data: string, isAlert: boolean) {
|
||||
const article = await client.news.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: {
|
||||
title: title,
|
||||
message: data,
|
||||
is_alert: isAlert,
|
||||
},
|
||||
});
|
||||
|
||||
return article !== null;
|
||||
}
|
||||
|
||||
export async function getArticle(articleId: number) {
|
||||
const article = await client.news.findFirst({
|
||||
where: {
|
||||
id: articleId,
|
||||
},
|
||||
});
|
||||
|
||||
return article;
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
---
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const { title } = Astro.props;
|
||||
|
|
@ -8,28 +10,14 @@ const { title } = Astro.props;
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
<style is:global>
|
||||
:root {
|
||||
--accent: 124, 58, 237;
|
||||
--accent-gradient: linear-gradient(45deg, rgb(var(--accent)), #da62c4 30%, white 60%);
|
||||
}
|
||||
html {
|
||||
font-family: system-ui, sans-serif;
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
code {
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
199
src/pages/articleEditor.astro
Normal file
199
src/pages/articleEditor.astro
Normal 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: {
|
||||
"Couldn’t 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>
|
||||
42
src/pages/articles/create.ts
Normal file
42
src/pages/articles/create.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
30
src/pages/articles/delete.ts
Normal file
30
src/pages/articles/delete.ts
Normal 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
38
src/pages/gate/login.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
29
src/pages/gate/register.ts
Normal file
29
src/pages/gate/register.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
|
|
@ -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
48
src/pages/login.astro
Normal 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
8
src/pages/logout.ts
Normal 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
48
src/pages/register.astro
Normal 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
31
src/pages/uploadFile.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue