Mod ují, mod u u új

This commit is contained in:
2026-04-13 12:14:35 +02:00
parent f8b06a973f
commit 7a581ce6e7
27 changed files with 573 additions and 183 deletions

View File

@@ -3,3 +3,28 @@
Český anonymní imageboard.
Migrace databází se budou opakovaně rozbíjet před vydáním první verze do produkce. Prosím, počítejte s tím.
## Průběh
- [x] Základní funkce
- [x] Formátování textu
- [x] Standardní + bezpečný tripcode
- [x] Katalog
- [x] Bany, limity, blokace, filtry
- [x] Správa účtú, nástěnek, role
- [x] Podpora de facto všech multimediálních formátů
- [x] Cache optimalizace, indexy v paměti
- [x] Uživatelské funkce, mazání heslem
- [-] Moderátorské funkce
- [-] Domovská strana
- [-] JavaScript na frontendu
### Plánováno
- [-] Lepší logger
- [-] Nastavení stránky a správa assetů z UI
- [-] Přesouvání příspěvků
- [-] phash
- [-] Memeflagy, rizz
- [-] Fortune
- [-] Pořádná dokumentace

View File

@@ -13,7 +13,7 @@
"MANAGE_FILTERS": "Spravovat filtry",
"ALL_FILTERS": "Spravovat filtry (+)",
"MANAGE_REPORTS": "Spravovat hlášení",
"MANAGE_POSTS": "Spravovat příspěvky",
"MANAGE_ATTRIBUTES": "Spravovat atributy příspěvků",
"DELETE_POSTS": "Mazat příspěvky",
"DELETE_FILES": "Mazat soubory",
"SPOILER_FILES": "Spoilerovat soubory",

View File

@@ -20,7 +20,7 @@
"MANAGE_BANS",
"ALL_BANS",
"MANAGE_REPORTS",
"MANAGE_POSTS",
"MANAGE_ATTRIBUTES",
"DELETE_POSTS",
"DELETE_FILES",
"SPOILER_FILES",
@@ -48,7 +48,7 @@
"MANAGE_RESTRICTIONS",
"MANAGE_FILTERS",
"MANAGE_REPORTS",
"MANAGE_POSTS",
"MANAGE_ATTRIBUTES",
"DELETE_POSTS",
"DELETE_FILES",
"SPOILER_FILES",
@@ -87,7 +87,7 @@
"MANAGE_FILTERS",
"ALL_FILTERS",
"MANAGE_REPORTS",
"MANAGE_POSTS",
"MANAGE_ATTRIBUTES",
"DELETE_POSTS",
"DELETE_FILES",
"SPOILER_FILES",

View File

