wildebeest/frontend/src/routes/(admin)/oauth/authorize/index.tsx

145 wiersze
5.2 KiB
TypeScript

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'
import { WildebeestLogo } from '~/components/MastodonLogo'
import { Avatar } from '~/components/avatar'
import { getPersonByEmail } from 'wildebeest/backend/src/activitypub/actors'
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
import { buildRedirect } from 'wildebeest/functions/oauth/authorize'
export const clientLoader = loader$<Promise<Client>, { DATABASE: D1Database }>(async ({ platform, query, html }) => {
const client_id = query.get('client_id') || ''
let client: Client | null = null
try {
client = await getClientById(platform.DATABASE, client_id)
} catch {
throw html(500, getErrorHtml('An error occurred while trying to fetch the client data, please try again later'))
}
if (client === null) {
throw new Error('client not found')
}
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 (err: unknown) {
console.warn((err as { stack: unknown }).stack)
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(platform.DATABASE, payload.email)
if (person === null) {
const isFirstLogin = true
const res = await buildRedirect(platform.DATABASE, 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: payload.email, avatar, name, url }
})
export default component$(() => {
const client = clientLoader().value
const { email, avatar, name: display_name, url } = userLoader().value
return (
<div class="flex flex-col p-4 items-center">
<h1 class="text-center mt-3 mb-5 flex items-center">
<WildebeestLogo size="medium" />
</h1>
<hr class="border-t-0 border-b border-wildebeest-700 w-full mb-10" />
<div class="max-w-lg text-left">
<div class="border-b border-wildebeest-700 pb-10 mx-auto mt-5 mb-[5rem] flex flex-wrap justify-between gap-4 items-center">
<div class="grid grid-rows-[repeat(2,_1fr)] grid-cols-[max-content,_1fr] items-center">
<div class="row-span-2 mr-4">
<Avatar
primary={{
avatar: avatar.toString(),
display_name,
url: url.toString(),
}}
secondary={null}
withLinks={true}
/>
</div>
<p class="col-start-2">Signed in as:</p>
<p class="col-start-2 font-bold">{email}</p>
</div>
<a
class="no-underline col-start-3 row-span-full ml-auto"
href="/cdn-cgi/access/logout"
aria-label="Change Account"
>
<div class="text-wildebeest-500 opacity-40 hover:opacity-70 focus:opacity-70 flex items-baseline">
<i class="fa fa-right-from-bracket text-[2.2rem]" />
</div>
</a>
</div>
<h2 class="text text-xl font-semibold mb-5">Authorization required</h2>
<p class="mb-10">
<strong class="text-[1rem]">{client.name}</strong>
<span class="text-wildebeest-400">
{' '}
would like permission to access your account. It is a third-party application.
</span>
<strong class="text-[1rem]"> If you do not trust it, then you should not authorize it.</strong>
</p>
<h2 class="text text-xl font-semibold mb-5">Review permissions</h2>
<div class="mb-5 grid grid-rows-[repeat(2,_1fr)] grid-cols-[max-content,_1fr] items-center bg-wildebeest-800 border border-wildebeest-600 p-4 rounded-md">
<i class="fa-solid fa-check col-span-1 row-span-full text-[1.3rem] ml-2 mr-5 text-green-500 w-[1.5rem]"></i>
<strong class="col-start-2">Everything</strong>
<span class="col-start-2">Read and write access</span>
</div>
<form method="post" class="flex flex-col w-full">
<button
type="submit"
class="mx-auto px-9 my-9 uppercase font-semibold bg-wildebeest-vibrant-600 hover:bg-wildebeest-vibrant-500 p-3 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"
>
Authorize
</button>
</form>
</div>
</div>
)
})
export const head: DocumentHead = () => {
return {
title: 'Wildebeest Authorization required',
meta: [
{
name: 'description',
content: 'Wildebeest Authorization required',
},
],
}
}