Mod ují, mod u u új
This commit is contained in:
25
README.md
25
README.md
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
1
src/web/components/content/post_attributes.tsx
Normal file
1
src/web/components/content/post_attributes.tsx
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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);
|
||||
|
||||
122
src/web/routes/actions/action_pages/delete_and_ban.tsx
Normal file
122
src/web/routes/actions/action_pages/delete_and_ban.tsx
Normal 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>
|
||||
>>>/<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 };
|
||||
114
src/web/routes/actions/action_pages/manage_attributes.tsx
Normal file
114
src/web/routes/actions/action_pages/manage_attributes.tsx
Normal 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>
|
||||
>>>/<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 };
|
||||
122
src/web/routes/actions/action_pages/manage_files.tsx
Normal file
122
src/web/routes/actions/action_pages/manage_files.tsx
Normal 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>
|
||||
>>>/<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 };
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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) => {
|
||||
70
src/web/routes/mod/edit/news_content.tsx
Normal file
70
src/web/routes/mod/edit/news_content.tsx
Normal 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>
|
||||
);
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user