diff --git a/backend/src/mastodon/client.ts b/backend/src/mastodon/client.ts index 2c2df3d..1f96606 100644 --- a/backend/src/mastodon/client.ts +++ b/backend/src/mastodon/client.ts @@ -6,16 +6,16 @@ export interface Client { secret: string name: string redirect_uris: string - website: string scopes: string + website?: string } export async function createClient( db: Database, name: string, redirect_uris: string, - website: string, - scopes: string + scopes: string, + website?: string ): Promise { const id = crypto.randomUUID() @@ -28,7 +28,10 @@ export async function createClient( INSERT INTO clients (id, secret, name, redirect_uris, website, scopes) VALUES (?, ?, ?, ?, ?, ?) ` - const { success, error } = await db.prepare(query).bind(id, secret, name, redirect_uris, website, scopes).run() + const { success, error } = await db + .prepare(query) + .bind(id, secret, name, redirect_uris, website === undefined ? null : website, scopes) + .run() if (!success) { throw new Error('SQL error: ' + error) } diff --git a/backend/src/utils/auth/getAdmins.ts b/backend/src/utils/auth/getAdmins.ts new file mode 100644 index 0000000..0232cac --- /dev/null +++ b/backend/src/utils/auth/getAdmins.ts @@ -0,0 +1,15 @@ +import { type Database } from 'wildebeest/backend/src/database' +import { Person, personFromRow } from 'wildebeest/backend/src/activitypub/actors' + +export async function getAdmins(db: Database): Promise { + let rows: unknown[] = [] + try { + const stmt = db.prepare('SELECT * FROM actors WHERE is_admin=1') + const result = await stmt.all() + rows = result.success ? (result.results as unknown[]) : [] + } catch { + /* empty */ + } + + return rows.map(personFromRow) +} diff --git a/frontend/src/utils/getJwtEmail.ts b/backend/src/utils/auth/getJwtEmail.ts similarity index 100% rename from frontend/src/utils/getJwtEmail.ts rename to backend/src/utils/auth/getJwtEmail.ts diff --git a/backend/src/utils/auth/isUserAdmin.ts b/backend/src/utils/auth/isUserAdmin.ts new file mode 100644 index 0000000..704959f --- /dev/null +++ b/backend/src/utils/auth/isUserAdmin.ts @@ -0,0 +1,30 @@ +import { emailSymbol } from 'wildebeest/backend/src/activitypub/actors' +import { Database } from 'wildebeest/backend/src/database' +import { getJwtEmail } from 'wildebeest/backend/src/utils/auth/getJwtEmail' +import { getAdmins } from './getAdmins' +import { isUserAuthenticated } from './isUserAuthenticated' + +export async function isUserAdmin( + request: Request, + jwt: string, + accessAuthDomain: string, + accessAud: string, + database: Database +): Promise { + let email: string + + try { + const authenticated = await isUserAuthenticated(request, jwt, accessAuthDomain, accessAud) + if (!authenticated) { + return false + } + + email = getJwtEmail(jwt) + } catch { + return false + } + + const admins = await getAdmins(database) + + return admins.some((admin) => admin[emailSymbol] === email) +} diff --git a/backend/src/utils/auth/isUserAuthenticated.ts b/backend/src/utils/auth/isUserAuthenticated.ts new file mode 100644 index 0000000..190580d --- /dev/null +++ b/backend/src/utils/auth/isUserAuthenticated.ts @@ -0,0 +1,22 @@ +import * as access from 'wildebeest/backend/src/access' + +export async function isUserAuthenticated(request: Request, jwt: string, accessAuthDomain: string, accessAud: string) { + if (!jwt) return false + + try { + const validate = access.generateValidator({ + jwt, + domain: accessAuthDomain, + aud: accessAud, + }) + await validate(new Request(request.url)) + } catch { + return false + } + + const identity = await access.getIdentity({ jwt, domain: accessAuthDomain }) + if (identity) { + return true + } + return false +} diff --git a/backend/test/mastodon/apps.spec.ts b/backend/test/mastodon/apps.spec.ts index 998a2fa..17896f5 100644 --- a/backend/test/mastodon/apps.spec.ts +++ b/backend/test/mastodon/apps.spec.ts @@ -36,6 +36,33 @@ describe('Mastodon APIs', () => { assert.deepEqual(rest, {}) }) + test('POST /apps registers client without website', async () => { + const db = await makeDB() + const vapidKeys = await generateVAPIDKeys() + const request = new Request('https://example.com', { + method: 'POST', + body: '{"redirect_uris":"mastodon://example.com/oauth","client_name":"Example mastodon client","scopes":"read write follow push"}', + headers: { + 'content-type': 'application/json', + }, + }) + + const res = await apps.handleRequest(db, request, vapidKeys) + assert.equal(res.status, 200) + assertCORS(res) + assertJSON(res) + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { name, redirect_uri, client_id, client_secret, vapid_key, id, ...rest } = await res.json< + Record + >() + + assert.equal(name, 'Example mastodon client') + assert.equal(redirect_uri, 'mastodon://example.com/oauth') + assert.equal(id, '20') + assert.deepEqual(rest, {}) + }) + test('POST /apps returns 422 for malformed requests', async () => { // client_name and redirect_uris are required according to https://docs.joinmastodon.org/methods/apps/#form-data-parameters const db = await makeDB() diff --git a/backend/test/utils.ts b/backend/test/utils.ts index 7220ca7..418607e 100644 --- a/backend/test/utils.ts +++ b/backend/test/utils.ts @@ -73,7 +73,7 @@ export async function createTestClient( redirectUri: string = 'https://localhost', scopes: string = 'read follow' ): Promise { - return createClient(db, 'test client', redirectUri, 'https://cloudflare.com', scopes) + return createClient(db, 'test client', redirectUri, scopes, 'https://cloudflare.com') } type TestQueue = Queue & { messages: Array } diff --git a/frontend/src/components/layout/RightColumn/RightColumn.tsx b/frontend/src/components/layout/RightColumn/RightColumn.tsx index 73a8659..e7a12a8 100644 --- a/frontend/src/components/layout/RightColumn/RightColumn.tsx +++ b/frontend/src/components/layout/RightColumn/RightColumn.tsx @@ -1,7 +1,7 @@ import { component$ } from '@builder.io/qwik' import { Link, useLocation } from '@builder.io/qwik-city' import { WildebeestLogo } from '~/components/MastodonLogo' -import { accessLoader } from '~/routes/layout' +import { authLoader } from '~/routes/layout' type LinkConfig = { iconName: string @@ -11,7 +11,7 @@ type LinkConfig = { } export default component$(() => { - const accessData = accessLoader().value + const { isAuthorized, loginUrl } = authLoader().value const location = useLocation() const renderNavLink = ({ iconName, linkText, linkTarget, linkActiveRegex }: LinkConfig) => { @@ -55,15 +55,15 @@ export default component$(() => { {renderNavLink(aboutLink)} */} - {!accessData.isAuthorized && ( + {!isAuthorized && ( Sign in )} - {accessData.isAuthorized && ( + {isAuthorized && ( Preferences diff --git a/frontend/src/routes/(admin)/oauth/authorize/index.tsx b/frontend/src/routes/(admin)/oauth/authorize/index.tsx index 027ce5d..2c4e711 100644 --- a/frontend/src/routes/(admin)/oauth/authorize/index.tsx +++ b/frontend/src/routes/(admin)/oauth/authorize/index.tsx @@ -8,7 +8,7 @@ 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' +import { getJwtEmail } from 'wildebeest/backend/src/utils/auth/getJwtEmail' export const clientLoader = loader$>(async ({ platform, query, html }) => { const client_id = query.get('client_id') || '' @@ -21,7 +21,7 @@ export const clientLoader = loader$>(async ({ platform, query, h 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') + throw html(500, getErrorHtml('client not found')) } return client }) diff --git a/frontend/src/routes/(admin)/settings/(admin)/layout.tsx b/frontend/src/routes/(admin)/settings/(admin)/layout.tsx new file mode 100644 index 0000000..5ee4079 --- /dev/null +++ b/frontend/src/routes/(admin)/settings/(admin)/layout.tsx @@ -0,0 +1,11 @@ +import { component$, Slot } from '@builder.io/qwik' + +export { adminLoader } from '~/utils/adminLoader' + +export default component$(() => { + return ( + <> + + + ) +}) diff --git a/frontend/src/routes/(admin)/settings/migration/index.tsx b/frontend/src/routes/(admin)/settings/(admin)/migration/index.tsx similarity index 90% rename from frontend/src/routes/(admin)/settings/migration/index.tsx rename to frontend/src/routes/(admin)/settings/(admin)/migration/index.tsx index 6180b41..e1c0e6b 100644 --- a/frontend/src/routes/(admin)/settings/migration/index.tsx +++ b/frontend/src/routes/(admin)/settings/(admin)/migration/index.tsx @@ -1,17 +1,4 @@ import { component$ } from '@builder.io/qwik' -import { loader$ } from '@builder.io/qwik-city' -// import { checkAuth } from '~/utils/checkAuth' - -export const loader = loader$(async ({ redirect }) => { - // Hiding this page for now - redirect(303, '/explore') - - // const isAuthorized = await checkAuth(request, platform) - - // if (!isAuthorized) { - // redirect(303, '/explore') - // } -}) export default component$(() => { return ( diff --git a/frontend/src/routes/(admin)/settings/server-settings/about/index.tsx b/frontend/src/routes/(admin)/settings/(admin)/server-settings/about/index.tsx similarity index 95% rename from frontend/src/routes/(admin)/settings/server-settings/about/index.tsx rename to frontend/src/routes/(admin)/settings/(admin)/server-settings/about/index.tsx index 4aad8cc..c5e515d 100644 --- a/frontend/src/routes/(admin)/settings/server-settings/about/index.tsx +++ b/frontend/src/routes/(admin)/settings/(admin)/server-settings/about/index.tsx @@ -17,7 +17,9 @@ export const action = action$(async (data, { request, platform }) => { try { const response = await handleRequestPost( await getDatabase(platform), - new Request(request, { body: JSON.stringify(data) }) + new Request(request, { body: JSON.stringify(data) }), + platform.ACCESS_AUTH_DOMAIN, + platform.ACCESS_AUD ) success = response.ok } catch (e: unknown) { diff --git a/frontend/src/routes/(admin)/settings/server-settings/branding/index.tsx b/frontend/src/routes/(admin)/settings/(admin)/server-settings/branding/index.tsx similarity index 95% rename from frontend/src/routes/(admin)/settings/server-settings/branding/index.tsx rename to frontend/src/routes/(admin)/settings/(admin)/server-settings/branding/index.tsx index 59c6059..a6e8961 100644 --- a/frontend/src/routes/(admin)/settings/server-settings/branding/index.tsx +++ b/frontend/src/routes/(admin)/settings/(admin)/server-settings/branding/index.tsx @@ -18,7 +18,9 @@ export const action = action$(async (data, { request, platform }) => { try { const response = await handleRequestPost( await getDatabase(platform), - new Request(request, { body: JSON.stringify(data) }) + new Request(request, { body: JSON.stringify(data) }), + platform.ACCESS_AUTH_DOMAIN, + platform.ACCESS_AUD ) success = response.ok } catch (e: unknown) { diff --git a/frontend/src/routes/(admin)/settings/server-settings/index.tsx b/frontend/src/routes/(admin)/settings/(admin)/server-settings/index.tsx similarity index 100% rename from frontend/src/routes/(admin)/settings/server-settings/index.tsx rename to frontend/src/routes/(admin)/settings/(admin)/server-settings/index.tsx diff --git a/frontend/src/routes/(admin)/settings/server-settings/layout.tsx b/frontend/src/routes/(admin)/settings/(admin)/server-settings/layout.tsx similarity index 100% rename from frontend/src/routes/(admin)/settings/server-settings/layout.tsx rename to frontend/src/routes/(admin)/settings/(admin)/server-settings/layout.tsx diff --git a/frontend/src/routes/(admin)/settings/server-settings/rules/edit/[id]/index.tsx b/frontend/src/routes/(admin)/settings/(admin)/server-settings/rules/edit/[id]/index.tsx similarity index 100% rename from frontend/src/routes/(admin)/settings/server-settings/rules/edit/[id]/index.tsx rename to frontend/src/routes/(admin)/settings/(admin)/server-settings/rules/edit/[id]/index.tsx diff --git a/frontend/src/routes/(admin)/settings/server-settings/rules/index.tsx b/frontend/src/routes/(admin)/settings/(admin)/server-settings/rules/index.tsx similarity index 100% rename from frontend/src/routes/(admin)/settings/server-settings/rules/index.tsx rename to frontend/src/routes/(admin)/settings/(admin)/server-settings/rules/index.tsx diff --git a/frontend/src/routes/(admin)/settings/aliases/index.tsx b/frontend/src/routes/(admin)/settings/(auth)/aliases/index.tsx similarity index 92% rename from frontend/src/routes/(admin)/settings/aliases/index.tsx rename to frontend/src/routes/(admin)/settings/(auth)/aliases/index.tsx index cb0863c..6d24fea 100644 --- a/frontend/src/routes/(admin)/settings/aliases/index.tsx +++ b/frontend/src/routes/(admin)/settings/(auth)/aliases/index.tsx @@ -1,14 +1,4 @@ import { component$, useStore, useSignal, $ } from '@builder.io/qwik' -import { loader$ } from '@builder.io/qwik-city' -import { checkAuth } from '~/utils/checkAuth' - -export const loader = loader$(async ({ request, platform, redirect }) => { - const isAuthorized = await checkAuth(request, platform) - - if (!isAuthorized) { - redirect(303, '/explore') - } -}) export default component$(() => { const ref = useSignal() diff --git a/frontend/src/routes/(admin)/settings/(auth)/layout.tsx b/frontend/src/routes/(admin)/settings/(auth)/layout.tsx new file mode 100644 index 0000000..9e4da41 --- /dev/null +++ b/frontend/src/routes/(admin)/settings/(auth)/layout.tsx @@ -0,0 +1,11 @@ +import { component$, Slot } from '@builder.io/qwik' + +export { authLoader } from '~/utils/authLoader' + +export default component$(() => { + return ( + <> + + + ) +}) diff --git a/frontend/src/routes/(frontend)/about/index.tsx b/frontend/src/routes/(frontend)/about/index.tsx index fe9164d..ea43c82 100644 --- a/frontend/src/routes/(frontend)/about/index.tsx +++ b/frontend/src/routes/(frontend)/about/index.tsx @@ -6,14 +6,14 @@ import { handleRequestGet as settingsHandleRequestGet } from 'wildebeest/functio import { handleRequestGet as rulesHandleRequestGet } from 'wildebeest/functions/api/v1/instance/rules' import { Accordion } from '~/components/Accordion/Accordion' import { HtmlContent } from '~/components/HtmlContent/HtmlContent' -import { ServerSettingsData } from '~/routes/(admin)/settings/server-settings/layout' +import { ServerSettingsData } from '~/routes/(admin)/settings/(admin)/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 { getAdmins } from 'wildebeest/backend/src/utils/auth/getAdmins' type AboutInfo = { image: string @@ -84,9 +84,13 @@ export default component$(() => { @@ -142,7 +146,7 @@ export const head: DocumentHead = ({ resolveValue, head }) => { return getDocumentHead( { title: `About - ${instance.title}`, - description: `About page for the ${instance.title} Mastodon instance`, + description: `About page for ${instance.title}`, og: { type: 'website', image: instance.thumbnail, diff --git a/frontend/src/routes/layout.tsx b/frontend/src/routes/layout.tsx index e4027ac..81b8e56 100644 --- a/frontend/src/routes/layout.tsx +++ b/frontend/src/routes/layout.tsx @@ -1,23 +1,28 @@ import { component$, Slot } from '@builder.io/qwik' import { loader$ } from '@builder.io/qwik-city' -import * as access from 'wildebeest/backend/src/access' -import { checkAuth } from '~/utils/checkAuth' +import { isUserAuthenticated } from 'wildebeest/backend/src/utils/auth/isUserAuthenticated' -type AccessLoaderData = { - loginUrl: string +type AuthLoaderData = { + loginUrl: URL isAuthorized: boolean } -export const accessLoader = loader$>(async ({ platform, request }) => { - const isAuthorized = await checkAuth(request, platform) +export const authLoader = loader$>(async ({ platform, request, cookie }) => { + const jwt = cookie.get('CF_Authorization')?.value ?? '' + const isAuthorized = await isUserAuthenticated(request, jwt, platform.ACCESS_AUTH_DOMAIN, platform.ACCESS_AUD) + // FIXME(sven): remove hardcoded value + const UI_CLIENT_ID = '924801be-d211-495d-8cac-e73503413af8' + const params = new URLSearchParams({ + redirect_uri: request.url, + response_type: 'code', + client_id: UI_CLIENT_ID, + scope: 'all', + }) + const loginUrl = new URL('/oauth/authorize?' + params, 'https://' + platform.DOMAIN) return { isAuthorized, - loginUrl: access.generateLoginURL({ - redirectURL: request.url, - domain: platform.ACCESS_AUTH_DOMAIN, - aud: platform.ACCESS_AUD, - }), + loginUrl, } }) diff --git a/frontend/src/utils/adminLoader.ts b/frontend/src/utils/adminLoader.ts new file mode 100644 index 0000000..4ed3606 --- /dev/null +++ b/frontend/src/utils/adminLoader.ts @@ -0,0 +1,16 @@ +import { loader$ } from '@builder.io/qwik-city' +import { parse } from 'cookie' +import { getDatabase } from 'wildebeest/backend/src/database' +import { isUserAdmin } from 'wildebeest/backend/src/utils/auth/isUserAdmin' +import { getErrorHtml } from './getErrorHtml/getErrorHtml' + +export const adminLoader = loader$(async ({ request, platform, html }) => { + const database = await getDatabase(platform) + const cookie = parse(request.headers.get('Cookie') || '') + const jwtCookie = cookie.CF_Authorization ?? '' + const isAdmin = await isUserAdmin(request, jwtCookie, platform.ACCESS_AUTH_DOMAIN, platform.ACCESS_AUD, database) + + if (!isAdmin) { + return html(401, getErrorHtml('You need to be an admin to view this page')) + } +}) diff --git a/frontend/src/utils/authLoader.ts b/frontend/src/utils/authLoader.ts new file mode 100644 index 0000000..92d4374 --- /dev/null +++ b/frontend/src/utils/authLoader.ts @@ -0,0 +1,19 @@ +import { loader$ } from '@builder.io/qwik-city' +import { parse } from 'cookie' +import { isUserAuthenticated } from 'wildebeest/backend/src/utils/auth/isUserAuthenticated' +import { getErrorHtml } from './getErrorHtml/getErrorHtml' + +export const authLoader = loader$(async ({ request, platform, html }) => { + const cookie = parse(request.headers.get('Cookie') || '') + const jwtCookie = cookie.CF_Authorization ?? '' + const isAuthenticated = await isUserAuthenticated( + request, + jwtCookie, + platform.ACCESS_AUTH_DOMAIN, + platform.ACCESS_AUD + ) + + if (!isAuthenticated) { + return html(401, getErrorHtml("You're not authorized to view this page")) + } +}) diff --git a/frontend/src/utils/checkAuth.ts b/frontend/src/utils/checkAuth.ts deleted file mode 100644 index 23fbe14..0000000 --- a/frontend/src/utils/checkAuth.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { RequestContext } from '@builder.io/qwik-city/middleware/request-handler' -import * as access from 'wildebeest/backend/src/access' - -type Env = { - ACCESS_AUTH_DOMAIN: string - ACCESS_AUD: string -} - -export const checkAuth = async (request: RequestContext, platform: Env) => { - const jwt = request.headers.get('Cf-Access-Jwt-Assertion') || '' - if (!jwt) return false - - try { - const validate = access.generateValidator({ - jwt, - domain: platform.ACCESS_AUTH_DOMAIN, - aud: platform.ACCESS_AUD, - }) - await validate(new Request(request.url)) - } catch { - return false - } - - const identity = await access.getIdentity({ jwt, domain: platform.ACCESS_AUTH_DOMAIN }) - if (identity) { - return true - } - return false -} diff --git a/frontend/src/utils/isUserAdmin.ts b/frontend/src/utils/isUserAdmin.ts deleted file mode 100644 index a247d96..0000000 --- a/frontend/src/utils/isUserAdmin.ts +++ /dev/null @@ -1,17 +0,0 @@ -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 { - let email: string - try { - email = getJwtEmail(jwtCookie) - } catch { - return false - } - - const admins = await getAdmins(database) - - return admins.some((admin) => admin[emailSymbol] === email) -} diff --git a/functions/api/v1/apps.ts b/functions/api/v1/apps.ts index 88393bc..90d8b14 100644 --- a/functions/api/v1/apps.ts +++ b/functions/api/v1/apps.ts @@ -11,7 +11,7 @@ import { type Database, getDatabase } from 'wildebeest/backend/src/database' type AppsPost = { redirect_uris: string - website: string + website?: string client_name: string scopes: string } @@ -42,9 +42,18 @@ export async function handleRequest(db: Database, request: Request, vapidKeys: J } catch { return errors.unprocessableEntity('redirect_uris must be a valid URI') } + } else if (body.website) { + if (body.website.length > 2000) { + return errors.unprocessableEntity('website cannot exceed 2000 characters') + } + try { + new URL('', body.website) + } catch { + return errors.unprocessableEntity('website is invalid URI') + } } - const client = await createClient(db, body.client_name, body.redirect_uris, body.website, body.scopes) + const client = await createClient(db, body.client_name, body.redirect_uris, body.scopes, body.website) const vapidKey = VAPIDPublicKey(vapidKeys) const res = { diff --git a/functions/api/wb/settings/server/admins.ts b/functions/api/wb/settings/server/admins.ts index 51a7dec..e69de29 100644 --- a/functions/api/wb/settings/server/admins.ts +++ b/functions/api/wb/settings/server/admins.ts @@ -1,26 +0,0 @@ -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 = 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 { - let rows: unknown[] = [] - try { - const stmt = db.prepare('SELECT * FROM actors WHERE is_admin=TRUE') - const result = await stmt.all() - rows = result.success ? (result.results as unknown[]) : [] - } catch { - /* empty */ - } - - return rows.map(personFromRow) -} diff --git a/functions/api/wb/settings/server/rules.ts b/functions/api/wb/settings/server/rules.ts index 1a66b30..99fc679 100644 --- a/functions/api/wb/settings/server/rules.ts +++ b/functions/api/wb/settings/server/rules.ts @@ -3,10 +3,10 @@ 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 { isUserAdmin } from 'wildebeest/backend/src/utils/auth/isUserAdmin' export const onRequestGet: PagesFunction = async ({ env, request }) => { - return handleRequestPost(await getDatabase(env), request) + return handleRequestPost(await getDatabase(env), request, env.ACCESS_AUTH_DOMAIN, env.ACCESS_AUD) } export async function handleRequestGet(db: Database) { @@ -21,13 +21,13 @@ export async function handleRequestGet(db: Database) { } export const onRequestPost: PagesFunction = async ({ env, request }) => { - return handleRequestPost(await getDatabase(env), request) + return handleRequestPost(await getDatabase(env), request, env.ACCESS_AUTH_DOMAIN, env.ACCESS_AUD) } -export async function handleRequestPost(db: Database, request: Request) { +export async function handleRequestPost(db: Database, request: Request, accessAuthDomain: string, accessAud: string) { const cookie = parse(request.headers.get('Cookie') || '') const jwt = cookie['CF_Authorization'] - const isAdmin = await isUserAdmin(jwt, db) + const isAdmin = await isUserAdmin(request, jwt, accessAuthDomain, accessAud, db) if (!isAdmin) { return errors.notAuthorized('Lacking authorization rights to edit server rules') @@ -56,10 +56,10 @@ export async function upsertRule(db: Database, rule: { id?: number; text: string .run() } -export async function handleRequestDelete(db: Database, request: Request) { +export async function handleRequestDelete(db: Database, request: Request, accessAuthDomain: string, accessAud: string) { const cookie = parse(request.headers.get('Cookie') || '') const jwt = cookie['CF_Authorization'] - const isAdmin = await isUserAdmin(jwt, db) + const isAdmin = await isUserAdmin(request, jwt, accessAuthDomain, accessAud, db) if (!isAdmin) { return errors.notAuthorized('Lacking authorization rights to edit server rules') diff --git a/functions/api/wb/settings/server/server.ts b/functions/api/wb/settings/server/server.ts index b5162c9..3b672c5 100644 --- a/functions/api/wb/settings/server/server.ts +++ b/functions/api/wb/settings/server/server.ts @@ -3,11 +3,11 @@ 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' +import { ServerSettingsData } from 'wildebeest/frontend/src/routes/(admin)/settings/(admin)/server-settings/layout' +import { isUserAdmin } from 'wildebeest/backend/src/utils/auth/isUserAdmin' export const onRequestGet: PagesFunction = async ({ env, request }) => { - return handleRequestPost(await getDatabase(env), request) + return handleRequestPost(await getDatabase(env), request, env.ACCESS_AUTH_DOMAIN, env.ACCESS_AUD) } export async function handleRequestGet(db: Database) { @@ -31,13 +31,13 @@ export async function handleRequestGet(db: Database) { } export const onRequestPost: PagesFunction = async ({ env, request }) => { - return handleRequestPost(await getDatabase(env), request) + return handleRequestPost(await getDatabase(env), request, env.ACCESS_AUTH_DOMAIN, env.ACCESS_AUD) } -export async function handleRequestPost(db: Database, request: Request) { +export async function handleRequestPost(db: Database, request: Request, accessAuthDomain: string, accessAud: string) { const cookie = parse(request.headers.get('Cookie') || '') const jwt = cookie['CF_Authorization'] - const isAdmin = await isUserAdmin(jwt, db) + const isAdmin = await isUserAdmin(request, jwt, accessAuthDomain, accessAud, db) if (!isAdmin) { return errors.notAuthorized('Lacking authorization rights to edit server settings') diff --git a/functions/first-login.ts b/functions/first-login.ts index 9a69ade..abccf82 100644 --- a/functions/first-login.ts +++ b/functions/first-login.ts @@ -7,7 +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' +import { getJwtEmail } from 'wildebeest/backend/src/utils/auth/getJwtEmail' export const onRequestPost: PagesFunction = async ({ request, env }) => { return handlePostRequest(request, await getDatabase(env), env.userKEK, env.ACCESS_AUTH_DOMAIN, env.ACCESS_AUD) diff --git a/functions/oauth/authorize.ts b/functions/oauth/authorize.ts index e25696b..04eaed4 100644 --- a/functions/oauth/authorize.ts +++ b/functions/oauth/authorize.ts @@ -8,6 +8,7 @@ import { getClientById } from 'wildebeest/backend/src/mastodon/client' import * as access from 'wildebeest/backend/src/access' import { getPersonByEmail } from 'wildebeest/backend/src/activitypub/actors' import { type Database, getDatabase } from 'wildebeest/backend/src/database' +import { isUserAuthenticated } from 'wildebeest/backend/src/utils/auth/isUserAuthenticated' // Extract the JWT token sent by Access (running before us). const extractJWTFromRequest = (request: Request) => request.headers.get('Cf-Access-Jwt-Assertion') || '' @@ -80,18 +81,14 @@ export async function handleRequestPost( } const jwt = extractJWTFromRequest(request) - if (!jwt) { + const isAuthenticated = await isUserAuthenticated(request, jwt, accessDomain, accessAud) + + if (!isAuthenticated) { return new Response('', { status: 401 }) } - const validate = access.generateValidator({ jwt, domain: accessDomain, aud: accessAud }) - await validate(request) const identity = await access.getIdentity({ jwt, domain: accessDomain }) - if (!identity) { - return new Response('', { status: 401 }) - } - - const isFirstLogin = (await getPersonByEmail(db, identity.email)) === null + const isFirstLogin = (await getPersonByEmail(db, identity!.email)) === null return buildRedirect(db, request, isFirstLogin, jwt) }

- Decentralised social media powered by{' '} - - Mastodon + Decentralized social network powered by{' '} + + Wildebeest