Merge pull request #347 from cloudflare/server-settings-ui

Server settings UI
pull/362/head
Dario Piotrowicz 2023-03-01 20:53:30 +00:00 zatwierdzone przez GitHub
commit 0580e089ab
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
29 zmienionych plików z 1016 dodań i 248 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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",

Wyświetl plik

@ -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>
)
}
)

Wyświetl plik

@ -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>
)
}
)

Wyświetl plik

@ -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

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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 />

Wyświetl plik

@ -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>
)
})

Wyświetl plik

@ -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>
)
})

Wyświetl plik

@ -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$(() => <></>)

Wyświetl plik

@ -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>
)
})

Wyświetl plik

@ -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>
</>
)
})

Wyświetl plik

@ -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>
</>
)
})

Wyświetl plik

@ -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

Wyświetl plik

@ -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>

Wyświetl plik

@ -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,

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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"

Wyświetl plik

@ -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 })
}

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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()
}

Wyświetl plik

@ -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()
}

Wyświetl plik

@ -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 })

Wyświetl plik

@ -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
);

Wyświetl plik

@ -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"
},

Wyświetl plik

@ -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')
})

Wyświetl plik

@ -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"