kopia lustrzana https://github.com/cloudflare/wildebeest
refactor UI auth
rodzic
32cc669141
commit
95e5beb70e
|
@ -8,6 +8,7 @@ import { Buffer } from 'buffer'
|
||||||
const PERSON = 'Person'
|
const PERSON = 'Person'
|
||||||
const isTesting = typeof jest !== 'undefined'
|
const isTesting = typeof jest !== 'undefined'
|
||||||
export const emailSymbol = Symbol()
|
export const emailSymbol = Symbol()
|
||||||
|
export const isAdminSymbol = Symbol()
|
||||||
|
|
||||||
export function actorURL(domain: string, id: string): URL {
|
export function actorURL(domain: string, id: string): URL {
|
||||||
return new URL(`/ap/users/${id}`, 'https://' + domain)
|
return new URL(`/ap/users/${id}`, 'https://' + domain)
|
||||||
|
@ -23,6 +24,7 @@ export interface Actor extends APObject {
|
||||||
alsoKnownAs?: string
|
alsoKnownAs?: string
|
||||||
|
|
||||||
[emailSymbol]: string
|
[emailSymbol]: string
|
||||||
|
[isAdminSymbol]: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
|
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
|
||||||
|
@ -298,6 +300,7 @@ export function personFromRow(row: any): Person {
|
||||||
return {
|
return {
|
||||||
// Hidden values
|
// Hidden values
|
||||||
[emailSymbol]: row.email,
|
[emailSymbol]: row.email,
|
||||||
|
[isAdminSymbol]: row.is_admin === 1,
|
||||||
|
|
||||||
...properties,
|
...properties,
|
||||||
name,
|
name,
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
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<boolean> {
|
|
||||||
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)
|
|
||||||
}
|
|
|
@ -4,13 +4,58 @@
|
||||||
* It's the entry point for cloudflare-pages when building for production.
|
* It's the entry point for cloudflare-pages when building for production.
|
||||||
*
|
*
|
||||||
* Learn more about the cloudflare integration here:
|
* Learn more about the cloudflare integration here:
|
||||||
* - https://qwik.builder.io/qwikcity/adaptors/cloudflare-pages/
|
* - https://qwik.builder.io/integrations/deployments/cloudflare-pages/#cloudflare-pages-entry-middleware
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
import { createQwikCity } from '@builder.io/qwik-city/middleware/cloudflare-pages'
|
import { createQwikCity } from '@builder.io/qwik-city/middleware/cloudflare-pages'
|
||||||
import qwikCityPlan from '@qwik-city-plan'
|
import qwikCityPlan from '@qwik-city-plan'
|
||||||
import render from './entry.ssr'
|
import render from './entry.ssr'
|
||||||
|
import type { Env } from 'wildebeest/backend/src/types/env'
|
||||||
|
import type { ContextData } from 'wildebeest/backend/src/types/context'
|
||||||
|
import { parse } from 'cookie'
|
||||||
|
import * as access from 'wildebeest/backend/src/access'
|
||||||
|
import { getJwtEmail } from 'wildebeest/backend/src/utils/auth/getJwtEmail'
|
||||||
|
import * as errors from 'wildebeest/backend/src/errors'
|
||||||
|
import * as actors from 'wildebeest/backend/src/activitypub/actors'
|
||||||
|
import { getDatabase } from 'wildebeest/backend/src/database'
|
||||||
|
import type { Person } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
|
|
||||||
const onRequest = createQwikCity({ render, qwikCityPlan })
|
const qwikHandler = createQwikCity({ render, qwikCityPlan })
|
||||||
|
|
||||||
export { onRequest }
|
type QwikContextData = {
|
||||||
|
connectedActor: Person | null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
export const onRequest: PagesFunction<Env, any, ContextData> = async (ctx) => {
|
||||||
|
const cookie = parse(ctx.request.headers.get('Cookie') || '')
|
||||||
|
const jwt = cookie['CF_Authorization']
|
||||||
|
|
||||||
|
const data: QwikContextData = {
|
||||||
|
connectedActor: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jwt) {
|
||||||
|
const validate = access.generateValidator({
|
||||||
|
jwt,
|
||||||
|
domain: ctx.env.ACCESS_AUTH_DOMAIN,
|
||||||
|
aud: ctx.env.ACCESS_AUD,
|
||||||
|
})
|
||||||
|
await validate(ctx.request)
|
||||||
|
|
||||||
|
let email = ''
|
||||||
|
try {
|
||||||
|
email = getJwtEmail(jwt ?? '')
|
||||||
|
} catch (e) {
|
||||||
|
return errors.notAuthorized((e as Error)?.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = await getDatabase(ctx.env)
|
||||||
|
data.connectedActor = await actors.getPersonByEmail(db, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
;(ctx.env as any).data = data
|
||||||
|
|
||||||
|
return qwikHandler(ctx)
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import { component$, Slot } from '@builder.io/qwik'
|
import { component$, Slot } from '@builder.io/qwik'
|
||||||
import { loader$ } from '@builder.io/qwik-city'
|
import { loader$ } from '@builder.io/qwik-city'
|
||||||
import { isUserAuthenticated } from 'wildebeest/backend/src/utils/auth/isUserAuthenticated'
|
|
||||||
|
|
||||||
type AuthLoaderData = {
|
type AuthLoaderData = {
|
||||||
loginUrl: URL
|
loginUrl: URL
|
||||||
isAuthorized: boolean
|
isAuthorized: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authLoader = loader$<Promise<AuthLoaderData>>(async ({ platform, request, cookie }) => {
|
export const authLoader = loader$<Promise<AuthLoaderData>>(async ({ platform, request }) => {
|
||||||
const jwt = cookie.get('CF_Authorization')?.value ?? ''
|
const isAuthorized = platform.data.connectedActor !== null
|
||||||
const isAuthorized = await isUserAuthenticated(request, jwt, platform.ACCESS_AUTH_DOMAIN, platform.ACCESS_AUD)
|
|
||||||
// FIXME(sven): remove hardcoded value
|
// FIXME(sven): remove hardcoded value
|
||||||
const UI_CLIENT_ID = '924801be-d211-495d-8cac-e73503413af8'
|
const UI_CLIENT_ID = '924801be-d211-495d-8cac-e73503413af8'
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
import { loader$ } from '@builder.io/qwik-city'
|
import { loader$ } from '@builder.io/qwik-city'
|
||||||
import { parse } from 'cookie'
|
import { isAdminSymbol } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
import { getDatabase } from 'wildebeest/backend/src/database'
|
|
||||||
import { isUserAdmin } from 'wildebeest/backend/src/utils/auth/isUserAdmin'
|
|
||||||
import { getErrorHtml } from './getErrorHtml/getErrorHtml'
|
import { getErrorHtml } from './getErrorHtml/getErrorHtml'
|
||||||
|
|
||||||
export const adminLoader = loader$(async ({ request, platform, html }) => {
|
export const adminLoader = loader$(async ({ platform, html }) => {
|
||||||
const database = await getDatabase(platform)
|
const isAuthorized = platform.data.connectedActor !== null
|
||||||
const cookie = parse(request.headers.get('Cookie') || '')
|
const isAdmin = isAuthorized && platform.data.connectedActor[isAdminSymbol]
|
||||||
const jwtCookie = cookie.CF_Authorization ?? ''
|
|
||||||
const isAdmin = await isUserAdmin(request, jwtCookie, platform.ACCESS_AUTH_DOMAIN, platform.ACCESS_AUD, database)
|
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
return html(401, getErrorHtml('You need to be an admin to view this page'))
|
return html(401, getErrorHtml('You need to be an admin to view this page'))
|
||||||
|
|
|
@ -1,17 +1,8 @@
|
||||||
import { loader$ } from '@builder.io/qwik-city'
|
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'
|
import { getErrorHtml } from './getErrorHtml/getErrorHtml'
|
||||||
|
|
||||||
export const authLoader = loader$(async ({ request, platform, html }) => {
|
export const authLoader = loader$(async ({ platform, html }) => {
|
||||||
const cookie = parse(request.headers.get('Cookie') || '')
|
const isAuthenticated = platform.data.connectedActor !== null
|
||||||
const jwtCookie = cookie.CF_Authorization ?? ''
|
|
||||||
const isAuthenticated = await isUserAuthenticated(
|
|
||||||
request,
|
|
||||||
jwtCookie,
|
|
||||||
platform.ACCESS_AUTH_DOMAIN,
|
|
||||||
platform.ACCESS_AUD
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return html(401, getErrorHtml("You're not authorized to view this page"))
|
return html(401, getErrorHtml("You're not authorized to view this page"))
|
||||||
|
|
Ładowanie…
Reference in New Issue