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,
|
db: Database,
|
||||||
userKEK: string,
|
userKEK: string,
|
||||||
email: string,
|
email: string,
|
||||||
properties: PersonProperties = {}
|
properties: PersonProperties = {},
|
||||||
|
admin: boolean = false
|
||||||
): Promise<Person> {
|
): Promise<Person> {
|
||||||
const userKeyPair = await generateUserKey(userKEK)
|
const userKeyPair = await generateUserKey(userKEK)
|
||||||
|
|
||||||
|
@ -198,12 +199,12 @@ export async function createPerson(
|
||||||
const row = await db
|
const row = await db
|
||||||
.prepare(
|
.prepare(
|
||||||
`
|
`
|
||||||
INSERT INTO actors(id, type, email, pubkey, privkey, privkey_salt, properties)
|
INSERT INTO actors(id, type, email, pubkey, privkey, privkey_salt, properties, is_admin)
|
||||||
VALUES(?, ?, ?, ?, ?, ?, ?)
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
RETURNING *
|
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()
|
.first()
|
||||||
|
|
||||||
return personFromRow(row)
|
return personFromRow(row)
|
||||||
|
|
|
@ -74,12 +74,21 @@ async function getOrCreatePerson(
|
||||||
db: Database,
|
db: Database,
|
||||||
{ username, avatar, display_name }: Account
|
{ username, avatar, display_name }: Account
|
||||||
): Promise<Person> {
|
): 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
|
if (person) return person
|
||||||
const newPerson = await createPerson(domain, db, 'test-kek', username, {
|
const newPerson = await createPerson(
|
||||||
icon: { url: avatar },
|
domain,
|
||||||
name: display_name,
|
db,
|
||||||
})
|
'test-kek',
|
||||||
|
email,
|
||||||
|
{
|
||||||
|
icon: { url: avatar },
|
||||||
|
name: display_name,
|
||||||
|
},
|
||||||
|
isAdmin
|
||||||
|
)
|
||||||
if (!newPerson) {
|
if (!newPerson) {
|
||||||
throw new Error('Could not create Actor ' + username)
|
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 { component$ } from '@builder.io/qwik'
|
||||||
import * as access from 'wildebeest/backend/src/access'
|
|
||||||
import type { Client } from 'wildebeest/backend/src/mastodon/client'
|
import type { Client } from 'wildebeest/backend/src/mastodon/client'
|
||||||
import { getClientById } from 'wildebeest/backend/src/mastodon/client'
|
import { getClientById } from 'wildebeest/backend/src/mastodon/client'
|
||||||
import { DocumentHead, loader$ } from '@builder.io/qwik-city'
|
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 { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
|
||||||
import { buildRedirect } from 'wildebeest/functions/oauth/authorize'
|
import { buildRedirect } from 'wildebeest/functions/oauth/authorize'
|
||||||
import { getDatabase } from 'wildebeest/backend/src/database'
|
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') || ''
|
const client_id = query.get('client_id') || ''
|
||||||
let client: Client | null = null
|
let client: Client | null = null
|
||||||
try {
|
try {
|
||||||
|
@ -26,50 +26,41 @@ export const clientLoader = loader$<Promise<Client>, { DATABASE: D1Database }>(a
|
||||||
return client
|
return client
|
||||||
})
|
})
|
||||||
|
|
||||||
export const userLoader = loader$<
|
export const userLoader = loader$<Promise<{ email: string; avatar: URL; name: string; url: URL }>>(
|
||||||
Promise<{ email: string; avatar: URL; name: string; url: URL }>,
|
async ({ cookie, platform, html, request, redirect, text }) => {
|
||||||
{ DATABASE: D1Database; domain: string }
|
const jwt = cookie.get('CF_Authorization')
|
||||||
>(async ({ cookie, platform, html, request, redirect, text }) => {
|
let email = ''
|
||||||
const jwt = cookie.get('CF_Authorization')
|
try {
|
||||||
if (jwt === null) {
|
email = getJwtEmail(jwt?.value ?? '')
|
||||||
throw html(500, getErrorHtml('Missing Authorization'))
|
} catch (e) {
|
||||||
}
|
throw html(500, getErrorHtml((e as Error)?.message))
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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$(() => {
|
export default component$(() => {
|
||||||
const client = clientLoader().value
|
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'
|
import { Person } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
|
|
||||||
export const accountPageLoader = loader$<
|
export const accountPageLoader = loader$<
|
||||||
Promise<{ account: MastodonAccount; accountHandle: string; isValidStatus: boolean }>,
|
Promise<{ account: MastodonAccount; accountHandle: string; isValidStatus: boolean }>
|
||||||
{ DATABASE: D1Database }
|
|
||||||
>(async ({ platform, params, request, html }) => {
|
>(async ({ platform, params, request, html }) => {
|
||||||
let isValidStatus = false
|
let isValidStatus = false
|
||||||
let account: MastodonAccount | null = null
|
let account: MastodonAccount | null = null
|
||||||
|
|
|
@ -1,67 +1,76 @@
|
||||||
import { component$ } from '@builder.io/qwik'
|
import { component$ } from '@builder.io/qwik'
|
||||||
import { DocumentHead, loader$ } from '@builder.io/qwik-city'
|
import { DocumentHead, loader$ } from '@builder.io/qwik-city'
|
||||||
|
import { getDatabase } from 'wildebeest/backend/src/database'
|
||||||
import { getDomain } from 'wildebeest/backend/src/utils/getDomain'
|
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 { Accordion } from '~/components/Accordion/Accordion'
|
||||||
import { AccountCard } from '~/components/AccountCard/AccountCard'
|
// import { AccountCard } from '~/components/AccountCard/AccountCard'
|
||||||
import { HtmlContent } from '~/components/HtmlContent/HtmlContent'
|
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 { Account } from '~/types'
|
||||||
import { getDocumentHead } from '~/utils/getDocumentHead'
|
import { getDocumentHead } from '~/utils/getDocumentHead'
|
||||||
import { getNotFoundHtml } from '~/utils/getNotFoundHtml/getNotFoundHtml'
|
|
||||||
import { instanceLoader } from '../layout'
|
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 = {
|
type AboutInfo = {
|
||||||
image: string
|
image: string
|
||||||
domain: string
|
domain: string
|
||||||
contact: {
|
admin: { account: Account | null; email: string }
|
||||||
account: Account
|
rules: { id: number; text: string }[]
|
||||||
email: string
|
|
||||||
}
|
|
||||||
rules: { id: string; text: string }[]
|
|
||||||
extended_description: {
|
extended_description: {
|
||||||
updated_at: string
|
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const aboutInfoLoader = loader$<Promise<AboutInfo>>(async ({ resolveValue, request, html }) => {
|
export const aboutInfoLoader = loader$<Promise<AboutInfo>>(async ({ resolveValue, request, platform, html }) => {
|
||||||
// TODO: properly implement loader and remove the following 404 throw
|
|
||||||
throw html(404, getNotFoundHtml())
|
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 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 {
|
return {
|
||||||
image: instance.thumbnail,
|
image: instance.thumbnail,
|
||||||
domain: getDomain(request.url),
|
domain: getDomain(request.url),
|
||||||
contact: {
|
admin: { account: JSON.parse(JSON.stringify(adminAccount)), email: platform.ADMIN_EMAIL },
|
||||||
account: george,
|
rules: JSON.parse(JSON.stringify(rules.sort(({ id: idA }, { id: idB }) => idA - idB))),
|
||||||
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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
extended_description: {
|
extended_description: {
|
||||||
updated_at: '2023-01-19T14:55:44Z',
|
content: brandingData?.['extended description'] ?? '',
|
||||||
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',
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -86,14 +95,19 @@ export default component$(() => {
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="rounded bg-wildebeest-700 flex flex-col md:flex-row p-2 w-full my-5" data-testid="contact">
|
<div
|
||||||
<div class="flex-1 p-4">
|
class="rounded bg-wildebeest-700 flex flex-col md:flex-row p-2 w-full my-5 overflow-auto"
|
||||||
<span class="block uppercase text-wildebeest-500 font-semibold mb-5">Administered by:</span>
|
data-testid="contact"
|
||||||
<AccountCard account={aboutInfo.contact.account} subText="username" />
|
>
|
||||||
</div>
|
{!!aboutInfo.admin.account && (
|
||||||
<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="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 class="block uppercase text-wildebeest-500 font-semibold mb-5">Contact:</span>
|
||||||
<span>{aboutInfo.contact.email}</span>
|
<span>{aboutInfo.admin.email}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,7 @@ import { InstanceConfigContext } from '~/utils/instanceConfig'
|
||||||
import { getDocumentHead } from '~/utils/getDocumentHead'
|
import { getDocumentHead } from '~/utils/getDocumentHead'
|
||||||
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
|
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
|
||||||
|
|
||||||
export const instanceLoader = loader$<
|
export const instanceLoader = loader$<Promise<InstanceConfig>>(async ({ platform, html }) => {
|
||||||
Promise<InstanceConfig>,
|
|
||||||
{ DATABASE: D1Database; INSTANCE_TITLE: string; INSTANCE_DESCR: string; ADMIN_EMAIL: string }
|
|
||||||
>(async ({ platform, html }) => {
|
|
||||||
const env = {
|
const env = {
|
||||||
INSTANCE_DESCR: platform.INSTANCE_DESCR,
|
INSTANCE_DESCR: platform.INSTANCE_DESCR,
|
||||||
INSTANCE_TITLE: platform.INSTANCE_TITLE,
|
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"
|
escape-string-regexp "^1.0.5"
|
||||||
supports-color "^5.3.0"
|
supports-color "^5.3.0"
|
||||||
|
|
||||||
chalk@^4.0.0, chalk@^4.1.0:
|
chalk@^4.0.0:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz"
|
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz"
|
||||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
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"
|
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
|
||||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
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:
|
convert-source-map@^1.6.0, convert-source-map@^1.7.0:
|
||||||
version "1.9.0"
|
version "1.9.0"
|
||||||
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz"
|
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"
|
resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz"
|
||||||
integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
|
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:
|
debug@^3.2.7:
|
||||||
version "3.2.7"
|
version "3.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
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"
|
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
|
||||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
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:
|
longest-streak@^3.0.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz"
|
resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz"
|
||||||
|
@ -3810,13 +3785,6 @@ run-parallel@^1.1.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask "^1.2.2"
|
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:
|
sade@^1.7.3:
|
||||||
version "1.8.1"
|
version "1.8.1"
|
||||||
resolved "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz"
|
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"
|
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
|
||||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
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:
|
signal-exit@^3.0.3, signal-exit@^3.0.7:
|
||||||
version "3.0.7"
|
version "3.0.7"
|
||||||
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz"
|
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"
|
resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz"
|
||||||
integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
|
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:
|
sprintf-js@~1.0.2:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
|
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
|
||||||
|
@ -4029,7 +3987,7 @@ supports-color@^7.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag "^4.0.0"
|
has-flag "^4.0.0"
|
||||||
|
|
||||||
supports-color@^8.0.0, supports-color@^8.1.0:
|
supports-color@^8.0.0:
|
||||||
version "8.1.1"
|
version "8.1.1"
|
||||||
resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz"
|
resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz"
|
||||||
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
|
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
|
||||||
|
@ -4122,11 +4080,6 @@ touch@^3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
nopt "~1.0.10"
|
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:
|
trim-lines@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz"
|
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"
|
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
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:
|
tsutils@^3.21.0:
|
||||||
version "3.21.0"
|
version "3.21.0"
|
||||||
resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"
|
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 errors from 'wildebeest/backend/src/errors'
|
||||||
import * as access from 'wildebeest/backend/src/access'
|
import * as access from 'wildebeest/backend/src/access'
|
||||||
import { type Database, getDatabase } from 'wildebeest/backend/src/database'
|
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 }) => {
|
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)
|
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> {
|
): Promise<Response> {
|
||||||
const url = new URL(request.url)
|
const url = new URL(request.url)
|
||||||
const cookie = parse(request.headers.get('Cookie') || '')
|
const cookie = parse(request.headers.get('Cookie') || '')
|
||||||
|
let email = ''
|
||||||
const jwt = cookie['CF_Authorization']
|
const jwt = cookie['CF_Authorization']
|
||||||
if (!jwt) {
|
try {
|
||||||
return errors.notAuthorized('missing CF_Authorization')
|
email = getJwtEmail(jwt ?? '')
|
||||||
|
} catch (e) {
|
||||||
|
return errors.notAuthorized((e as Error)?.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = access.getPayload(jwt)
|
await access.generateValidator({
|
||||||
if (!payload.email) {
|
|
||||||
return errors.notAuthorized('missing email')
|
|
||||||
}
|
|
||||||
|
|
||||||
const validatate = access.generateValidator({
|
|
||||||
jwt,
|
jwt,
|
||||||
domain: accessDomain,
|
domain: accessDomain,
|
||||||
aud: accessAud,
|
aud: accessAud,
|
||||||
})
|
})(request)
|
||||||
await validatate(request)
|
|
||||||
|
|
||||||
const domain = url.hostname
|
const domain = url.hostname
|
||||||
|
|
||||||
|
@ -52,7 +49,7 @@ export async function handlePostRequest(
|
||||||
properties.name = formData.get('name') || ''
|
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')) {
|
if (!url.searchParams.has('redirect_uri')) {
|
||||||
return new Response('', { status: 400 })
|
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",
|
"pages": "NO_D1_WARNING=true wrangler pages",
|
||||||
"database:migrate": "yarn d1 migrations apply DATABASE",
|
"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",
|
"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",
|
"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' --compatibility-date=2022-12-20",
|
"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:init": "yarn pages project create wildebeest && yarn d1 create wildebeest",
|
||||||
"deploy": "yarn build && yarn database:migrate && yarn pages publish frontend/dist --project-name=wildebeest"
|
"deploy": "yarn build && yarn database:migrate && yarn pages publish frontend/dist --project-name=wildebeest"
|
||||||
},
|
},
|
||||||
|
|
Ładowanie…
Reference in New Issue