kopia lustrzana https://github.com/cloudflare/wildebeest
Merge remote-tracking branch 'upstream/main' into api/v1/instance
commit
1685c6a1f2
|
@ -0,0 +1,20 @@
|
||||||
|
import { setActorAlias } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
|
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
|
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
|
||||||
|
import { queryAcct } from 'wildebeest/backend/src/webfinger'
|
||||||
|
import { type Database } from 'wildebeest/backend/src/database'
|
||||||
|
|
||||||
|
export async function addAlias(db: Database, alias: string, connectedActor: Actor) {
|
||||||
|
const handle = parseHandle(alias)
|
||||||
|
const acct = `${handle.localPart}@${handle.domain}`
|
||||||
|
if (handle.domain === null) {
|
||||||
|
throw new Error("account migration within an instance isn't supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
const actor = await queryAcct(handle.domain, db, acct)
|
||||||
|
if (actor === null) {
|
||||||
|
throw new Error('actor not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
await setActorAlias(db, connectedActor.id, actor.id)
|
||||||
|
}
|
|
@ -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
|
||||||
|
@ -223,7 +225,7 @@ export async function updateActorProperty(db: Database, actorId: URL, key: strin
|
||||||
|
|
||||||
export async function setActorAlias(db: Database, actorId: URL, alias: URL) {
|
export async function setActorAlias(db: Database, actorId: URL, alias: URL) {
|
||||||
const { success, error } = await db
|
const { success, error } = await db
|
||||||
.prepare(`UPDATE actors SET properties=json_set(properties, '$.alsoKnownAs', json_array(?)) WHERE id=?`)
|
.prepare(`UPDATE actors SET properties=json_set(properties, '$.alsoKnownAs', ${db.qb.jsonArray('?1')}) WHERE id=?2`)
|
||||||
.bind(alias.toString(), actorId.toString())
|
.bind(alias.toString(), actorId.toString())
|
||||||
.run()
|
.run()
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
@ -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,
|
||||||
|
|
|
@ -25,6 +25,10 @@ const qb: QueryBuilder = {
|
||||||
psqlOnly(): string {
|
psqlOnly(): string {
|
||||||
return ''
|
return ''
|
||||||
},
|
},
|
||||||
|
|
||||||
|
jsonArray(r: string): string {
|
||||||
|
return `json_array(${r})`
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function make({ DATABASE }: Pick<Env, 'DATABASE'>): Database {
|
export default function make({ DATABASE }: Pick<Env, 'DATABASE'>): Database {
|
||||||
|
|
|
@ -33,6 +33,7 @@ export interface QueryBuilder {
|
||||||
epoch(): string
|
epoch(): string
|
||||||
insertOrIgnore(q: string): string
|
insertOrIgnore(q: string): string
|
||||||
psqlOnly(raw: string): string
|
psqlOnly(raw: string): string
|
||||||
|
jsonArray(r: string): string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDatabase(env: Pick<Env, 'DATABASE' | 'NEON_DATABASE_URL'>): Promise<Database> {
|
export async function getDatabase(env: Pick<Env, 'DATABASE' | 'NEON_DATABASE_URL'>): Promise<Database> {
|
||||||
|
|
|
@ -34,6 +34,10 @@ const qb: QueryBuilder = {
|
||||||
psqlOnly(q: string): string {
|
psqlOnly(q: string): string {
|
||||||
return q
|
return q
|
||||||
},
|
},
|
||||||
|
|
||||||
|
jsonArray(r: string): string {
|
||||||
|
return `json_array_elements_text(${r})`
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function make(env: Pick<Env, 'NEON_DATABASE_URL'>): Promise<Database> {
|
export default async function make(env: Pick<Env, 'NEON_DATABASE_URL'>): Promise<Database> {
|
||||||
|
|
|
@ -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 'wildebeest/backend/src/utils/auth/getAdmins'
|
|
||||||
import { isUserAuthenticated } from 'wildebeest/backend/src/utils/auth/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)
|
|
||||||
}
|
|
|
@ -85,7 +85,7 @@ describe('Mastodon APIs', () => {
|
||||||
headers,
|
headers,
|
||||||
})
|
})
|
||||||
const res = await oauth_authorize.handleRequestPost(req, db, userKEK, accessDomain, accessAud)
|
const res = await oauth_authorize.handleRequestPost(req, db, userKEK, accessDomain, accessAud)
|
||||||
assert.equal(res.status, 403)
|
assert.equal(res.status, 422)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('authorize redirects with code on success and show first login', async () => {
|
test('authorize redirects with code on success and show first login', async () => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { makeDB } from '../utils'
|
import { makeDB } from '../utils'
|
||||||
import { strict as assert } from 'node:assert/strict'
|
import { strict as assert } from 'node:assert/strict'
|
||||||
import { createPerson, getActorById } from 'wildebeest/backend/src/activitypub/actors'
|
import { createPerson, getActorById } from 'wildebeest/backend/src/activitypub/actors'
|
||||||
import * as account_alias from 'wildebeest/functions/api/wb/settings/account/alias'
|
import * as alias from 'wildebeest/backend/src/accounts/alias'
|
||||||
|
|
||||||
const domain = 'cloudflare.com'
|
const domain = 'cloudflare.com'
|
||||||
const userKEK = 'test_kek22'
|
const userKEK = 'test_kek22'
|
||||||
|
@ -39,14 +39,7 @@ describe('Wildebeest', () => {
|
||||||
throw new Error('unexpected request to ' + input)
|
throw new Error('unexpected request to ' + input)
|
||||||
}
|
}
|
||||||
|
|
||||||
const alias = 'test@example.com'
|
await alias.addAlias(db, 'test@example.com', actor)
|
||||||
|
|
||||||
const req = new Request('https://example.com', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ alias }),
|
|
||||||
})
|
|
||||||
const res = await account_alias.handleRequestPost(db, req, actor)
|
|
||||||
assert.equal(res.status, 201)
|
|
||||||
|
|
||||||
// Ensure the actor has the alias set
|
// Ensure the actor has the alias set
|
||||||
const newActor = await getActorById(db, actor.id)
|
const newActor = await getActorById(db, actor.id)
|
||||||
|
|
|
@ -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,7 +1,33 @@
|
||||||
import { component$, useStore, useSignal, $ } from '@builder.io/qwik'
|
import { component$, useStore, useSignal, $ } from '@builder.io/qwik'
|
||||||
|
import { getDatabase } from 'wildebeest/backend/src/database'
|
||||||
|
import { action$, Form, zod$, z } from '@builder.io/qwik-city'
|
||||||
|
import { addAlias } from 'wildebeest/backend/src/accounts/alias'
|
||||||
|
|
||||||
|
const zodSchema = zod$({
|
||||||
|
alias: z.string().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const action = action$(async (data, { platform, json }) => {
|
||||||
|
const db = await getDatabase(platform)
|
||||||
|
const connectedActor = platform.data.connectedActor
|
||||||
|
if (connectedActor === null) {
|
||||||
|
throw json(500, { error: 'user not present in context' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addAlias(db, data.alias, connectedActor)
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const error = e as { stack: string; cause: string }
|
||||||
|
console.error(error.stack, error.cause)
|
||||||
|
throw json(500, { error: 'failed to add alias' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
}, zodSchema)
|
||||||
|
|
||||||
export default component$(() => {
|
export default component$(() => {
|
||||||
const ref = useSignal<Element>()
|
|
||||||
const state = useStore({ alias: '' })
|
const state = useStore({ alias: '' })
|
||||||
const toast = useSignal<'success' | 'failure' | null>(null)
|
const toast = useSignal<'success' | 'failure' | null>(null)
|
||||||
|
|
||||||
|
@ -9,17 +35,10 @@ export default component$(() => {
|
||||||
state.alias = (event.target as HTMLInputElement).value
|
state.alias = (event.target as HTMLInputElement).value
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSubmit = $(async () => {
|
const saveAction = action()
|
||||||
const res = await fetch('/api/wb/settings/account/alias', { method: 'POST', body: JSON.stringify(state) })
|
|
||||||
if (res.status == 200) {
|
|
||||||
toast.value = 'success'
|
|
||||||
} else {
|
|
||||||
toast.value = 'failure'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form ref={ref} class="login-form" preventdefault:submit onSubmit$={handleSubmit}>
|
<Form class="login-form" action={saveAction}>
|
||||||
<div class="max-w-4xl py-14 px-8">
|
<div class="max-w-4xl py-14 px-8">
|
||||||
<h2 class="text-2xl font-bold mb-6">Account Aliases</h2>
|
<h2 class="text-2xl font-bold mb-6">Account Aliases</h2>
|
||||||
|
|
||||||
|
@ -92,6 +111,6 @@ export default component$(() => {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table> */}
|
</table> */}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</Form>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const statusesLoader = loader$<Promise<MastodonStatus[]>>(async ({ platfo
|
||||||
return JSON.parse(results) as MastodonStatus[]
|
return JSON.parse(results) as MastodonStatus[]
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const error = e as { stack: string; cause: string }
|
const error = e as { stack: string; cause: string }
|
||||||
console.warn(error.stack, error.cause)
|
console.error(error.stack, error.cause)
|
||||||
throw html(500, getErrorHtml('The timeline is unavailable, please try again later'))
|
throw html(500, getErrorHtml('The timeline is unavailable, please try again later'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
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 }) => {
|
||||||
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)
|
// defined in migrations/0010_add_ui_client.sql
|
||||||
// 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({
|
||||||
redirect_uri: request.url,
|
redirect_uri: '/',
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
client_id: UI_CLIENT_ID,
|
client_id: UI_CLIENT_ID,
|
||||||
scope: 'all',
|
scope: 'all',
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
import type { Env } from 'wildebeest/backend/src/types/env'
|
|
||||||
import { setActorAlias } from 'wildebeest/backend/src/activitypub/actors'
|
|
||||||
import type { ContextData } from 'wildebeest/backend/src/types/context'
|
|
||||||
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
|
|
||||||
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
|
|
||||||
import { queryAcct } from 'wildebeest/backend/src/webfinger'
|
|
||||||
import * as errors from 'wildebeest/backend/src/errors'
|
|
||||||
import { type Database, getDatabase } from 'wildebeest/backend/src/database'
|
|
||||||
|
|
||||||
export const onRequestPost: PagesFunction<Env, any, ContextData> = async ({ env, request, data }) => {
|
|
||||||
return handleRequestPost(await getDatabase(env), request, data.connectedActor)
|
|
||||||
}
|
|
||||||
|
|
||||||
type AddAliasRequest = {
|
|
||||||
alias: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleRequestPost(db: Database, request: Request, connectedActor: Actor): Promise<Response> {
|
|
||||||
const body = await request.json<AddAliasRequest>()
|
|
||||||
|
|
||||||
const handle = parseHandle(body.alias)
|
|
||||||
const acct = `${handle.localPart}@${handle.domain}`
|
|
||||||
if (handle.domain === null) {
|
|
||||||
console.warn("account migration within an instance isn't supported")
|
|
||||||
return new Response('', { status: 400 })
|
|
||||||
}
|
|
||||||
const actor = await queryAcct(handle.domain, db, acct)
|
|
||||||
if (actor === null) {
|
|
||||||
return errors.resourceNotFound('actor', acct)
|
|
||||||
}
|
|
||||||
|
|
||||||
await setActorAlias(db, connectedActor.id, actor.id)
|
|
||||||
|
|
||||||
return new Response('', { status: 201 })
|
|
||||||
}
|
|
|
@ -50,7 +50,7 @@ export async function buildRedirect(
|
||||||
|
|
||||||
const redirect_uri = url.searchParams.get('redirect_uri')
|
const redirect_uri = url.searchParams.get('redirect_uri')
|
||||||
if (client.redirect_uris !== redirect_uri) {
|
if (client.redirect_uris !== redirect_uri) {
|
||||||
return new Response('', { status: 403 })
|
return errors.validationError('redirect_uri not allowed')
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = `${client.id}.${jwt}`
|
const code = `${client.id}.${jwt}`
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
-- Migration number: 0010 2023-03-08T09:40:30.734Z
|
||||||
|
|
||||||
|
INSERT INTO clients (id, secret, name, redirect_uris, scopes)
|
||||||
|
VALUES ('924801be-d211-495d-8cac-e73503413af8', hex(randomblob(42)), 'Wildebeest User Interface', '/', 'all');
|
Ładowanie…
Reference in New Issue