@@ -17,7 +17,7 @@ enum CzchanPerm {
ALL_FILTERS = "ALL_FILTERS",
// Post permissions
MANAGE_REPORTS = "MANAGE_REPORTS",
MANAGE_POSTS = "MANAGE_POSTS",
MANAGE_ATTRIBUTES = "MANAGE_ATTRIBUTES",
DELETE_POSTS = "DELETE_POSTS",
DELETE_FILES = "DELETE_FILES",
SPOILER_FILES = "SPOILER_FILES",

View File

@@ -46,9 +46,9 @@ const zPostAction = z.enum(
"delete_files",
"delete_posts",
"report_posts",
"ban_and_delete",
"delete_and_ban",
"manage_files",
"manage_posts",
"manage_attributes",
"move_posts",
],
"Neplatná akce.",

View File

@@ -19,24 +19,24 @@ const CatalogTile = ({ post }: PropsWithChildren<{ post: Post }>) => (
<br />
<span>
<b>R:</b> {post.t_replies} / <b>F:</b> {post.t_files}{" "}
{post.f_sticky !== 0 && (
{post.f_sticky > 0 && (
<>
<img src="/public/img/icons/sticky.png" />{" "}
<img class="icon" src="/public/img/icons/sticky.png" />{" "}
</>
)}
{post.f_locked && (
<>
<img src="/public/img/icons/locked.png" />{" "}
<img class="icon" src="/public/img/icons/locked.png" />{" "}
</>
)}
{post.f_bumplocked && (
<>
<img src="/public/img/icons/down.png" />{" "}
<img class="icon" src="/public/img/icons/down.png" />{" "}
</>
)}
{post.f_looped && (
<>
<img src="/public/img/icons/loop.png" />
<img class="icon" src="/public/img/icons/loop.png" />
</>
)}
</span>

View File

@@ -97,24 +97,24 @@ const PostComponent = ({
<a href={threadURL(post)}>[Otevřít]</a>{" "}
</>
)}
{post.f_sticky !== 0 && (
{post.f_sticky > 0 && (
<>
<img src="/public/img/icons/sticky.png" />{" "}
<img class="icon" src="/public/img/icons/sticky.png" />{" "}
</>
)}
{post.f_locked && (
<>
<img src="/public/img/icons/locked.png" />{" "}
<img class="icon" src="/public/img/icons/locked.png" />{" "}
</>
)}
{post.f_bumplocked && (
<>
<img src="/public/img/icons/down.png" />{" "}
<img class="icon" src="/public/img/icons/down.png" />{" "}
</>
)}
{post.f_looped && (
<>
<img src="/public/img/icons/loop.png" />{" "}
<img class="icon" src="/public/img/icons/loop.png" />{" "}
</>
)}
</div>

View File

@@ -0,0 +1 @@

View File

@@ -1,13 +1,16 @@
import { File } from "../../../schema/jsonb";
import { PropsWithChildren } from "@kitajs/html";
const Thumbnail = ({ file }: PropsWithChildren<{ file: File }>) => (
const Thumbnail = ({
file,
nospecial = false,
}: PropsWithChildren<{ file: File; nospecial?: boolean }>) => (
<img
class={"thumb thumb-" + file.type}
src={(() => {
if (file.deleted) return "/public/img/thumb/deleted.png";
if (file.spoiler) return "/public/img/thumb/spoiler.png";
if (file.thumb_url) return file.thumb_url;
if (!nospecial && file.spoiler) return "/public/img/thumb/spoiler.png";
if (!nospecial && file.thumb_url) return file.thumb_url;
switch (file.type) {
case "image":

View File

@@ -5,7 +5,7 @@ import { PropsWithChildren } from "@kitajs/html";
const PostActionsForm = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => (
<>
<details class="action-details">
<details class="form-group">
<summary>Akce příspěvku</summary>
<Form>
<FormField label="Kód na odstranění">
@@ -29,48 +29,17 @@ const PostActionsForm = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => (
<Form>
{(canUser(ctx.user, CzchanPerm.DELETE_POSTS) ||
canUser(ctx.user, CzchanPerm.MANAGE_BANS)) && (
<FormButton
submit={
<>
Odstranit/Zabanovat{" "}
<img class="icon" src="/public/img/icons/right.png" />
</>
}
action="ban_and_delete"
/>
<FormButton submit="Vymazat/Zabanovat →" action="delete_and_ban" />
)}
{(canUser(ctx.user, CzchanPerm.DELETE_FILES) ||
canUser(ctx.user, CzchanPerm.SPOILER_FILES)) && (
<FormButton
submit={
<>
Soubory <img class="icon" src="/public/img/icons/right.png" />
</>
}
action="manage_files"
/>
<FormButton submit="Soubory →" action="manage_files" />
)}
{canUser(ctx.user, CzchanPerm.MANAGE_POSTS) && (
<FormButton
submit={
<>
Atributy{" "}
<img class="icon" src="/public/img/icons/right.png" />
</>
}
action="manage_posts"
/>
{canUser(ctx.user, CzchanPerm.MANAGE_ATTRIBUTES) && (
<FormButton submit="Atributy →" action="manage_attributes" />
)}
{canUser(ctx.user, CzchanPerm.MOVE_POSTS) && (
<FormButton
submit={
<>
Přesunout{" "}
<img class="icon" src="/public/img/icons/right.png" />
</>
}
action="move_posts"
/>
<FormButton submit="Přesunout →" action="move_posts" />
)}
{(canUser(ctx.user, CzchanPerm.VIEW_IPS) ||
canUser(ctx.user, CzchanPerm.VIEW_METADATA)) && (
@@ -82,13 +51,12 @@ const PostActionsForm = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => (
</>
);
// @ts-ignore
const anyModTools = (ctx: TemplateCtx) =>
canUser(ctx.user, CzchanPerm.DELETE_POSTS) ||
canUser(ctx.user, CzchanPerm.MANAGE_BANS) ||
canUser(ctx.user, CzchanPerm.DELETE_FILES) ||
canUser(ctx.user, CzchanPerm.SPOILER_FILES) ||
canUser(ctx.user, CzchanPerm.MANAGE_POSTS) ||
canUser(ctx.user, CzchanPerm.MANAGE_ATTRIBUTES) ||
canUser(ctx.user, CzchanPerm.MOVE_POSTS) ||
canUser(ctx.user, CzchanPerm.VIEW_IPS) ||
canUser(ctx.user, CzchanPerm.VIEW_METADATA);

View File

@@ -0,0 +1,122 @@
import { canUser, CzchanPerm } from "../../../../lib/permissions";
import { truncate } from "../../../../lib/util";
import { Post } from "../../../../schema/tables";
import Page from "../../../components/chrome/page";
import PageBody from "../../../components/chrome/page_body";
import PageHead from "../../../components/chrome/page_head";
import { Form, FormField, FormButton } from "../../../components/forms/form";
import IPAddress from "../../../components/primitives/ip_address";
import { TemplateCtx } from "../../../ctx";
import { PropsWithChildren } from "@kitajs/html";
const DeleteAndBanPage = (ctx: TemplateCtx, posts: Post[]) => (
<Page>
<PageHead ctx={ctx} title="Odstranit/Zabanovat" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Odstranit/Zabanovat</h1>
<hr />
<form method="post" action="/mod/actions/post_actions/delete_and_ban">
<table class="data-table">
<thead>
<tr>
<th></th>
<th>ID</th>
<th>IP</th>
<th>Obsah</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr>
<td>
<input
name="posts"
type="checkbox"
value={`${post.board};${post.id}`}
checked
/>
</td>
<td>
&gt;&gt;&gt;/<span safe>{post.board}</span>/{post.id}
</td>
<td>
<IPAddress ctx={ctx} ip={post.ip} />
</td>
<td safe>{truncate(post.content_unformatted, 128)}</td>
</tr>
))}
</tbody>
</table>
<hr />
<DeleteAndBanForm ctx={ctx} />
</form>
<hr />
</PageBody>
</Page>
);
const DeleteAndBanForm = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => (
<div class="form-group">
{canUser(ctx.user, CzchanPerm.DELETE_POSTS) && (
<Form>
<FormField label="Odstranit příspěvky" wrapper>
<input name="delete_posts" type="checkbox" />
</FormField>
<FormField label="↳ Odstranit soubory z disku" wrapper>
<input name="unlink_files" type="checkbox" />
</FormField>
{canUser(ctx.user, CzchanPerm.VIEW_IPS) && (
<FormField label="↳ Odstranit příspěvky od této IP" wrapper>
<input name="delete_ip_posts" type="checkbox" />
</FormField>
)}
</Form>
)}
{canUser(ctx.user, CzchanPerm.MANAGE_BANS) && (
<Form>
<FormField label="Zabanovat uživatele" wrapper>
<input name="ban_users" type="checkbox" />
</FormField>
<FormField label="↳ Rozsah">
<select name="ban_range">
<option value="ip">Jedna IP</option>
<option value="default" selected>
Výchozí (/32, /64)
</option>
<option value="wide">Široký (/24, /56)</option>
<option value="aggressive">Agresivní (/16, /48)</option>
</select>
</FormField>
<FormField label="↳ Důvod">
<input name="ban_reson" type="text" />
</FormField>
<FormField label="↳ Veřejná zpráva">
<input name="ban_message" type="text" />
</FormField>
<FormField
label={
<>
Délka banu <small>(např. 4d2h3m1s)</small>
</>
}
>
<input name="ban_duration" type="text" />
</FormField>
<FormField label="↳ Trvalý" wrapper>
<input name="permanent_ban" type="checkbox" />
</FormField>
<FormField label="↳ Na všech nástěnkách" wrapper>
<input name="global_ban" type="checkbox" />
</FormField>
<FormField label="↳ Bez možnosti odvolání" wrapper>
<input name="unappealable_ban" type="checkbox" />
</FormField>
</Form>
)}
<Form>
<FormButton submit="Potvrdit" action="delete_and_ban" />
</Form>
</div>
);
export { DeleteAndBanPage };

View File

@@ -0,0 +1,114 @@
import { truncate } from "../../../../lib/util";
import { Post } from "../../../../schema/tables";
import Page from "../../../components/chrome/page";
import PageBody from "../../../components/chrome/page_body";
import PageHead from "../../../components/chrome/page_head";
import { Form, FormField, FormButton } from "../../../components/forms/form";
import IPAddress from "../../../components/primitives/ip_address";
import { TemplateCtx } from "../../../ctx";
const ManageAttributesPage = (ctx: TemplateCtx, posts: Post[]) => (
<Page>
<PageHead ctx={ctx} title="Atributy" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Atributy</h1>
<hr />
<form method="post" action="/mod/actions/post_actions/manage_attributes">
<table class="data-table">
<thead>
<tr>
<th></th>
<th>ID</th>
<th>IP</th>
<th>Obsah</th>
<th>Atributy</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr>
<td>
<input
name="posts"
type="checkbox"
value={`${post.board};${post.id}`}
checked={post.thread === null}
disabled={post.thread !== null}
/>
</td>
<td>
&gt;&gt;&gt;/<span safe>{post.board}</span>/{post.id}
</td>
<td>
<IPAddress ctx={ctx} ip={post.ip} />
</td>
<td safe>{truncate(post.content_unformatted, 128)}</td>
<td>
{post.thread !== null && (
<>
<i>Není vlákno</i>{" "}
<img class="icon" src="/public/img/icons/no.png" />{" "}
</>
)}
{post.f_sticky > 0 && (
<>
<img
class="icon"
src="/public/img/icons/sticky.png"
/>{" "}
</>
)}
{post.f_locked && (
<>
<img
class="icon"
src="/public/img/icons/locked.png"
/>{" "}
</>
)}
{post.f_bumplocked && (
<>
<img class="icon" src="/public/img/icons/down.png" />{" "}
</>
)}
{post.f_looped && (
<>
<img class="icon" src="/public/img/icons/loop.png" />{" "}
</>
)}
</td>
</tr>
))}
</tbody>
</table>
<hr />
<ManageAttributesForm />
</form>
<hr />
</PageBody>
</Page>
);
const ManageAttributesForm = () => (
<div class="form-group">
<Form>
<FormField label="Priorita připnutí">
<input name="sticky" type="number" value="0" />
</FormField>
<FormField label="Přepnout zámek" wrapper>
<input name="toggle_lock" type="checkbox" />
</FormField>
<FormField label="Přepnout autosage" wrapper>
<input name="toggle_bumplock" type="checkbox" />
</FormField>
<FormField label="Přepnout cyklus" wrapper>
<input name="toggle_loop" type="checkbox" />
</FormField>
</Form>
<Form>
<FormButton submit="Potvrdit" action="manage_attributes" />
</Form>
</div>
);
export { ManageAttributesPage };

View File

@@ -0,0 +1,122 @@
import { canUser, CzchanPerm } from "../../../../lib/permissions";
import { truncateFilename } from "../../../../lib/util";
import { Post } from "../../../../schema/tables";
import Page from "../../../components/chrome/page";
import PageBody from "../../../components/chrome/page_body";
import PageHead from "../../../components/chrome/page_head";
import Thumbnail from "../../../components/content/thumbnail";
import { Form, FormField, FormButton } from "../../../components/forms/form";
import IPAddress from "../../../components/primitives/ip_address";
import { TemplateCtx } from "../../../ctx";
import { PropsWithChildren } from "@kitajs/html";
const ManageFilesPage = (ctx: TemplateCtx, posts: Post[]) => (
<Page>
<PageHead ctx={ctx} title="Soubory" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">Soubory</h1>
<hr />
<form method="post" action="/mod/actions/post_actions/manage_files">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>IP</th>
<th>Soubory</th>
</tr>
</thead>
<tbody>
{posts.map((post) => (
<tr>
<td>
&gt;&gt;&gt;/<span safe>{post.board}</span>/{post.id}
</td>
<td>
<IPAddress ctx={ctx} ip={post.ip} />
</td>
<td>
<div class="post-files multiple-files">
{post.files.map((file) => (
<div class="post-file mod-thumb">
<div class="post-file-info">
<span safe>
{truncateFilename(file.original_filename, 16)}
</span>{" "}
<input
name="posts"
type="checkbox"
value={`${post.board};${post.id};${file.hash}`}
checked
/>
</div>
<Thumbnail file={file} nospecial />
</div>
))}
</div>
</td>
</tr>
))}
</tbody>
</table>
<hr />
<ManageFilesForm ctx={ctx} />
</form>
<hr />
</PageBody>
</Page>
);
const ManageFilesForm = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => (
<div class="form-group">
{canUser(ctx.user, CzchanPerm.SPOILER_FILES) && (
<Form>
<FormField label="Přidat/Odebrat spoilery" wrapper>
<input name="spoiler_files" type="checkbox" />
</FormField>
<FormField label="↳ Přidat/Odebrat všude" wrapper>
<input name="spoiler_everywhere" type="checkbox" />
</FormField>
</Form>
)}
{canUser(ctx.user, CzchanPerm.DELETE_FILES) && (
<Form>
<FormField label="Vymazat soubory" wrapper>
<input name="delete_files" type="checkbox" />
</FormField>
<FormField label="↳ Vymazat všude" wrapper>
<input name="delete_everywhere" type="checkbox" />
</FormField>
<FormField label="↳ Vymazat z disku" wrapper>
<input name="unlink_files" type="checkbox" />
</FormField>
</Form>
)}
{canUser(ctx.user, CzchanPerm.MANAGE_FILTERS) && (
<Form>
<FormField label="Zakázat soubory (přidat filtr)" wrapper>
<input name="filter_files" type="checkbox" />
</FormField>
<FormField label="↳ Typ filtru">
<select name="filter_type">
<option value="block">block</option>
<option value="ban">ban</option>
</select>
</FormField>
<FormField label="↳ Hodnota filtru">
<input name="filter_value" type="text" />
</FormField>
<FormField label="↳ Priorita filtru">
<input name="filter_priority" type="number" value="0" />
</FormField>
<FormField label="↳ Důvod filtru (poznámka)">
<input name="filter_reason" type="text" />
</FormField>
</Form>
)}
<Form>
<FormButton submit="Potvrdit" action="manage_files" />
</Form>
</div>
);
export { ManageFilesPage };

View File

@@ -38,7 +38,7 @@ export default async (req: Request, res: Response) => {
const form = CreatePostForm.parse(fields);
const uploadedFiles = (uploads["files"] || []).filter(
(file) => file.size !== 0,
(file) => file.size > 0,
);
const ip = req.ipInfo.realIP;
@@ -211,7 +211,7 @@ export default async (req: Request, res: Response) => {
board,
req.ipInfo.atomicIP,
postHash(content_unformatted),
content_unformatted.length !== 0,
content_unformatted.length > 0,
);
if (floodDetected) throw new CzchanError("Příspěvek byl zamítnut.", 400);

View File

@@ -12,6 +12,9 @@ import { Board, Post } from "../../../schema/tables";
import { ZPostActionsForm } from "../../../schema/validation/post";
import { getCtx } from "../../ctx";
import { ResultPage } from "../../templates/result";
import { DeleteAndBanPage } from "./action_pages/delete_and_ban";
import { ManageAttributesPage } from "./action_pages/manage_attributes";
import { ManageFilesPage } from "./action_pages/manage_files";
import bcrypt from "bcrypt";
import { Request, Response } from "express";
import formidable from "formidable";
@@ -32,28 +35,57 @@ export default async (req: Request, res: Response) => {
!boards[post.board].config.private,
);
const ctx = await getCtx(req);
let html;
switch (form.action) {
case "update_spoiler":
if (!form.deletion_code) throw new CzchanError("Kód je povinný.", 400);
await updateSpoiler(form.deletion_code, boards, posts);
html = ResultPage(ctx, ["Zvolená akce byla úspěšně provedena."]);
break;
case "delete_files":
if (!form.deletion_code) throw new CzchanError("Kód je povinný.", 400);
await deleteFiles(form.deletion_code, boards, posts);
html = ResultPage(ctx, ["Zvolená akce byla úspěšně provedena."]);
break;
case "delete_posts":
if (!form.deletion_code) throw new CzchanError("Kód je povinný.", 400);
await deletePosts(form.deletion_code, boards, posts);
html = ResultPage(ctx, ["Zvolená akce byla úspěšně provedena."]);
break;
case "report_posts":
if (!form.report_reason) throw new CzchanError("Důvod je povinný.", 400);
await reportPosts(req, form.report_reason, posts);
html = ResultPage(ctx, ["Zvolená akce byla úspěšně provedena."]);
break;
case "delete_and_ban":
if (
!(
canUser(req.user, CzchanPerm.DELETE_POSTS) ||
canUser(req.user, CzchanPerm.MANAGE_BANS)
)
)
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
html = DeleteAndBanPage(ctx, posts);
break;
case "manage_files":
if (
!(
canUser(req.user, CzchanPerm.DELETE_FILES) ||
canUser(req.user, CzchanPerm.SPOILER_FILES)
)
)
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
html = ManageFilesPage(ctx, posts);
break;
case "manage_attributes":
if (!canUser(req.user, CzchanPerm.MANAGE_ATTRIBUTES))
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
html = ManageAttributesPage(ctx, posts);
break;
}
const ctx = await getCtx(req);
const html = ResultPage(ctx, ["Zvolená akce byla úspěšně provedena."]);
res.send(html);
};

View File

@@ -13,7 +13,7 @@ export default async (req: Request, res: Response) => {
const parser = formidable({ allowEmptyFiles: true, minFileSize: 0 });
const [_, files] = await parser.parse(req);
const files_ = (files["banners"] || []).filter((file) => file.size !== 0);
const files_ = (files["banners"] || []).filter((file) => file.size > 0);
const processedFiles = await processFiles(
req.config,

View File

@@ -103,7 +103,7 @@ const BoardRow = ({
<td>
{ctx.user?.username === board.created_by ||
canUser(ctx.user, CzchanPerm.ALL_BOARDS) ? (
<a href={`/mod/board_config/${board.id}`}>[Upravit]</a>
<a href={`/mod/edit/board_config/${board.id}`}>[Upravit]</a>
) : (
<s class="dead-link">[Upravit]</s>
)}

View File

@@ -1,13 +1,13 @@
import { readBoard } from "../../../db/board";
import { CzchanError } from "../../../lib/error";
import { canUser, CzchanPerm } from "../../../lib/permissions";
import { Board } from "../../../schema/tables";
import { zBoardID } from "../../../schema/validation/board";
import Page from "../../components/chrome/page";
import PageBody from "../../components/chrome/page_body";
import PageHead from "../../components/chrome/page_head";
import BoardConfigForm from "../../components/forms/board_config_form";
import { getCtx, TemplateCtx } from "../../ctx";
import { readBoard } from "../../../../db/board";
import { CzchanError } from "../../../../lib/error";
import { canUser, CzchanPerm } from "../../../../lib/permissions";
import { Board } from "../../../../schema/tables";
import { zBoardID } from "../../../../schema/validation/board";
import Page from "../../../components/chrome/page";
import PageBody from "../../../components/chrome/page_body";
import PageHead from "../../../components/chrome/page_head";
import BoardConfigForm from "../../../components/forms/board_config_form";
import { getCtx, TemplateCtx } from "../../../ctx";
import { Request, Response } from "express";
export default async (req: Request, res: Response) => {

View File

@@ -0,0 +1,70 @@
import { readNewsPost } from "../../../../db/news";
import { CzchanError } from "../../../../lib/error";
import { canUser, CzchanPerm } from "../../../../lib/permissions";
import { News } from "../../../../schema/tables";
import { zNewsID } from "../../../../schema/validation/news";
import Page from "../../../components/chrome/page";
import PageBody from "../../../components/chrome/page_body";
import PageHead from "../../../components/chrome/page_head";
import { Form, FormButton, FormField } from "../../../components/forms/form";
import { getCtx, TemplateCtx } from "../../../ctx";
import { PropsWithChildren } from "@kitajs/html";
import { Request, Response } from "express";
export default async (req: Request, res: Response) => {
const ctx = await getCtx(req);
if (!canUser(req.user, CzchanPerm.MANAGE_NEWS))
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
const news = await readNewsPost(zNewsID.parse(req.params.news));
if (
!(
req.user?.username === news.created_by ||
canUser(req.user, CzchanPerm.ALL_NEWS)
)
) {
throw new CzchanError(
`K upravení novinek ${news.id} nemáš oprávnění.`,
403,
);
}
const html = Template(ctx, news);
res.send(html);
};
const Template = (ctx: TemplateCtx, news: News) => (
<Page>
<PageHead ctx={ctx} title="Novinky" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">
Novinky
<br />(<span safe>{news.subject}</span>)
</h1>
<UpdateForm ctx={ctx} news={news} />
<hr />
</PageBody>
</Page>
);
const UpdateForm = ({
news,
}: PropsWithChildren<{ ctx: TemplateCtx; news: News }>) => (
<form method="post" action="/mod/actions/update_news_content">
<input name="news" type="hidden" value={news.id.toString()} />
<Form center>
<FormField label="Předmět">
<input name="subject" type="text" value={news.subject} />
</FormField>
<FormField label="Obsah">
<textarea name="content" rows="10" cols="35" safe>
{news.content_unformatted}
</textarea>
</FormField>
<FormButton submit="Upravit" action="update_news_content" />
</Form>
</form>
);

View File

@@ -1,12 +1,12 @@
import { readUser } from "../../../db/user";
import { canUser, canUserActOn, CzchanPerm } from "../../../lib/permissions";
import { User } from "../../../schema/tables";
import { zUsername } from "../../../schema/validation/user";
import Page from "../../components/chrome/page";
import PageBody from "../../components/chrome/page_body";
import PageHead from "../../components/chrome/page_head";
import { Form, FormButton, FormField } from "../../components/forms/form";
import { getCtx, TemplateCtx } from "../../ctx";
import { readUser } from "../../../../db/user";
import { canUser, canUserActOn, CzchanPerm } from "../../../../lib/permissions";
import { User } from "../../../../schema/tables";
import { zUsername } from "../../../../schema/validation/user";
import Page from "../../../components/chrome/page";
import PageBody from "../../../components/chrome/page_body";
import PageHead from "../../../components/chrome/page_head";
import { Form, FormButton, FormField } from "../../../components/forms/form";
import { getCtx, TemplateCtx } from "../../../ctx";
import { PropsWithChildren } from "@kitajs/html";
import { Request, Response } from "express";

View File

@@ -66,7 +66,7 @@ const UserTable = ({ ctx }: PropsWithChildren<{ ctx: TemplateCtx }>) => (
<th>Oprávnění</th>
<td>
[{ctx.user?.permissions.length}]{" "}
<a href={`/mod/user_permissions/${ctx.user?.username}`}>
<a href={`/mod/edit/user_permissions/${ctx.user?.username}`}>
[Zobrazit]
</a>
</td>

View File

@@ -100,7 +100,7 @@ const NewsRow = ({
<td>
{ctx.user?.username === news.created_by ||
canUser(ctx.user, CzchanPerm.ALL_NEWS) ? (
<a href={`/mod/news_content/${news.id}`}>[Upravit]</a>
<a href={`/mod/edit/news_content/${news.id}`}>[Upravit]</a>
) : (
<s class="dead-link">[Upravit]</s>
)}

View File

@@ -1,73 +0,0 @@
import { readNewsPost } from "../../../db/news";
import { CzchanError } from "../../../lib/error";
import { canUser, CzchanPerm } from "../../../lib/permissions";
import { News } from "../../../schema/tables";
import { zNewsID } from "../../../schema/validation/news";
import Page from "../../components/chrome/page";
import PageBody from "../../components/chrome/page_body";
import PageHead from "../../components/chrome/page_head";
import { Form, FormButton, FormField } from "../../components/forms/form";
import { getCtx, TemplateCtx } from "../../ctx";
import { PropsWithChildren } from "@kitajs/html";
import { Request, Response } from "express";
export default async (req: Request, res: Response) => {
const ctx = await getCtx(req);
if (!canUser(req.user, CzchanPerm.MANAGE_NEWS))
throw new CzchanError("K tomuto nemáš oprávnění.", 403);
const news = await readNewsPost(zNewsID.parse(req.params.news));
if (
!(
req.user?.username === news.created_by ||
canUser(req.user, CzchanPerm.ALL_NEWS)
)
) {
throw new CzchanError(
`K upravení novinek ${news.id} nemáš oprávnění.`,
403,
);
}
const html = Template(ctx, news);
res.send(html);
};
const Template = (ctx: TemplateCtx, news: News) => (
<Page>
<PageHead ctx={ctx} title="Novinky" description="" />
<PageBody ctx={ctx}>
<h1 class="title center">
Novinky
<br />(<span safe>{news.subject}</span>)
</h1>
<UpdateForm ctx={ctx} news={news} />
<hr />
</PageBody>
</Page>
);
const UpdateForm = ({
news,
}: PropsWithChildren<{ ctx: TemplateCtx; news: News }>) => (
<>
<hr />
<form method="post" action="/mod/actions/update_news_content">
<input name="news" type="hidden" value={news.id.toString()} />
<Form center>
<FormField label="Předmět">
<input name="subject" type="text" value={news.subject} />
</FormField>
<FormField label="Obsah">
<textarea name="content" rows="10" cols="35" safe>
{news.content_unformatted}
</textarea>
</FormField>
<FormButton submit="Upravit" action="update_news_content" />
</Form>
</form>
</>
);

View File

@@ -122,7 +122,7 @@ const UserRow = ({
</td>
<td>
[{user.permissions.length}]{" "}
<a href={`/mod/user_permissions/${user.username}`}>[Zobrazit]</a>
<a href={`/mod/edit/user_permissions/${user.username}`}>[Zobrazit]</a>
</td>
<td>
<Datetime ctx={ctx} date={user.created} />

View File

@@ -17,21 +17,22 @@ import modActionUpdateUserPermissions from "../routes/mod/actions/update_user_pe
import modActionUserActions from "../routes/mod/actions/user_actions";
import modBanners from "../routes/mod/banners";
import modBans from "../routes/mod/bans";
import modBoardConfig from "../routes/mod/board_config";
import modBoards from "../routes/mod/boards";
import modEditBoardConfig from "../routes/mod/edit/board_config";
import modEditNewsContent from "../routes/mod/edit/news_content";
import modEditUserPermissions from "../routes/mod/edit/user_permissions";
import modFilters from "../routes/mod/filters";
import modIndex from "../routes/mod/index";
import modLogs from "../routes/mod/logs";
import modNews from "../routes/mod/news";
import modNewsContent from "../routes/mod/news_content";
import modReports from "../routes/mod/reports";
import modRestrictions from "../routes/mod/restrictions";
import modUserPermissions from "../routes/mod/user_permissions";
import modUsers from "../routes/mod/users";
import { e } from "../templates/error_page";
import { Router } from "express";
const registerModRoutes = (router: Router) => {
// Dashboards
router.get("/mod/", e(modIndex));
router.get("/mod/users", e(modUsers));
router.get("/mod/boards", e(modBoards));
@@ -43,10 +44,26 @@ const registerModRoutes = (router: Router) => {
router.get("/mod/filters", e(modFilters));
router.get("/mod/logs", e(modLogs));
router.get("/mod/user_permissions/:user", e(modUserPermissions));
router.get("/mod/board_config/:board", e(modBoardConfig));
router.get("/mod/news_content/:news", e(modNewsContent));
// Edit pages
router.get("/mod/edit/user_permissions/:user", e(modEditUserPermissions));
router.get("/mod/edit/board_config/:board", e(modEditBoardConfig));
router.get("/mod/edit/news_content/:news", e(modEditNewsContent));
// Edit page actions
router.post(
"/mod/actions/update_user_permissions",
e(modActionUpdateUserPermissions),
);
router.post(
"/mod/actions/update_board_config",
e(modActionUpdateBoardConfig),
);
router.post(
"/mod/actions/update_news_content",
e(modActionUpdateNewsContent),
);
// Action pages
router.post("/mod/actions/create_user", e(modActionCreateUser));
router.post("/mod/actions/user_actions", e(modActionUserActions));
router.post(
@@ -67,18 +84,6 @@ const registerModRoutes = (router: Router) => {
);
router.post("/mod/actions/create_filter", e(modActionCreateFilter));
router.post("/mod/actions/filter_actions", e(modActionFilterActions));
router.post(
"/mod/actions/update_user_permissions",
e(modActionUpdateUserPermissions),
);
router.post(
"/mod/actions/update_board_config",
e(modActionUpdateBoardConfig),
);
router.post(
"/mod/actions/update_news_content",
e(modActionUpdateNewsContent),
);
};
export { registerModRoutes };

View File

@@ -158,7 +158,7 @@ footer {
list-style-type: none;
}
.action-details {
.form-group {
display: inline-block;
padding: 8px;
}
@@ -216,7 +216,8 @@ footer {
display: none;
}
.catalog-tile .thumb {
.catalog-tile .thumb,
.mod-thumb .thumb {
margin: 8px auto;
max-width: 75%;
max-height: 50%;

View File

@@ -168,7 +168,7 @@ select,
.inner-footer,
.reply,
.catalog-tile,
.action-details {
.form-group {
border-right: 1px solid $box-border;
border-bottom: 1px solid $box-border;
background-color: $box-bg;
@@ -183,7 +183,7 @@ select,
/* Actions */
.action-details .form-table {
.form-group .form-table {
border: none;
background: none;
}