kopia lustrzana https://github.com/cloudflare/wildebeest
add both FE and BE for server settings (including rules)
rodzic
b131e83aa0
commit
246edfc789
|
@ -150,7 +150,8 @@ export async function createPerson(
|
|||
db: Database,
|
||||
userKEK: string,
|
||||
email: string,
|
||||
properties: PersonProperties = {}
|
||||
properties: PersonProperties = {},
|
||||
admin: boolean = false
|
||||
): Promise<Person> {
|
||||
const userKeyPair = await generateUserKey(userKEK)
|
||||
|
||||
|
@ -198,12 +199,12 @@ export async function createPerson(
|
|||
const row = await db
|
||||
.prepare(
|
||||
`
|
||||
INSERT INTO actors(id, type, email, pubkey, privkey, privkey_salt, properties)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO actors(id, type, email, pubkey, privkey, privkey_salt, properties, is_admin)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING *
|
||||
`
|
||||
)
|
||||
.bind(id, PERSON, email, userKeyPair.pubKey, privkey, salt, JSON.stringify(properties))
|
||||
.bind(id, PERSON, email, userKeyPair.pubKey, privkey, salt, JSON.stringify(properties), admin ? 1 : null)
|
||||
.first()
|
||||
|
||||
return personFromRow(row)
|
||||
|
|
|
@ -74,12 +74,21 @@ async function getOrCreatePerson(
|
|||
db: Database,
|
||||
{ username, avatar, display_name }: Account
|
||||
): Promise<Person> {
|
||||
const person = await getPersonByEmail(db, username)
|
||||
const isAdmin = username === 'george'
|
||||
const email = `${username}@test.email`
|
||||
const person = await getPersonByEmail(db, email)
|
||||
if (person) return person
|
||||
const newPerson = await createPerson(domain, db, 'test-kek', username, {
|
||||
icon: { url: avatar },
|
||||
name: display_name,
|
||||
})
|
||||
const newPerson = await createPerson(
|
||||
domain,
|
||||
db,
|
||||
'test-kek',
|
||||
email,
|
||||
{
|
||||
icon: { url: avatar },
|
||||
name: display_name,
|
||||
},
|
||||
isAdmin
|
||||
)
|
||||
if (!newPerson) {
|
||||
throw new Error('Could not create Actor ' + username)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { component$, useSignal } from '@builder.io/qwik'
|
||||
|
||||
type Props = {
|
||||
label: string
|
||||
name?: string
|
||||
description?: string
|
||||
class?: string
|
||||
invalid?: boolean
|
||||
value?: string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export const TextArea = component$<Props>(
|
||||
({ class: className, label, name, description, invalid, value, required }) => {
|
||||
const inputId = useSignal(`${label.replace(/\s+/g, '_')}___${crypto.randomUUID()}`).value
|
||||
return (
|
||||
<div class={`mb-6 ${className || ''}`}>
|
||||
<label class="font-semibold block mb-1" for={inputId}>
|
||||
{label}
|
||||
{!!required && <span class="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
{!!description && <div class="text-sm inline-block mb-2 text-wildebeest-400">{description}</div>}
|
||||
<textarea
|
||||
class={`bg-black text-white p-3 rounded outline-none border hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500 w-full ${
|
||||
invalid ? 'border-red-500' : 'border-black'
|
||||
}`}
|
||||
id={inputId}
|
||||
name={name}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
|
@ -0,0 +1,36 @@
|
|||
import { component$, useSignal } from '@builder.io/qwik'
|
||||
|
||||
type Props = {
|
||||
label: string
|
||||
name?: string
|
||||
description?: string
|
||||
class?: string
|
||||
invalid?: boolean
|
||||
value?: string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export const TextInput = component$<Props>(
|
||||
({ class: className, label, name, description, invalid, value, required }) => {
|
||||
const inputId = useSignal(`${label.replace(/\s+/g, '_')}___${crypto.randomUUID()}`).value
|
||||
const includeDefaultMb = !/(^|\s)m[y,b]?-\S+(\s|$)/.test(className || '')
|
||||
return (
|
||||
<div class={`${className || ''} ${includeDefaultMb ? 'mb-6' : ''}`}>
|
||||
<label class="font-semibold block mb-2" for={inputId}>
|
||||
{label}
|
||||
{!!required && <span class="ml-1 text-red-500">*</span>}
|
||||
</label>
|
||||
<input
|
||||
class={`bg-black text-white p-3 mb-1 rounded outline-none border hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500 w-full ${
|
||||
invalid ? 'border-red-500' : 'border-black'
|
||||
}`}
|
||||
type="text"
|
||||
id={inputId}
|
||||
name={name}
|
||||
value={value}
|
||||
/>
|
||||
{!!description && <div class="text-sm text-wildebeest-400">{description}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
|
@ -1,5 +1,4 @@
|
|||
import { component$ } from '@builder.io/qwik'
|
||||
import * as access from 'wildebeest/backend/src/access'
|
||||
import type { Client } from 'wildebeest/backend/src/mastodon/client'
|
||||
import { getClientById } from 'wildebeest/backend/src/mastodon/client'
|
||||
import { DocumentHead, loader$ } from '@builder.io/qwik-city'
|
||||
|
@ -9,8 +8,9 @@ import { getPersonByEmail } from 'wildebeest/backend/src/activitypub/actors'
|
|||
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
|
||||
import { buildRedirect } from 'wildebeest/functions/oauth/authorize'
|
||||
import { getDatabase } from 'wildebeest/backend/src/database'
|
||||
import { getJwtEmail } from '~/utils/getJwtEmail'
|
||||
|
||||
export const clientLoader = loader$<Promise<Client>, { DATABASE: D1Database }>(async ({ platform, query, html }) => {
|
||||
export const clientLoader = loader$<Promise<Client>>(async ({ platform, query, html }) => {
|
||||
const client_id = query.get('client_id') || ''
|
||||
let client: Client | null = null
|
||||
try {
|
||||
|
@ -26,50 +26,41 @@ export const clientLoader = loader$<Promise<Client>, { DATABASE: D1Database }>(a
|
|||
return client
|
||||
})
|
||||
|
||||
export const userLoader = loader$<
|
||||
Promise<{ email: string; avatar: URL; name: string; url: URL }>,
|
||||
{ DATABASE: D1Database; domain: string }
|
||||
>(async ({ cookie, platform, html, request, redirect, text }) => {
|
||||
const jwt = cookie.get('CF_Authorization')
|
||||
if (jwt === null) {
|
||||
throw html(500, getErrorHtml('Missing Authorization'))
|
||||
}
|
||||
let payload: access.JWTPayload
|
||||
try {
|
||||
// TODO: eventually, verify the JWT with Access, however this
|
||||
// is not critical.
|
||||
payload = access.getPayload(jwt.value)
|
||||
} catch (e: unknown) {
|
||||
const error = e as { stack: string; cause: string }
|
||||
console.warn(error.stack, error.cause)
|
||||
throw html(500, getErrorHtml('Failed to validate Access JWT'))
|
||||
}
|
||||
|
||||
if (!payload.email) {
|
||||
throw html(500, getErrorHtml("The Access JWT doesn't contain an email"))
|
||||
}
|
||||
|
||||
const person = await getPersonByEmail(await getDatabase(platform), payload.email)
|
||||
if (person === null) {
|
||||
const isFirstLogin = true
|
||||
const res = await buildRedirect(await getDatabase(platform), request as Request, isFirstLogin, jwt.value)
|
||||
if (res.status === 302) {
|
||||
throw redirect(302, res.headers.get('location') || '')
|
||||
} else {
|
||||
throw text(res.status, await res.text())
|
||||
export const userLoader = loader$<Promise<{ email: string; avatar: URL; name: string; url: URL }>>(
|
||||
async ({ cookie, platform, html, request, redirect, text }) => {
|
||||
const jwt = cookie.get('CF_Authorization')
|
||||
let email = ''
|
||||
try {
|
||||
email = getJwtEmail(jwt?.value ?? '')
|
||||
} catch (e) {
|
||||
throw html(500, getErrorHtml((e as Error)?.message))
|
||||
}
|
||||
|
||||
const person = await getPersonByEmail(await getDatabase(platform), email)
|
||||
if (person === null) {
|
||||
const isFirstLogin = true
|
||||
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
-- jwt is defined otherwise getJwtEmail would have thrown
|
||||
*/
|
||||
const res = await buildRedirect(await getDatabase(platform), request as Request, isFirstLogin, jwt!.value)
|
||||
if (res.status === 302) {
|
||||
throw redirect(302, res.headers.get('location') || '')
|
||||
} else {
|
||||
throw text(res.status, await res.text())
|
||||
}
|
||||
}
|
||||
|
||||
const name = person.name
|
||||
const avatar = person.icon?.url
|
||||
const url = person.url
|
||||
|
||||
if (!name || !avatar) {
|
||||
throw html(500, getErrorHtml("The person associated with the Access JWT doesn't include a name or avatar"))
|
||||
}
|
||||
|
||||
return { email, avatar, name, url }
|
||||
}
|
||||
|
||||
const name = person.name
|
||||
const avatar = person.icon?.url
|
||||
const url = person.url
|
||||
|
||||
if (!name || !avatar) {
|
||||
throw html(500, getErrorHtml("The person associated with the Access JWT doesn't include a name or avatar"))
|
||||
}
|
||||
|
||||
return { email: payload.email, avatar, name, url }
|
||||
})
|
||||
)
|
||||
|
||||
export default component$(() => {
|
||||
const client = clientLoader().value
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import { component$ } from '@builder.io/qwik'
|
||||
import { action$, Form, Link, z, zod$ } from '@builder.io/qwik-city'
|
||||
import { getDatabase } from 'wildebeest/backend/src/database'
|
||||
import { handleRequestPost } from 'wildebeest/functions/api/wb/settings/server/server'
|
||||
import { TextArea } from '~/components/Settings/TextArea'
|
||||
import { serverSettingsLoader } from '../layout'
|
||||
|
||||
const zodSchema = zod$({
|
||||
'extended description': z.string().min(1),
|
||||
'privacy policy': z.string().optional(),
|
||||
})
|
||||
|
||||
export type ServerAboutData = Awaited<typeof zodSchema>['_type']
|
||||
|
||||
export const action = action$(async (data, { request, platform }) => {
|
||||
let success = false
|
||||
try {
|
||||
const response = await handleRequestPost(
|
||||
getDatabase(platform),
|
||||
new Request(request, { body: JSON.stringify(data) })
|
||||
)
|
||||
success = response.ok
|
||||
} catch (e: unknown) {
|
||||
success = false
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
}
|
||||
}, zodSchema)
|
||||
|
||||
export default component$(() => {
|
||||
const existingSettings = serverSettingsLoader()
|
||||
const saveAction = action()
|
||||
|
||||
return (
|
||||
<Form action={saveAction} spaReset>
|
||||
<p class="mt-12 mb-9">Provide in-depth information about how the server is operated, moderated, funded.</p>
|
||||
|
||||
<div class="mb-12">
|
||||
<TextArea
|
||||
class="mb-1"
|
||||
label="Extended description"
|
||||
name="extended description"
|
||||
description="Any additional information that may be useful to visitors and your users. Can be structured with Markdown syntax."
|
||||
value={existingSettings.value['extended description']}
|
||||
/>
|
||||
<div class="text-sm text-wildebeest-400">
|
||||
There is a dedicated area for rules that your users are expected to adhere to{' '}
|
||||
<Link href="/settings/server-settings/rules">Manage server rules</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
label="Privacy Policy"
|
||||
description="Use your own privacy policy or leave blank to use the default. Can be structured with Markdown syntax."
|
||||
name="privacy policy"
|
||||
value={existingSettings.value['privacy policy']}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full my-10 bg-wildebeest-vibrant-600 hover:bg-wildebeest-vibrant-500 p-2 text-white text-uppercase border-wildebeest-vibrant-600 text-lg text-semi outline-none border rounded hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</Form>
|
||||
)
|
||||
})
|
|
@ -0,0 +1,71 @@
|
|||
import { component$ } from '@builder.io/qwik'
|
||||
import { action$, Form, zod$, z } from '@builder.io/qwik-city'
|
||||
import { getDatabase } from 'wildebeest/backend/src/database'
|
||||
import { handleRequestPost } from 'wildebeest/functions/api/wb/settings/server/server'
|
||||
import { TextArea } from '~/components/Settings/TextArea'
|
||||
import { TextInput } from '~/components/Settings/TextInput'
|
||||
import { serverSettingsLoader } from '../layout'
|
||||
|
||||
const zodSchema = zod$({
|
||||
'server name': z.string().min(1),
|
||||
'server description': z.string().min(1),
|
||||
})
|
||||
|
||||
export type ServerBrandingData = Awaited<typeof zodSchema>['_type']
|
||||
|
||||
export const action = action$(async (data, { request, platform }) => {
|
||||
let success = false
|
||||
try {
|
||||
const response = await handleRequestPost(
|
||||
await getDatabase(platform),
|
||||
new Request(request, { body: JSON.stringify(data) })
|
||||
)
|
||||
success = response.ok
|
||||
} catch (e: unknown) {
|
||||
success = false
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
}
|
||||
}, zodSchema)
|
||||
|
||||
export default component$(() => {
|
||||
const existingSettings = serverSettingsLoader()
|
||||
const saveAction = action()
|
||||
|
||||
return (
|
||||
<Form action={saveAction} spaReset>
|
||||
<p class="mt-12 mb-9">
|
||||
Your server's branding differentiates it from other servers in the network. This information may be displayed
|
||||
across a variety of environments, such as Mastodon's web interface, native applications, in link previews on
|
||||
other websites and within messaging apps, and so on. For this reason, it is best to keep this information clear,
|
||||
short and concise.
|
||||
</p>
|
||||
|
||||
<TextInput
|
||||
class="mb-9"
|
||||
label="Server name"
|
||||
name="server name"
|
||||
value={existingSettings.value['server name']}
|
||||
invalid={!!saveAction.value?.fieldErrors?.['server name']}
|
||||
description="How people may refer to your server besides its domain name."
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Server description"
|
||||
name="server description"
|
||||
value={existingSettings.value['server description']}
|
||||
invalid={!!saveAction.value?.fieldErrors?.['server description']}
|
||||
description="A short description to help uniquely identify your server. Who is running it, who is it for?"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full my-10 bg-wildebeest-vibrant-600 hover:bg-wildebeest-vibrant-500 p-2 text-white text-uppercase border-wildebeest-vibrant-600 text-lg text-semi outline-none border rounded hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</Form>
|
||||
)
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
import { component$ } from '@builder.io/qwik'
|
||||
import { loader$ } from '@builder.io/qwik-city'
|
||||
|
||||
export const loader = loader$(({ redirect }) => {
|
||||
redirect(303, 'server-settings/branding')
|
||||
})
|
||||
|
||||
export default component$(() => <></>)
|
|
@ -0,0 +1,74 @@
|
|||
import { component$, Slot } from '@builder.io/qwik'
|
||||
import { Link, loader$, useLocation } from '@builder.io/qwik-city'
|
||||
import { getDatabase } from 'wildebeest/backend/src/database'
|
||||
import { handleRequestGet } from 'wildebeest/functions/api/wb/settings/server/server'
|
||||
import { ServerAboutData } from './about'
|
||||
import { ServerBrandingData } from './branding'
|
||||
|
||||
export type ServerSettingsData = ServerBrandingData & ServerAboutData
|
||||
|
||||
export const serverSettingsLoader = loader$<Promise<Partial<ServerSettingsData>>>(async ({ platform }) => {
|
||||
const database = await getDatabase(platform)
|
||||
|
||||
const settingsResp = await handleRequestGet(database)
|
||||
let settingsData: Partial<ServerSettingsData> = {}
|
||||
try {
|
||||
settingsData = await settingsResp.json()
|
||||
} catch {
|
||||
settingsData = {}
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(settingsData))
|
||||
})
|
||||
|
||||
export default component$(() => {
|
||||
const sectionLinks = [
|
||||
{
|
||||
text: 'Branding',
|
||||
faIcon: 'fa-pen',
|
||||
path: 'branding',
|
||||
},
|
||||
{
|
||||
text: 'About',
|
||||
faIcon: 'fa-file-lines',
|
||||
path: 'about',
|
||||
},
|
||||
{
|
||||
text: 'Rules',
|
||||
faIcon: 'fa-pen-ruler',
|
||||
path: 'rules',
|
||||
},
|
||||
] as const
|
||||
|
||||
const currentPath = useLocation().url.pathname.replace(/\/$/, '')
|
||||
|
||||
return (
|
||||
<div class="max-w-4xl py-14 px-8">
|
||||
<h2 class="text-2xl font-bold mb-6">Server Settings</h2>
|
||||
|
||||
<ul class="flex gap-4 mb-6">
|
||||
{sectionLinks.map(({ text, faIcon, path }) => {
|
||||
const isActive = currentPath.endsWith(path)
|
||||
return (
|
||||
<Link
|
||||
key={text}
|
||||
class={`
|
||||
py-2 px-3 rounded text-sm no-underline flex gap-2
|
||||
${
|
||||
isActive
|
||||
? 'bg-wildebeest-vibrant-500 hover:bg-wildebeest-vibrant-400 focus-visible:bg-wildebeest-vibrant-400'
|
||||
: 'hover:bg-wildebeest-700 focus-visible:bg-wildebeest-700'
|
||||
}`}
|
||||
href={`/settings/server-settings/${path}`}
|
||||
>
|
||||
<i class={`fa-solid ${faIcon} leading-normal w-3 h-3`}></i>
|
||||
{text}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<Slot />
|
||||
</div>
|
||||
)
|
||||
})
|
|
@ -0,0 +1,96 @@
|
|||
import { component$ } from '@builder.io/qwik'
|
||||
import { action$, Form, loader$, useNavigate, z, zod$ } from '@builder.io/qwik-city'
|
||||
import { getDatabase } from 'wildebeest/backend/src/database'
|
||||
import { handleRequestGet } from 'wildebeest/functions/api/v1/instance/rules'
|
||||
import { upsertRule } from 'wildebeest/functions/api/wb/settings/server/rules'
|
||||
import { TextArea } from '~/components/Settings/TextArea'
|
||||
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
|
||||
|
||||
export type ServerSettingsData = { rules: string[] }
|
||||
|
||||
export const editAction = action$(
|
||||
async (data, { platform }) => {
|
||||
let success = false
|
||||
try {
|
||||
const result = await upsertRule(await getDatabase(platform), {
|
||||
id: +data.id,
|
||||
text: data.text,
|
||||
})
|
||||
success = result.success
|
||||
} catch (e: unknown) {
|
||||
success = false
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
}
|
||||
},
|
||||
zod$({
|
||||
id: z.string().min(1),
|
||||
text: z.string().min(1),
|
||||
})
|
||||
)
|
||||
|
||||
export const ruleLoader = loader$<Promise<{ id: number; text: string }>>(async ({ params, platform, html }) => {
|
||||
const database = await getDatabase(platform)
|
||||
|
||||
const settingsResp = await handleRequestGet(database)
|
||||
let rules: { id: number; text: string }[] = []
|
||||
try {
|
||||
rules = await settingsResp.json()
|
||||
} catch {
|
||||
rules = []
|
||||
}
|
||||
|
||||
const rule: { id: number; text: string } | undefined = rules.find((r) => r.id === +params['id'])
|
||||
|
||||
if (!rule) {
|
||||
throw html(404, getErrorHtml('The selected rule could not be found'))
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(rule))
|
||||
})
|
||||
|
||||
export default component$(() => {
|
||||
const rule = ruleLoader()
|
||||
const editActionObj = editAction()
|
||||
|
||||
const nav = useNavigate()
|
||||
|
||||
if (editActionObj.value?.success) {
|
||||
nav('/settings/server-settings/rules')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form action={editActionObj} spaReset>
|
||||
<p class="mt-12 mb-9">
|
||||
While most claim to have read and agree to the terms of service, usually people do not read through until
|
||||
after a problem arises. Make it easier to see your server's rules at a glance by providing them in a flat
|
||||
bullet point list. Try to keep individual rules short and simple, but try not to split them up into many
|
||||
separate items either.
|
||||
</p>
|
||||
|
||||
<input hidden name="id" value={rule.value.id} />
|
||||
|
||||
<div class="mb-12">
|
||||
<TextArea
|
||||
class="mb-1"
|
||||
label="Rule"
|
||||
required
|
||||
name="text"
|
||||
value={rule.value.text}
|
||||
description="Describe a rule or requirement for users on this server. Try to keep it short and simple."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full my-5 bg-wildebeest-vibrant-600 hover:bg-wildebeest-vibrant-500 p-2 text-white text-uppercase border-wildebeest-vibrant-600 text-lg text-semi outline-none border rounded hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</Form>
|
||||
</>
|
||||
)
|
||||
})
|
|
@ -0,0 +1,122 @@
|
|||
import { component$ } from '@builder.io/qwik'
|
||||
import { action$, Form, Link, loader$, z, zod$ } from '@builder.io/qwik-city'
|
||||
import { getDatabase } from 'wildebeest/backend/src/database'
|
||||
import { handleRequestGet } from 'wildebeest/functions/api/v1/instance/rules'
|
||||
import { deleteRule, upsertRule } from 'wildebeest/functions/api/wb/settings/server/rules'
|
||||
import { TextArea } from '~/components/Settings/TextArea'
|
||||
|
||||
export type ServerSettingsData = { rules: string[] }
|
||||
|
||||
export const addAction = action$(
|
||||
async (data, { platform }) => {
|
||||
let success = false
|
||||
try {
|
||||
const result = await upsertRule(await getDatabase(platform), data.text)
|
||||
success = result.success
|
||||
} catch (e: unknown) {
|
||||
success = false
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
}
|
||||
},
|
||||
zod$({
|
||||
text: z.string().min(1),
|
||||
})
|
||||
)
|
||||
|
||||
export const deleteAction = action$(
|
||||
async (data, { platform }) => {
|
||||
let success = false
|
||||
|
||||
try {
|
||||
const result = await deleteRule(await getDatabase(platform), data.id)
|
||||
success = result.success
|
||||
} catch (e: unknown) {
|
||||
success = false
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
}
|
||||
},
|
||||
zod$({
|
||||
id: z.number(),
|
||||
})
|
||||
)
|
||||
|
||||
export const rulesLoader = loader$<Promise<{ id: number; text: string }[]>>(async ({ platform }) => {
|
||||
const database = await getDatabase(platform)
|
||||
|
||||
const settingsResp = await handleRequestGet(database)
|
||||
let rules: { id: number; text: string }[] = []
|
||||
try {
|
||||
rules = await settingsResp.json()
|
||||
} catch {
|
||||
rules = []
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(rules))
|
||||
})
|
||||
|
||||
export default component$(() => {
|
||||
const rules = rulesLoader()
|
||||
const addActionObj = addAction()
|
||||
const deleteActionObj = deleteAction()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form action={addActionObj} spaReset>
|
||||
<p class="mt-12 mb-9">
|
||||
While most claim to have read and agree to the terms of service, usually people do not read through until
|
||||
after a problem arises. Make it easier to see your server's rules at a glance by providing them in a flat
|
||||
bullet point list. Try to keep individual rules short and simple, but try not to split them up into many
|
||||
separate items either.
|
||||
</p>
|
||||
|
||||
<div class="mb-12">
|
||||
<TextArea
|
||||
class="mb-1"
|
||||
label="Rule"
|
||||
name="text"
|
||||
required
|
||||
description="Describe a rule or requirement for users on this server. Try to keep it short and simple."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full my-5 bg-wildebeest-vibrant-600 hover:bg-wildebeest-vibrant-500 p-2 text-white text-uppercase border-wildebeest-vibrant-600 text-lg text-semi outline-none border rounded hover:border-wildebeest-vibrant-500 focus:border-wildebeest-vibrant-500"
|
||||
>
|
||||
Add Rule
|
||||
</button>
|
||||
</Form>
|
||||
<div>
|
||||
{rules.value.map(({ id, text }, idx) => {
|
||||
const ruleId = idx + 1
|
||||
const ruleBtnText = `${ruleId}. ${text.slice(0, 27)}${text.length > 27 ? '...' : ''}`
|
||||
return (
|
||||
<div key={id} class="p-4 my-4 bg-wildebeest-600 rounded">
|
||||
<Link href={`./edit/${ruleId}`} class="max-w-max inline-block mb-4 no-underline text-lg font-semibold">
|
||||
{ruleBtnText}
|
||||
</Link>
|
||||
<div class="flex justify-between text-wildebeest-400">
|
||||
<span>{text}</span>
|
||||
<button
|
||||
onClick$={() => {
|
||||
if (confirm('Are you sure?')) {
|
||||
deleteActionObj.run({ id })
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i class="fa-solid fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
|
@ -15,8 +15,7 @@ import { getDatabase } from 'wildebeest/backend/src/database'
|
|||
import { Person } from 'wildebeest/backend/src/activitypub/actors'
|
||||
|
||||
export const accountPageLoader = loader$<
|
||||
Promise<{ account: MastodonAccount; accountHandle: string; isValidStatus: boolean }>,
|
||||
{ DATABASE: D1Database }
|
||||
Promise<{ account: MastodonAccount; accountHandle: string; isValidStatus: boolean }>
|
||||
>(async ({ platform, params, request, html }) => {
|
||||
let isValidStatus = false
|
||||
let account: MastodonAccount | null = null
|
||||
|
|
|
@ -1,67 +1,76 @@
|
|||
import { component$ } from '@builder.io/qwik'
|
||||
import { DocumentHead, loader$ } from '@builder.io/qwik-city'
|
||||
import { getDatabase } from 'wildebeest/backend/src/database'
|
||||
import { getDomain } from 'wildebeest/backend/src/utils/getDomain'
|
||||
import { handleRequestGet as settingsHandleRequestGet } from 'wildebeest/functions/api/wb/settings/server/server'
|
||||
import { handleRequestGet as rulesHandleRequestGet } from 'wildebeest/functions/api/v1/instance/rules'
|
||||
import { Accordion } from '~/components/Accordion/Accordion'
|
||||
import { AccountCard } from '~/components/AccountCard/AccountCard'
|
||||
// import { AccountCard } from '~/components/AccountCard/AccountCard'
|
||||
import { HtmlContent } from '~/components/HtmlContent/HtmlContent'
|
||||
import { george } from '~/dummyData/accounts'
|
||||
import { ServerSettingsData } from '~/routes/(admin)/settings/server-settings/layout'
|
||||
import { Account } from '~/types'
|
||||
import { getDocumentHead } from '~/utils/getDocumentHead'
|
||||
import { getNotFoundHtml } from '~/utils/getNotFoundHtml/getNotFoundHtml'
|
||||
import { instanceLoader } from '../layout'
|
||||
import { getAdmins } from 'wildebeest/functions/api/wb/settings/server/admins'
|
||||
import { emailSymbol } from 'wildebeest/backend/src/activitypub/actors'
|
||||
import { loadLocalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
|
||||
import { AccountCard } from '~/components/AccountCard/AccountCard'
|
||||
import { getNotFoundHtml } from '~/utils/getNotFoundHtml/getNotFoundHtml'
|
||||
|
||||
type AboutInfo = {
|
||||
image: string
|
||||
domain: string
|
||||
contact: {
|
||||
account: Account
|
||||
email: string
|
||||
}
|
||||
rules: { id: string; text: string }[]
|
||||
admin: { account: Account | null; email: string }
|
||||
rules: { id: number; text: string }[]
|
||||
extended_description: {
|
||||
updated_at: string
|
||||
content: string
|
||||
}
|
||||
}
|
||||
|
||||
export const aboutInfoLoader = loader$<Promise<AboutInfo>>(async ({ resolveValue, request, html }) => {
|
||||
// TODO: properly implement loader and remove the following 404 throw
|
||||
export const aboutInfoLoader = loader$<Promise<AboutInfo>>(async ({ resolveValue, request, platform, html }) => {
|
||||
throw html(404, getNotFoundHtml())
|
||||
|
||||
// TODO: fetching the instance for the thumbnail, but that should be part of the settings
|
||||
const instance = await resolveValue(instanceLoader)
|
||||
|
||||
const database = await getDatabase(platform)
|
||||
|
||||
const brandingDataResp = await settingsHandleRequestGet(database)
|
||||
let brandingData: ServerSettingsData | null
|
||||
try {
|
||||
brandingData = await brandingDataResp.json()
|
||||
} catch {
|
||||
brandingData = null
|
||||
}
|
||||
|
||||
const rulesResp = await rulesHandleRequestGet(database)
|
||||
let rules: { id: number; text: string }[] = []
|
||||
try {
|
||||
rules = await rulesResp.json()
|
||||
} catch {
|
||||
rules = []
|
||||
}
|
||||
|
||||
const admins = await getAdmins(database)
|
||||
let adminAccount: Account | null = null
|
||||
|
||||
const adminPerson = admins.find((admin) => admin[emailSymbol] === platform.ADMIN_EMAIL)
|
||||
|
||||
if (adminPerson) {
|
||||
try {
|
||||
adminAccount = (await loadLocalMastodonAccount(database, adminPerson)) as Account
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
image: instance.thumbnail,
|
||||
domain: getDomain(request.url),
|
||||
contact: {
|
||||
account: george,
|
||||
email: 'test@test.com',
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
id: '1',
|
||||
text: 'Sexually explicit or violent media must be marked as sensitive when posting',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
text: 'No racism, sexism, homophobia, transphobia, xenophobia, or casteism',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
text: 'No incitement of violence or promotion of violent ideologies',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
text: 'No harassment, dogpiling or doxxing of other users',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
text: 'Do not share intentionally false or misleading information',
|
||||
},
|
||||
],
|
||||
admin: { account: JSON.parse(JSON.stringify(adminAccount)), email: platform.ADMIN_EMAIL },
|
||||
rules: JSON.parse(JSON.stringify(rules.sort(({ id: idA }, { id: idB }) => idA - idB))),
|
||||
extended_description: {
|
||||
updated_at: '2023-01-19T14:55:44Z',
|
||||
content:
|
||||
'<p>Please mind that the <a href="mailto:staff@mastodon.social">staff@mastodon.social</a> e-mail is for inquiries related to the operation of the mastodon.social server specifically. If your account is on another server, <strong>we will not be able to assist you</strong>. For inquiries not related specifically to the operation of this server, such as press inquiries about Mastodon gGmbH, please contact <a href="mailto:press@joinmastodon.org">press@joinmastodon.org</a>. Additional addresses:</p>\n\n<ul>\n<li>Legal, GDPR, DMCA: <a href="mailto:legal@mastodon.social">legal@mastodon.social</a></li>\n<li>Appeals: <a href="mailto:moderation@mastodon.social">moderation@mastodon.social</a></li>\n</ul>\n\n<h2>Funding</h2>\n\n<p>This server is crowdfunded by <a href="https://patreon.com/mastodon">Patreon donations</a>. For a list of sponsors, see <a href="https://joinmastodon.org/sponsors">joinmastodon.org</a>.</p>\n\n<h2>Reporting and moderation</h2>\n\n<p>When reporting accounts, please make sure to include at least a few posts that show rule-breaking behaviour, when applicable. If there is any additional context that might help make a decision, please also include it in the comment. This is especially important when the content is in a language nobody on the moderation team speaks.</p>\n\n<p>We usually handle reports within 24 hours. Please mind that you are not notified when a report you have made has led to a punitive action, and that not all punitive actions are externally visible. For first time offenses, we may opt to delete offending content, escalating to harsher measures on repeat offenses.</p>\n\n<p>We have a team of paid moderators. If you would like to become a moderator, get in touch with us through the e-mail address above.</p>\n\n<h2>Impressum</h2>\n\n<p>Mastodon gGmbH<br>\nMühlenstraße 8a<br>\n14167 Berlin<br>\nGermany</p>\n\n<p>E-Mail-Adresse: hello@joinmastodon.org</p>\n\n<p>Vertretungsberechtigt: Eugen Rochko (Geschäftsführer)</p>\n\n<p>Umsatzsteuer Identifikationsnummer (USt-ID): DE344258260</p>\n\n<p>Handelsregister<br>\nGeführt bei: Amtsgericht Charlottenburg<br>\nNummer: HRB 230086 B</p>\n',
|
||||
content: brandingData?.['extended description'] ?? '',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -86,14 +95,19 @@ export default component$(() => {
|
|||
</span>
|
||||
</p>
|
||||
|
||||
<div class="rounded bg-wildebeest-700 flex flex-col md:flex-row p-2 w-full my-5" data-testid="contact">
|
||||
<div class="flex-1 p-4">
|
||||
<span class="block uppercase text-wildebeest-500 font-semibold mb-5">Administered by:</span>
|
||||
<AccountCard account={aboutInfo.contact.account} subText="username" />
|
||||
</div>
|
||||
<div class="flex-1 p-4 pt-6 md:pt-4 md:pl-6 border-wildebeest-500 border-solid border-t md:border-t-0 md:border-l">
|
||||
<div
|
||||
class="rounded bg-wildebeest-700 flex flex-col md:flex-row p-2 w-full my-5 overflow-auto"
|
||||
data-testid="contact"
|
||||
>
|
||||
{!!aboutInfo.admin.account && (
|
||||
<div class="flex-1 p-4 border-wildebeest-500 border-solid border-b md:border-b-0 md:border-r">
|
||||
<span class="block uppercase text-wildebeest-500 font-semibold mb-5">Administered by:</span>
|
||||
<AccountCard account={aboutInfo.admin.account} subText="username" />
|
||||
</div>
|
||||
)}
|
||||
<div class="flex-1 p-4 pt-6 md:pt-4 md:pl-6 min-w-max">
|
||||
<span class="block uppercase text-wildebeest-500 font-semibold mb-5">Contact:</span>
|
||||
<span>{aboutInfo.contact.email}</span>
|
||||
<span>{aboutInfo.admin.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -11,10 +11,7 @@ import { InstanceConfigContext } from '~/utils/instanceConfig'
|
|||
import { getDocumentHead } from '~/utils/getDocumentHead'
|
||||
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
|
||||
|
||||
export const instanceLoader = loader$<
|
||||
Promise<InstanceConfig>,
|
||||
{ DATABASE: D1Database; INSTANCE_TITLE: string; INSTANCE_DESCR: string; ADMIN_EMAIL: string }
|
||||
>(async ({ platform, html }) => {
|
||||
export const instanceLoader = loader$<Promise<InstanceConfig>>(async ({ platform, html }) => {
|
||||
const env = {
|
||||
INSTANCE_DESCR: platform.INSTANCE_DESCR,
|
||||
INSTANCE_TITLE: platform.INSTANCE_TITLE,
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import * as access from 'wildebeest/backend/src/access'
|
||||
|
||||
export function getJwtEmail(jwtCookie: string) {
|
||||
let payload: access.JWTPayload
|
||||
if (!jwtCookie) {
|
||||
throw new Error('Missing Authorization')
|
||||
}
|
||||
try {
|
||||
// TODO: eventually, verify the JWT with Access, however this
|
||||
// is not critical.
|
||||
payload = access.getPayload(jwtCookie)
|
||||
} catch (e: unknown) {
|
||||
const error = e as { stack: string; cause: string }
|
||||
console.warn(error.stack, error.cause)
|
||||
throw new Error('Failed to validate Access JWT')
|
||||
}
|
||||
|
||||
if (!payload.email) {
|
||||
throw new Error("The Access JWT doesn't contain an email")
|
||||
}
|
||||
|
||||
return payload.email
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { emailSymbol } from 'wildebeest/backend/src/activitypub/actors'
|
||||
import { Database } from 'wildebeest/backend/src/database'
|
||||
import { getAdmins } from 'wildebeest/functions/api/wb/settings/server/admins'
|
||||
import { getJwtEmail } from './getJwtEmail'
|
||||
|
||||
export async function isUserAdmin(jwtCookie: string, database: Database): Promise<boolean> {
|
||||
let email: string
|
||||
try {
|
||||
email = getJwtEmail(jwtCookie)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
const admins = await getAdmins(database)
|
||||
|
||||
return admins.some((admin) => admin[emailSymbol] === email)
|
||||
}
|
|
@ -1328,7 +1328,7 @@ chalk@^2.0.0:
|
|||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chalk@^4.0.0, chalk@^4.1.0:
|
||||
chalk@^4.0.0:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
|
@ -1456,21 +1456,6 @@ concat-map@0.0.1:
|
|||
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
concurrently@^7.6.0:
|
||||
version "7.6.0"
|
||||
resolved "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz"
|
||||
integrity sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==
|
||||
dependencies:
|
||||
chalk "^4.1.0"
|
||||
date-fns "^2.29.1"
|
||||
lodash "^4.17.21"
|
||||
rxjs "^7.0.0"
|
||||
shell-quote "^1.7.3"
|
||||
spawn-command "^0.0.2-1"
|
||||
supports-color "^8.1.0"
|
||||
tree-kill "^1.2.2"
|
||||
yargs "^17.3.1"
|
||||
|
||||
convert-source-map@^1.6.0, convert-source-map@^1.7.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz"
|
||||
|
@ -1500,11 +1485,6 @@ data-uri-to-buffer@^4.0.0:
|
|||
resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz"
|
||||
integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
|
||||
|
||||
date-fns@^2.29.1:
|
||||
version "2.29.3"
|
||||
resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz"
|
||||
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
|
||||
|
||||
debug@^3.2.7:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||
|
@ -2826,11 +2806,6 @@ lodash.merge@^4.6.2:
|
|||
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
longest-streak@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz"
|
||||
|
@ -3810,13 +3785,6 @@ run-parallel@^1.1.9:
|
|||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
rxjs@^7.0.0:
|
||||
version "7.8.0"
|
||||
resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz"
|
||||
integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
sade@^1.7.3:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz"
|
||||
|
@ -3867,11 +3835,6 @@ shebang-regex@^3.0.0:
|
|||
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
shell-quote@^1.7.3:
|
||||
version "1.7.4"
|
||||
resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz"
|
||||
integrity sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==
|
||||
|
||||
signal-exit@^3.0.3, signal-exit@^3.0.7:
|
||||
version "3.0.7"
|
||||
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz"
|
||||
|
@ -3922,11 +3885,6 @@ space-separated-tokens@^2.0.0:
|
|||
resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz"
|
||||
integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
|
||||
|
||||
spawn-command@^0.0.2-1:
|
||||
version "0.0.2-1"
|
||||
resolved "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz"
|
||||
integrity sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
|
||||
|
@ -4029,7 +3987,7 @@ supports-color@^7.1.0:
|
|||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
supports-color@^8.0.0, supports-color@^8.1.0:
|
||||
supports-color@^8.0.0:
|
||||
version "8.1.1"
|
||||
resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz"
|
||||
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
|
||||
|
@ -4122,11 +4080,6 @@ touch@^3.1.0:
|
|||
dependencies:
|
||||
nopt "~1.0.10"
|
||||
|
||||
tree-kill@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz"
|
||||
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
|
||||
|
||||
trim-lines@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz"
|
||||
|
@ -4170,11 +4123,6 @@ tslib@^1.8.1, tslib@^1.9.3:
|
|||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.1.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz"
|
||||
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||
import type { ContextData } from 'wildebeest/backend/src/types/context'
|
||||
import { type Database, getDatabase } from 'wildebeest/backend/src/database'
|
||||
|
||||
export const onRequestGet: PagesFunction<Env, any, ContextData> = async ({ env }) => {
|
||||
return handleRequestGet(await getDatabase(env))
|
||||
}
|
||||
|
||||
export async function handleRequestGet(db: Database) {
|
||||
const query = `SELECT * from server_rules;`
|
||||
const result = await db.prepare(query).all<{ id: string; text: string }>()
|
||||
|
||||
if (!result.success) {
|
||||
return new Response('SQL error: ' + result.error, { status: 500 })
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(result.results ?? []), { status: 200 })
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||
import type { ContextData } from 'wildebeest/backend/src/types/context'
|
||||
import { type Database, getDatabase } from 'wildebeest/backend/src/database'
|
||||
import { Person, personFromRow } from 'wildebeest/backend/src/activitypub/actors'
|
||||
|
||||
export const onRequestGet: PagesFunction<Env, any, ContextData> = async ({ env }) => {
|
||||
return handleRequestGet(await getDatabase(env))
|
||||
}
|
||||
|
||||
export async function handleRequestGet(db: Database) {
|
||||
const admins = await getAdmins(db)
|
||||
return new Response(JSON.stringify(admins), { status: 200 })
|
||||
}
|
||||
|
||||
export async function getAdmins(db: Database): Promise<Person[]> {
|
||||
let rows: unknown[] = []
|
||||
try {
|
||||
const stmt = db.prepare('SELECT * FROM actors WHERE is_admin=TRUE')
|
||||
const result = await stmt.all<unknown>()
|
||||
rows = result.success ? (result.results as unknown[]) : []
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
return rows.map(personFromRow)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||
import type { ContextData } from 'wildebeest/backend/src/types/context'
|
||||
import * as errors from 'wildebeest/backend/src/errors'
|
||||
import { type Database, getDatabase } from 'wildebeest/backend/src/database'
|
||||
import { parse } from 'cookie'
|
||||
import { isUserAdmin } from 'wildebeest/frontend/src/utils/isUserAdmin'
|
||||
|
||||
export const onRequestPost: PagesFunction<Env, any, ContextData> = async ({ env, request }) => {
|
||||
return handleRequestPost(await getDatabase(env), request)
|
||||
}
|
||||
|
||||
export async function handleRequestPost(db: Database, request: Request) {
|
||||
const cookie = parse(request.headers.get('Cookie') || '')
|
||||
const jwt = cookie['CF_Authorization']
|
||||
const isAdmin = await isUserAdmin(jwt, db)
|
||||
|
||||
if (!isAdmin) {
|
||||
return errors.notAuthorized('Lacking authorization rights to edit server rules')
|
||||
}
|
||||
|
||||
const rule = await request.json<{ id?: number; text: string }>()
|
||||
const result = await upsertRule(db, rule)
|
||||
|
||||
if (!result.success) {
|
||||
return new Response('SQL error: ' + result.error, { status: 500 })
|
||||
}
|
||||
|
||||
return new Response('', { status: 200 })
|
||||
}
|
||||
|
||||
export async function upsertRule(db: Database, rule: { id?: number; text: string } | string) {
|
||||
const id = typeof rule === 'string' ? null : rule.id ?? null
|
||||
const text = typeof rule === 'string' ? rule : rule.text
|
||||
return await db
|
||||
.prepare(
|
||||
`INSERT INTO server_rules (id, text)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET text=excluded.text;`
|
||||
)
|
||||
.bind(id, text)
|
||||
.run()
|
||||
}
|
||||
|
||||
export async function handleRequestDelete(db: Database, request: Request) {
|
||||
const cookie = parse(request.headers.get('Cookie') || '')
|
||||
const jwt = cookie['CF_Authorization']
|
||||
const isAdmin = await isUserAdmin(jwt, db)
|
||||
|
||||
if (!isAdmin) {
|
||||
return errors.notAuthorized('Lacking authorization rights to edit server rules')
|
||||
}
|
||||
|
||||
const rule = await request.json<{ id: number }>()
|
||||
const result = await deleteRule(db, rule.id)
|
||||
|
||||
if (!result.success) {
|
||||
return new Response('SQL error: ' + result.error, { status: 500 })
|
||||
}
|
||||
|
||||
return new Response('', { status: 200 })
|
||||
}
|
||||
|
||||
export async function deleteRule(db: Database, ruleId: number) {
|
||||
return await db.prepare('DELETE FROM server_rules WHERE id=?').bind(ruleId).run()
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||
import type { ContextData } from 'wildebeest/backend/src/types/context'
|
||||
import * as errors from 'wildebeest/backend/src/errors'
|
||||
import { type Database, getDatabase } from 'wildebeest/backend/src/database'
|
||||
import { ServerBrandingData } from 'wildebeest/frontend/src/routes/(admin)/settings/server-settings/branding'
|
||||
import { parse } from 'cookie'
|
||||
import { isUserAdmin } from 'wildebeest/frontend/src/utils/isUserAdmin'
|
||||
|
||||
export const onRequestGet: PagesFunction<Env, any, ContextData> = async ({ env, request }) => {
|
||||
return handleRequestPost(await getDatabase(env), request)
|
||||
}
|
||||
|
||||
export async function handleRequestGet(db: Database) {
|
||||
const query = `SELECT * from server_settings`
|
||||
const result = await db.prepare(query).all<{ setting_name: string; setting_value: string }>()
|
||||
|
||||
const data = (result.results ?? []).reduce(
|
||||
(settings, { setting_name, setting_value }) => ({
|
||||
...settings,
|
||||
[setting_name]: setting_value,
|
||||
}),
|
||||
{} as Object
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
return new Response('SQL error: ' + result.error, { status: 500 })
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), { status: 200 })
|
||||
}
|
||||
|
||||
export const onRequestPost: PagesFunction<Env, any, ContextData> = async ({ env, request }) => {
|
||||
return handleRequestPost(await getDatabase(env), request)
|
||||
}
|
||||
|
||||
export async function handleRequestPost(db: Database, request: Request) {
|
||||
const cookie = parse(request.headers.get('Cookie') || '')
|
||||
const jwt = cookie['CF_Authorization']
|
||||
const isAdmin = await isUserAdmin(jwt, db)
|
||||
|
||||
if (!isAdmin) {
|
||||
return errors.notAuthorized('Lacking authorization rights to edit server settings')
|
||||
}
|
||||
|
||||
const data = await request.json<ServerBrandingData>()
|
||||
|
||||
const settingsEntries = Object.entries(data)
|
||||
|
||||
const query = `
|
||||
INSERT INTO server_settings (setting_name, setting_value)
|
||||
VALUES ${settingsEntries.map(() => `(?, ?)`).join(', ')}
|
||||
ON CONFLICT(setting_name) DO UPDATE SET setting_value=excluded.setting_value
|
||||
`
|
||||
const result = await db
|
||||
.prepare(query)
|
||||
.bind(...settingsEntries.flat())
|
||||
.run()
|
||||
|
||||
if (!result.success) {
|
||||
return new Response('SQL error: ' + result.error, { status: 500 })
|
||||
}
|
||||
|
||||
return new Response('', { status: 200 })
|
||||
}
|
|
@ -7,6 +7,7 @@ import { parse } from 'cookie'
|
|||
import * as errors from 'wildebeest/backend/src/errors'
|
||||
import * as access from 'wildebeest/backend/src/access'
|
||||
import { type Database, getDatabase } from 'wildebeest/backend/src/database'
|
||||
import { getJwtEmail } from 'wildebeest/frontend/src/utils/getJwtEmail'
|
||||
|
||||
export const onRequestPost: PagesFunction<Env, any, ContextData> = async ({ request, env }) => {
|
||||
return handlePostRequest(request, await getDatabase(env), env.userKEK, env.ACCESS_AUTH_DOMAIN, env.ACCESS_AUD)
|
||||
|
@ -21,23 +22,19 @@ export async function handlePostRequest(
|
|||
): Promise<Response> {
|
||||
const url = new URL(request.url)
|
||||
const cookie = parse(request.headers.get('Cookie') || '')
|
||||
|
||||
let email = ''
|
||||
const jwt = cookie['CF_Authorization']
|
||||
if (!jwt) {
|
||||
return errors.notAuthorized('missing CF_Authorization')
|
||||
try {
|
||||
email = getJwtEmail(jwt ?? '')
|
||||
} catch (e) {
|
||||
return errors.notAuthorized((e as Error)?.message)
|
||||
}
|
||||
|
||||
const payload = access.getPayload(jwt)
|
||||
if (!payload.email) {
|
||||
return errors.notAuthorized('missing email')
|
||||
}
|
||||
|
||||
const validatate = access.generateValidator({
|
||||
await access.generateValidator({
|
||||
jwt,
|
||||
domain: accessDomain,
|
||||
aud: accessAud,
|
||||
})
|
||||
await validatate(request)
|
||||
})(request)
|
||||
|
||||
const domain = url.hostname
|
||||
|
||||
|
@ -52,7 +49,7 @@ export async function handlePostRequest(
|
|||
properties.name = formData.get('name') || ''
|
||||
}
|
||||
|
||||
await createPerson(domain, db, userKEK, payload.email, properties)
|
||||
await createPerson(domain, db, userKEK, email, properties)
|
||||
|
||||
if (!url.searchParams.has('redirect_uri')) {
|
||||
return new Response('', { status: 400 })
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
-- Migration number: 0003 2023-02-24T15:03:27.478Z
|
||||
|
||||
CREATE TABLE IF NOT EXISTS server_settings (
|
||||
setting_name TEXT UNIQUE NOT NULL,
|
||||
setting_value TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS server_rules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL
|
||||
);
|
|
@ -37,8 +37,8 @@
|
|||
"pages": "NO_D1_WARNING=true wrangler pages",
|
||||
"database:migrate": "yarn d1 migrations apply DATABASE",
|
||||
"database:create-mock": "rm -f .wrangler/state/d1/DATABASE.sqlite3 && CI=true yarn database:migrate --local && node ./frontend/mock-db/run.mjs",
|
||||
"dev": "export COMMIT_HASH=$(git rev-parse HEAD) && yarn build && yarn database:migrate --local && yarn pages dev frontend/dist --d1 DATABASE --persist --compatibility-date=2022-12-20 --binding 'INSTANCE_TITLE=Test Wildebeest' 'INSTANCE_DESCR=My Wildebeest Instance' 'ACCESS_AUTH_DOMAIN=0.0.0.0.cloudflareaccess.com' 'ACCESS_AUD=DEV_AUD' --live-reload",
|
||||
"ci-dev-test-ui": "yarn build && yarn database:create-mock && yarn pages dev frontend/dist --d1 DATABASE --persist --port 8788 --binding 'INSTANCE_TITLE=Test Wildebeest' 'INSTANCE_DESCR=My Wildebeest Instance' 'ACCESS_AUTH_DOMAIN=0.0.0.0.cloudflareaccess.com' 'ACCESS_AUD=DEV_AUD' --compatibility-date=2022-12-20",
|
||||
"dev": "export COMMIT_HASH=$(git rev-parse HEAD) && yarn build && yarn database:migrate --local && yarn pages dev frontend/dist --d1 DATABASE --persist --compatibility-date=2022-12-20 --binding 'INSTANCE_TITLE=Test Wildebeest' 'INSTANCE_DESCR=My Wildebeest Instance' 'ACCESS_AUTH_DOMAIN=0.0.0.0.cloudflareaccess.com' 'ACCESS_AUD=DEV_AUD' 'ADMIN_EMAIL=george@test.email' --live-reload",
|
||||
"ci-dev-test-ui": "yarn build && yarn database:create-mock && yarn pages dev frontend/dist --d1 DATABASE --persist --port 8788 --binding 'INSTANCE_TITLE=Test Wildebeest' 'INSTANCE_DESCR=My Wildebeest Instance' 'ACCESS_AUTH_DOMAIN=0.0.0.0.cloudflareaccess.com' 'ACCESS_AUD=DEV_AUD' 'ADMIN_EMAIL=george@test.email' --compatibility-date=2022-12-20",
|
||||
"deploy:init": "yarn pages project create wildebeest && yarn d1 create wildebeest",
|
||||
"deploy": "yarn build && yarn database:migrate && yarn pages publish frontend/dist --project-name=wildebeest"
|
||||
},
|
||||
|
|
Ładowanie…
Reference in New Issue