kopia lustrzana https://github.com/cloudflare/wildebeest
commit
0580e089ab
|
@ -151,7 +151,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)
|
||||
|
||||
|
@ -199,12 +200,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)
|
||||
|
|
|
@ -7,6 +7,8 @@ import { createReply as createReplyInBackend } from 'wildebeest/backend/test/sha
|
|||
import { createStatus } from 'wildebeest/backend/src/mastodon/status'
|
||||
import type { APObject } from 'wildebeest/backend/src/activitypub/objects'
|
||||
import { type Database } from 'wildebeest/backend/src/database'
|
||||
import { upsertRule } from 'wildebeest/functions/api/wb/settings/server/rules'
|
||||
import { upsertServerSettings } from 'wildebeest/functions/api/wb/settings/server/server'
|
||||
|
||||
/**
|
||||
* Run helper commands to initialize the database with actors, statuses, etc.
|
||||
|
@ -41,6 +43,17 @@ export async function init(domain: string, db: Database) {
|
|||
for (const reply of replies) {
|
||||
await createReply(domain, db, reply, loadedStatuses)
|
||||
}
|
||||
|
||||
await createServerData(db)
|
||||
}
|
||||
|
||||
async function createServerData(db: Database) {
|
||||
await upsertServerSettings(db, {
|
||||
'extended description': 'this is a test wildebeest instance!',
|
||||
})
|
||||
await upsertRule(db, "don't be mean")
|
||||
await upsertRule(db, "don't insult people")
|
||||
await upsertRule(db, 'respect the rules')
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,12 +87,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)
|
||||
}
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
"lint": "eslint src mock-db adaptors",
|
||||
"build": "vite build && vite build -c adaptors/cloudflare-pages/vite.config.ts",
|
||||
"dev": "vite --mode ssr",
|
||||
"watch": "concurrently \"vite build -w\" \"vite build -w -c adaptors/cloudflare-pages/vite.config.ts\""
|
||||
"watch": "nodemon -w ./src --ext tsx,ts --exec npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@builder.io/qwik": "0.18.1",
|
||||
"@builder.io/qwik-city": "0.2.1",
|
||||
"@builder.io/qwik": "0.19.2",
|
||||
"@builder.io/qwik-city": "0.4.0",
|
||||
"@types/eslint": "8.4.10",
|
||||
"@types/jest": "^29.2.4",
|
||||
"@types/node": "^18.11.16",
|
||||
|
@ -22,12 +22,12 @@
|
|||
"@typescript-eslint/eslint-plugin": "5.46.1",
|
||||
"@typescript-eslint/parser": "5.46.1",
|
||||
"autoprefixer": "10.4.11",
|
||||
"concurrently": "^7.6.0",
|
||||
"eslint": "8.30.0",
|
||||
"eslint-plugin-qwik": "0.16.1",
|
||||
"jest": "^29.3.1",
|
||||
"lorem-ipsum": "^2.0.8",
|
||||
"node-fetch": "3.3.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"postcss": "^8.4.16",
|
||||
"prettier": "2.8.1",
|
||||
"sass": "^1.57.0",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,7 +3,7 @@ import { loader$ } from '@builder.io/qwik-city'
|
|||
import { WildebeestEnv } from '~/types'
|
||||
import { checkAuth } from '~/utils/checkAuth'
|
||||
|
||||
export const loader = loader$<WildebeestEnv, void>(async ({ request, platform, redirect }) => {
|
||||
export const loader = loader$<void, WildebeestEnv>(async ({ request, platform, redirect }) => {
|
||||
const isAuthorized = await checkAuth(request, platform)
|
||||
|
||||
if (!isAuthorized) {
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
import { component$, Slot } from '@builder.io/qwik'
|
||||
import { WildebeestLogo } from '~/components/MastodonLogo'
|
||||
|
||||
import { loader$ } from '@builder.io/qwik-city'
|
||||
import { getNotFoundHtml } from '~/utils/getNotFoundHtml/getNotFoundHtml'
|
||||
|
||||
export const loader = loader$(({ html }) => {
|
||||
html(404, getNotFoundHtml())
|
||||
})
|
||||
|
||||
export default component$(() => {
|
||||
loader()
|
||||
|
||||
return (
|
||||
<div class="flex w-screen min-h-screen justify-center">
|
||||
<AccountSidebar />
|
||||
|
|
|
@ -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,66 +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 { 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, redirect }) => {
|
||||
// TODO: properly implement loader and remove redirect
|
||||
throw redirect(302, '/')
|
||||
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'] ?? '',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -85,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)
|
||||
}
|
|
@ -297,10 +297,10 @@
|
|||
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@builder.io/qwik-city@0.2.1":
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@builder.io/qwik-city/-/qwik-city-0.2.1.tgz#c57f481a75534ff54ddb0f38403acc66b5d02f41"
|
||||
integrity sha512-g+ZC4Neo1XYQ/8uquUp6GKwr0eagpuCyQ3LkAtFhaIARaO67+cZfR6EFLJzf9wz5AVSt8/0QSD7wJEpni1i4IA==
|
||||
"@builder.io/qwik-city@0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@builder.io/qwik-city/-/qwik-city-0.4.0.tgz#9afb97ba0e11119e44a2527f545e0d84a8fe9759"
|
||||
integrity sha512-XNpmHzSHam7ZYrd12kJdwFerMEck0iOk3Wgb9IlVIuaN/nLuN033qrNWLVq+ZzlhplUea9DGc4job8qMix7WWA==
|
||||
dependencies:
|
||||
"@mdx-js/mdx" "2.3.0"
|
||||
"@types/mdx" "2.0.3"
|
||||
|
@ -308,10 +308,10 @@
|
|||
vfile "5.3.7"
|
||||
zod "^3.20.6"
|
||||
|
||||
"@builder.io/qwik@0.18.1":
|
||||
version "0.18.1"
|
||||
resolved "https://registry.yarnpkg.com/@builder.io/qwik/-/qwik-0.18.1.tgz#341d01c5749a07230c700a5e4df859b857654cd0"
|
||||
integrity sha512-11qx5Wh6WRxgvHDJDppJORhykzkACUYuu9qRKEGdS3vTkBQ2Rr8NFDjYon2x6+8Wu9WukHk84ANywWnS91gS/w==
|
||||
"@builder.io/qwik@0.19.2":
|
||||
version "0.19.2"
|
||||
resolved "https://registry.yarnpkg.com/@builder.io/qwik/-/qwik-0.19.2.tgz#d2f7d39bf6adb6de8497690cb3c41ad62a5fc1c6"
|
||||
integrity sha512-Rxdyx96ucx0+nABLsg1sH7SgrlMdHgpxbR+NR3c53Ux4Jj+Xf+QHNQSIKF9zTV8KClrzmDlqILgmkDU4uMTVcg==
|
||||
|
||||
"@cush/relative@^1.0.0":
|
||||
version "1.0.0"
|
||||
|
@ -1031,6 +1031,11 @@
|
|||
"@typescript-eslint/types" "5.46.1"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
|
||||
|
||||
acorn-jsx@^5.0.0, acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
|
||||
|
@ -1323,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==
|
||||
|
@ -1356,7 +1361,7 @@ character-reference-invalid@^2.0.0:
|
|||
resolved "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz"
|
||||
integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==
|
||||
|
||||
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3:
|
||||
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.2, chokidar@^3.5.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz"
|
||||
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
|
||||
|
@ -1451,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"
|
||||
|
@ -1495,10 +1485,12 @@ 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"
|
||||
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
|
||||
version "4.3.4"
|
||||
|
@ -2149,6 +2141,11 @@ human-signals@^2.1.0:
|
|||
resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz"
|
||||
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
|
||||
|
||||
ignore-by-default@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
|
||||
integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
|
||||
|
||||
ignore@^5.2.0:
|
||||
version "5.2.4"
|
||||
resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz"
|
||||
|
@ -2809,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"
|
||||
|
@ -3311,6 +3303,11 @@ ms@2.1.2:
|
|||
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
ms@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
mz@^2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz"
|
||||
|
@ -3359,6 +3356,29 @@ node-releases@^2.0.6:
|
|||
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz"
|
||||
integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==
|
||||
|
||||
nodemon@^2.0.20:
|
||||
version "2.0.20"
|
||||
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.20.tgz#e3537de768a492e8d74da5c5813cb0c7486fc701"
|
||||
integrity sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==
|
||||
dependencies:
|
||||
chokidar "^3.5.2"
|
||||
debug "^3.2.7"
|
||||
ignore-by-default "^1.0.1"
|
||||
minimatch "^3.1.2"
|
||||
pstree.remy "^1.1.8"
|
||||
semver "^5.7.1"
|
||||
simple-update-notifier "^1.0.7"
|
||||
supports-color "^5.5.0"
|
||||
touch "^3.1.0"
|
||||
undefsafe "^2.0.5"
|
||||
|
||||
nopt@~1.0.10:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
|
||||
integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==
|
||||
dependencies:
|
||||
abbrev "1"
|
||||
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
|
||||
|
@ -3621,6 +3641,11 @@ property-information@^6.0.0:
|
|||
resolved "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz"
|
||||
integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==
|
||||
|
||||
pstree.remy@^1.1.8:
|
||||
version "1.1.8"
|
||||
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
|
||||
integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==
|
||||
|
||||
punycode@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz"
|
||||
|
@ -3760,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"
|
||||
|
@ -3790,11 +3808,21 @@ semver@7.x, semver@^7.3.5, semver@^7.3.7:
|
|||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@^5.7.1:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
|
||||
semver@^6.0.0, semver@^6.3.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
semver@~7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
|
||||
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
|
||||
|
||||
shebang-command@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
|
||||
|
@ -3807,16 +3835,18 @@ 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"
|
||||
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
|
||||
|
||||
simple-update-notifier@^1.0.7:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz#67694c121de354af592b347cdba798463ed49c82"
|
||||
integrity sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==
|
||||
dependencies:
|
||||
semver "~7.0.0"
|
||||
|
||||
sisteransi@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz"
|
||||
|
@ -3855,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"
|
||||
|
@ -3948,7 +3973,7 @@ sucrase@^3.20.3:
|
|||
pirates "^4.0.1"
|
||||
ts-interface-checker "^0.1.9"
|
||||
|
||||
supports-color@^5.3.0:
|
||||
supports-color@^5.3.0, supports-color@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz"
|
||||
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
|
||||
|
@ -3962,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==
|
||||
|
@ -4048,10 +4073,12 @@ to-regex-range@^5.0.1:
|
|||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
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==
|
||||
touch@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
|
||||
integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==
|
||||
dependencies:
|
||||
nopt "~1.0.10"
|
||||
|
||||
trim-lines@^3.0.0:
|
||||
version "3.0.1"
|
||||
|
@ -4096,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"
|
||||
|
@ -4135,6 +4157,11 @@ typescript@4.9.4:
|
|||
resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz"
|
||||
integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==
|
||||
|
||||
undefsafe@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
|
||||
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
|
||||
|
||||
undici@5.19.1:
|
||||
version "5.19.1"
|
||||
resolved "https://registry.yarnpkg.com/undici/-/undici-5.19.1.tgz#92b1fd3ab2c089b5a6bd3e579dcda8f1934ebf6d"
|
||||
|
|
|
@ -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,80 @@
|
|||
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 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_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 })
|
||||
}
|
||||
|
||||
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,73 @@
|
|||
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'
|
||||
import { ServerSettingsData } from 'wildebeest/frontend/src/routes/(admin)/settings/server-settings/layout'
|
||||
|
||||
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<ServerSettingsData>()
|
||||
|
||||
const result = await upsertServerSettings(db, data)
|
||||
|
||||
if (result && !result.success) {
|
||||
return new Response('SQL error: ' + result.error, { status: 500 })
|
||||
}
|
||||
|
||||
return new Response('', { status: 200 })
|
||||
}
|
||||
|
||||
export async function upsertServerSettings(db: Database, settings: Partial<ServerSettingsData>) {
|
||||
const settingsEntries = Object.entries(settings)
|
||||
|
||||
if (!settingsEntries.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
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
|
||||
`
|
||||
|
||||
return await db
|
||||
.prepare(query)
|
||||
.bind(...settingsEntries.flat())
|
||||
.run()
|
||||
}
|
|
@ -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
|
||||
);
|
|
@ -14,7 +14,6 @@
|
|||
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||
"@typescript-eslint/parser": "^5.46.1",
|
||||
"better-sqlite3": "8",
|
||||
"concurrently": "^7.6.0",
|
||||
"eslint": "^8.29.0",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-miniflare": "^2.11.0",
|
||||
|
@ -38,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"
|
||||
},
|
||||
|
|
|
@ -11,28 +11,28 @@ test.skip('View of the about page', async ({ page }) => {
|
|||
await expect(page.getByTestId('domain-text')).toHaveText('0.0.0.0')
|
||||
await expect(page.getByTestId('social-text')).toHaveText('Decentralised social media powered by Mastodon')
|
||||
|
||||
await expect(page.getByTestId('contact').getByText('Administered by:George 👍@george')).toBeVisible()
|
||||
await expect(page.getByTestId('contact').getByText('contact:test@test.com')).toBeVisible()
|
||||
// await expect(page.getByTestId('contact').getByText('Administered by: ...')).toBeVisible()
|
||||
// await expect(page.getByTestId('contact').getByText('contact: ...')).toBeVisible()
|
||||
|
||||
await expect(page.getByRole('region').filter({ hasText: 'Please mind that the staff' })).not.toBeVisible()
|
||||
await expect(page.getByRole('region').filter({ hasText: 'this is a test wildebeest instance!' })).not.toBeVisible()
|
||||
await page.getByRole('button', { name: 'About' }).click()
|
||||
await expect(page.getByRole('region').filter({ hasText: 'Please mind that the staff' })).toBeVisible()
|
||||
await expect(page.getByRole('region').filter({ hasText: 'this is a test wildebeest instance!' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'About' }).click()
|
||||
await expect(page.getByRole('region').filter({ hasText: 'Please mind that the staff' })).not.toBeVisible()
|
||||
await expect(page.getByRole('region').filter({ hasText: 'this is a test wildebeest instance!' })).not.toBeVisible()
|
||||
|
||||
const getRuleLocator = (ruleId: string) =>
|
||||
page.getByRole('listitem').filter({ has: page.getByText(ruleId, { exact: true }) })
|
||||
|
||||
await expect(page.getByRole('listitem')).toHaveCount(0)
|
||||
await expect(getRuleLocator('1')).not.toBeVisible()
|
||||
await expect(getRuleLocator('2')).not.toBeVisible()
|
||||
await expect(getRuleLocator('3')).not.toBeVisible()
|
||||
await page.getByRole('button', { name: 'Server rules' }).click()
|
||||
await expect(page.getByRole('listitem')).toHaveCount(3)
|
||||
await expect(getRuleLocator('1')).toBeVisible()
|
||||
await expect(getRuleLocator('1')).toContainText(
|
||||
'Sexually explicit or violent media must be marked as sensitive when posting'
|
||||
)
|
||||
await expect(getRuleLocator('1')).toContainText("don't be mean")
|
||||
await expect(getRuleLocator('2')).toBeVisible()
|
||||
await expect(getRuleLocator('2')).toContainText('No racism, sexism, homophobia, transphobia, xenophobia, or casteism')
|
||||
await expect(getRuleLocator('2')).toContainText("don't insult people")
|
||||
await expect(getRuleLocator('3')).toBeVisible()
|
||||
await expect(getRuleLocator('3')).toContainText('No incitement of violence or promotion of violent ideologies')
|
||||
await expect(getRuleLocator('3')).toContainText('respect the rules')
|
||||
})
|
||||
|
|
53
yarn.lock
53
yarn.lock
|
@ -1374,7 +1374,7 @@ chalk@^2.0.0, chalk@^2.4.1:
|
|||
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==
|
||||
|
@ -1465,21 +1465,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.yarnpkg.com/concurrently/-/concurrently-7.6.0.tgz#531a6f5f30cf616f355a4afb8f8fcb2bba65a49a"
|
||||
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"
|
||||
|
@ -1525,11 +1510,6 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
|||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
date-fns@^2.29.1:
|
||||
version "2.29.3"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
|
||||
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
|
||||
|
||||
debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
|
||||
|
@ -3095,11 +3075,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.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz"
|
||||
|
@ -3726,13 +3701,6 @@ run-parallel@^1.1.9:
|
|||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
rxjs@^7.0.0:
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
|
||||
integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
|
||||
|
@ -3805,7 +3773,7 @@ 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.6.1, shell-quote@^1.7.3:
|
||||
shell-quote@^1.6.1:
|
||||
version "1.7.4"
|
||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.4.tgz#33fe15dee71ab2a81fcbd3a52106c5cfb9fb75d8"
|
||||
integrity sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==
|
||||
|
@ -3879,11 +3847,6 @@ sourcemap-codec@^1.4.8:
|
|||
resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz"
|
||||
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
||||
|
||||
spawn-command@^0.0.2-1:
|
||||
version "0.0.2-1"
|
||||
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
|
||||
integrity sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==
|
||||
|
||||
spdx-correct@^3.0.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
|
||||
|
@ -4034,7 +3997,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==
|
||||
|
@ -4107,11 +4070,6 @@ toucan-js@^3.1.0:
|
|||
"@sentry/types" "7.28.1"
|
||||
"@sentry/utils" "7.28.1"
|
||||
|
||||
tree-kill@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
||||
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
|
||||
|
||||
ts-jest@^29.0.3:
|
||||
version "29.0.3"
|
||||
resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz"
|
||||
|
@ -4131,11 +4089,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.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
|
||||
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"
|
||||
|
|
Ładowanie…
Reference in New Issue