fix!: disallow shouts via form submit
This commit is contained in:
parent
f7063a355a
commit
53321427ce
3 changed files with 27 additions and 31 deletions
|
@ -148,7 +148,6 @@ export async function createShout(params: {
|
|||
fromIp: string
|
||||
private: boolean
|
||||
text: string
|
||||
isFormSubmit: boolean
|
||||
}): Promise<boolean | string> {
|
||||
let { text } = params
|
||||
|
||||
|
@ -157,7 +156,7 @@ export async function createShout(params: {
|
|||
const validateResult = validateShout(text, !params.private)
|
||||
|
||||
const header = html`${params.private ? 'private message' : 'shout'} from <code>${params.fromIp}</code>`
|
||||
const subheader = html`<br>via: ${params.isFormSubmit ? '#form' : '#api'}<br><br>`
|
||||
const subheader = html`<br>via: #api<br><br>`
|
||||
|
||||
if (params.private || validateResult !== true) {
|
||||
const was = params.private ? '' : ` was auto-declined (${validateResult})`
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable no-alert */
|
||||
/** @jsxImportSource solid-js */
|
||||
import { type ComponentProps, Show, createSignal } from 'solid-js'
|
||||
import { type ComponentProps, Show, createSignal, onMount } from 'solid-js'
|
||||
import { QueryClient, QueryClientProvider, createQuery, keepPreviousData } from '@tanstack/solid-query'
|
||||
import { format } from 'date-fns/format'
|
||||
|
||||
|
@ -42,6 +42,8 @@ function ShoutboxInner(props: {
|
|||
initialData: initData,
|
||||
}))
|
||||
const [sending, setSending] = createSignal(false)
|
||||
const [jsEnabled, setJsEnabled] = createSignal(false)
|
||||
onMount(() => setJsEnabled(true))
|
||||
|
||||
const onPageClick = (next: boolean) => (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
|
@ -89,14 +91,15 @@ function ShoutboxInner(props: {
|
|||
)
|
||||
})
|
||||
|
||||
let form!: HTMLFormElement
|
||||
let privateCheckbox!: HTMLInputElement
|
||||
let messageInput!: HTMLTextAreaElement
|
||||
|
||||
const onSubmit = (e: Event) => {
|
||||
e.preventDefault()
|
||||
setSending(true)
|
||||
setInitData(undefined)
|
||||
|
||||
const isPrivate = (form.elements.namedItem('private') as HTMLInputElement).checked
|
||||
const isPrivate = privateCheckbox.checked
|
||||
fetch('/api/shoutbox', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -104,8 +107,8 @@ function ShoutboxInner(props: {
|
|||
},
|
||||
body: JSON.stringify({
|
||||
_csrf: props.csrf,
|
||||
message: (form.elements.namedItem('message') as HTMLInputElement).value,
|
||||
private: isPrivate ? '' : undefined,
|
||||
message: messageInput.value,
|
||||
private: isPrivate,
|
||||
}),
|
||||
})
|
||||
.then(res => res.json())
|
||||
|
@ -114,17 +117,23 @@ function ShoutboxInner(props: {
|
|||
alert(data.error + (data.message ? `: ${data.message}` : ''))
|
||||
} else if (isPrivate) {
|
||||
alert('private message sent')
|
||||
form.reset()
|
||||
messageInput.value = ''
|
||||
} else {
|
||||
alert('shout sent! it will appear after moderation')
|
||||
shouts.refetch()
|
||||
form.reset()
|
||||
messageInput.value = ''
|
||||
}
|
||||
|
||||
setSending(false)
|
||||
})
|
||||
}
|
||||
|
||||
const placeholder = () => {
|
||||
if (props.shoutError) return props.shoutError
|
||||
if (!jsEnabled()) return '⚠️ please enable javascript to use the form.\nim sorry, but there are just too many spammers out there :c'
|
||||
return 'let the void hear you'
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<section>
|
||||
|
@ -137,22 +146,24 @@ function ShoutboxInner(props: {
|
|||
pre-moderated, but they do not reflect my views.
|
||||
</TextComment>
|
||||
|
||||
<form action="/api/shoutbox" class={css.form} method="post" ref={form}>
|
||||
<div class={css.form}>
|
||||
<input type="hidden" name="_csrf" value={props.csrf} />
|
||||
<div class={css.formInput}>
|
||||
<TextArea
|
||||
disabled={sending()}
|
||||
ref={messageInput}
|
||||
disabled={sending() || !jsEnabled()}
|
||||
class={css.textarea}
|
||||
grow
|
||||
maxRows={5}
|
||||
name="message"
|
||||
placeholder={props.shoutError || 'let the void hear you'}
|
||||
placeholder={placeholder()}
|
||||
required
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={onSubmit}
|
||||
disabled={sending() || !jsEnabled()}
|
||||
title="submit"
|
||||
>
|
||||
<Icon glyph={GravityMegaphone} size={16} />
|
||||
|
@ -160,6 +171,7 @@ function ShoutboxInner(props: {
|
|||
</div>
|
||||
<div class={css.formControls}>
|
||||
<Checkbox
|
||||
ref={privateCheckbox}
|
||||
label="make it private"
|
||||
name="private"
|
||||
/>
|
||||
|
@ -191,7 +203,7 @@ function ShoutboxInner(props: {
|
|||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class={css.shouts}>
|
||||
{shoutsRender()}
|
||||
|
|
|
@ -11,24 +11,14 @@ import { HttpResponse } from '~/backend/utils/response'
|
|||
const schema = z.object({
|
||||
_csrf: z.string(),
|
||||
message: z.string(),
|
||||
private: z.string().optional(),
|
||||
private: z.boolean(),
|
||||
})
|
||||
|
||||
const rateLimitPerIp = new RateLimiterMemory({ points: 3, duration: 300 })
|
||||
const rateLimitGlobal = new RateLimiterMemory({ points: 100, duration: 3600 })
|
||||
|
||||
export const POST: APIRoute = async (ctx) => {
|
||||
const contentType = ctx.request.headers.get('content-type')
|
||||
const isFormSubmit = contentType === 'application/x-www-form-urlencoded'
|
||||
|
||||
let bodyRaw: unknown
|
||||
if (isFormSubmit) {
|
||||
bodyRaw = Object.fromEntries((await ctx.request.formData()).entries())
|
||||
} else {
|
||||
bodyRaw = await ctx.request.json()
|
||||
}
|
||||
|
||||
const body = await schema.safeParseAsync(bodyRaw)
|
||||
const body = await schema.safeParseAsync(await ctx.request.json())
|
||||
if (body.error) {
|
||||
return HttpResponse.json({
|
||||
error: fromError(body.error).message,
|
||||
|
@ -71,18 +61,13 @@ export const POST: APIRoute = async (ctx) => {
|
|||
|
||||
const result = await createShout({
|
||||
fromIp: ip,
|
||||
private: body.data.private !== undefined,
|
||||
private: body.data.private,
|
||||
text: body.data.message,
|
||||
isFormSubmit,
|
||||
})
|
||||
|
||||
await rateLimitPerIp.penalty(ip, 1)
|
||||
await rateLimitGlobal.penalty('GLOBAL', 1)
|
||||
|
||||
if (isFormSubmit) {
|
||||
return HttpResponse.redirect(typeof result === 'string' ? `/?shout_error=${result}` : '/')
|
||||
}
|
||||
|
||||
return HttpResponse.json(
|
||||
typeof result === 'string' ? { error: result } : { ok: true },
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue