From 60e4be1679f3c0cb8f4f2d782a68c1bc0cfa897b Mon Sep 17 00:00:00 2001 From: nicosammito Date: Fri, 7 Aug 2026 11:14:33 +0200 Subject: [PATCH 1/5] feat: add Application query to fetch application data with identity provider pagination --- .../services/queries/Application.query.graphql | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/packages/cloud/src/application/services/queries/Application.query.graphql diff --git a/src/packages/cloud/src/application/services/queries/Application.query.graphql b/src/packages/cloud/src/application/services/queries/Application.query.graphql new file mode 100644 index 00000000..be59e713 --- /dev/null +++ b/src/packages/cloud/src/application/services/queries/Application.query.graphql @@ -0,0 +1,10 @@ +#import "@ce-internal/application/services/fragments/Application.fragment.graphql" + +query Application( + $firstIdentityProvider: Int, + $afterIdentityProvider: String +) { + application { + ...Application + } +} \ No newline at end of file From cb5faceeaeb3dc517134e7a8b57765a97d477cf1 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Fri, 7 Aug 2026 11:14:41 +0200 Subject: [PATCH 2/5] feat: implement middleware for handling authentication redirects and callback URLs --- src/middleware.ts | 65 +--------------------------- src/packages/ce/src/Middleware.ts | 6 +++ src/packages/cloud/src/Middleware.ts | 64 +++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 63 deletions(-) create mode 100644 src/packages/ce/src/Middleware.ts create mode 100644 src/packages/cloud/src/Middleware.ts diff --git a/src/middleware.ts b/src/middleware.ts index 089e9366..a70945fc 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,69 +1,8 @@ import type {NextRequest} from 'next/server' -import {NextResponse} from 'next/server' -import {isValidRedirect} from "@core/util/redirect"; +import {middleware as editionMiddleware} from "@edition/Middleware"; export function middleware(request: NextRequest) { - - const {searchParams, pathname} = request.nextUrl - const callbackUrl = searchParams.get('callbackUrl') - const selectNamespace = searchParams.get('selectNamespace') - const authPaths = ["/email", "/login", "/password", "/redirect", "/register"] - const isAuthPath = authPaths.some(path => pathname.startsWith(path)) - - if (callbackUrl && isValidRedirect(callbackUrl)) { - let response: NextResponse - - if (isAuthPath) { - response = NextResponse.next() - } else { - const redirectUrl = new URL('/redirect', request.url) - redirectUrl.searchParams.set('callbackUrl', callbackUrl) - if (selectNamespace) redirectUrl.searchParams.set('selectNamespace', selectNamespace) - response = NextResponse.redirect(redirectUrl) - } - - response.cookies.set('codezero_callback', callbackUrl, { - path: '/', - sameSite: 'lax', - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 5, - }) - - if (selectNamespace) { - response.cookies.set('codezero_selectNamespace', selectNamespace, { - path: '/', - sameSite: 'lax', - secure: process.env.NODE_ENV === 'production', - maxAge: 60 * 5, - }) - } - - return response - } - - const callbackCookie = request.cookies.get('codezero_callback')?.value - const selectNamespaceCookie = request.cookies.get('codezero_selectNamespace')?.value - - if (callbackCookie) { - if (!isAuthPath) { - const redirectUrl = new URL('/redirect', request.url) - redirectUrl.searchParams.set('callbackUrl', callbackCookie) - if (selectNamespaceCookie) redirectUrl.searchParams.set('selectNamespace', selectNamespaceCookie) - return NextResponse.redirect(redirectUrl) - } - - if (pathname.startsWith("/redirect")) { - const url = request.nextUrl.clone() - if (!searchParams.has('callbackUrl')) { - url.searchParams.set('callbackUrl', callbackCookie) - if (selectNamespaceCookie) url.searchParams.set('selectNamespace', selectNamespaceCookie) - return NextResponse.redirect(url) - } - } - } - - return NextResponse.next() - + return editionMiddleware(request) } export const config = { diff --git a/src/packages/ce/src/Middleware.ts b/src/packages/ce/src/Middleware.ts new file mode 100644 index 00000000..a6e244dd --- /dev/null +++ b/src/packages/ce/src/Middleware.ts @@ -0,0 +1,6 @@ +import type {NextRequest} from 'next/server' +import {NextResponse} from 'next/server' + +export function middleware(_request: NextRequest) { + return NextResponse.next() +} diff --git a/src/packages/cloud/src/Middleware.ts b/src/packages/cloud/src/Middleware.ts new file mode 100644 index 00000000..3fb7cae4 --- /dev/null +++ b/src/packages/cloud/src/Middleware.ts @@ -0,0 +1,64 @@ +import type {NextRequest} from 'next/server' +import {NextResponse} from 'next/server' +import {isValidRedirect} from "@core/util/redirect"; + +export function middleware(request: NextRequest) { + + const {searchParams, pathname} = request.nextUrl + const callbackUrl = searchParams.get('callbackUrl') + const selectNamespace = searchParams.get('selectNamespace') + const authPaths = ["/email", "/login", "/password", "/redirect", "/register", "/callback"] + const isAuthPath = authPaths.some(path => pathname.startsWith(path)) + const isRedirectPath = pathname.startsWith("/redirect") + + const cookieOptions = { + path: '/', + sameSite: 'lax' as const, + secure: request.nextUrl.protocol === 'https:', + maxAge: 60 * 5, + } + + if (callbackUrl && isValidRedirect(callbackUrl)) { + + if (isRedirectPath) return NextResponse.next() + if (!isAuthPath) { + // Any other page: send the user on to the consent screen. + const redirectUrl = new URL('/redirect', request.url) + redirectUrl.searchParams.set('callbackUrl', callbackUrl) + if (selectNamespace) redirectUrl.searchParams.set('selectNamespace', selectNamespace) + const response = NextResponse.redirect(redirectUrl) + response.cookies.set('codezero_callback', callbackUrl, cookieOptions) + if (selectNamespace) response.cookies.set('codezero_selectNamespace', selectNamespace, cookieOptions) + return response + } + + const redirectUrl = request.nextUrl.clone() + redirectUrl.searchParams.delete('callbackUrl') + redirectUrl.searchParams.delete('selectNamespace') + const response = NextResponse.redirect(redirectUrl) + response.cookies.set('codezero_callback', callbackUrl, cookieOptions) + if (selectNamespace) response.cookies.set('codezero_selectNamespace', selectNamespace, cookieOptions) + return response + } + + const callbackCookie = request.cookies.get('codezero_callback')?.value + const selectNamespaceCookie = request.cookies.get('codezero_selectNamespace')?.value + + if (callbackCookie) { + if (!isAuthPath) { + const redirectUrl = new URL('/redirect', request.url) + redirectUrl.searchParams.set('callbackUrl', callbackCookie) + if (selectNamespaceCookie) redirectUrl.searchParams.set('selectNamespace', selectNamespaceCookie) + return NextResponse.redirect(redirectUrl) + } + + if (isRedirectPath && !searchParams.has('callbackUrl')) { + const url = request.nextUrl.clone() + url.searchParams.set('callbackUrl', callbackCookie) + if (selectNamespaceCookie) url.searchParams.set('selectNamespace', selectNamespaceCookie) + return NextResponse.redirect(url) + } + } + + return NextResponse.next() +} From 7aafc932433f1c015353e22f523eb7e66c944ea8 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Fri, 7 Aug 2026 11:14:54 +0200 Subject: [PATCH 3/5] feat: add GraphQL mutation for creating Crater token with error handling --- .../User.createCraterToken.mutation.graphql | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/packages/ce/src/user/services/mutations/User.createCraterToken.mutation.graphql diff --git a/src/packages/ce/src/user/services/mutations/User.createCraterToken.mutation.graphql b/src/packages/ce/src/user/services/mutations/User.createCraterToken.mutation.graphql new file mode 100644 index 00000000..a95be3c4 --- /dev/null +++ b/src/packages/ce/src/user/services/mutations/User.createCraterToken.mutation.graphql @@ -0,0 +1,15 @@ +mutation createCraterToken { + usersCreateCraterToken(input: {}) { + errors { + ...on Error { + errorCode, + details { + __typename + } + } + } + token { + token + } + } +} From e83803059dbcbb72d09a74f0737cf2c94e90aa26 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Fri, 7 Aug 2026 11:15:08 +0200 Subject: [PATCH 4/5] feat: add GraphQL mutation for creating Crater token --- src/packages/ce/src/user/services/User.service.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/packages/ce/src/user/services/User.service.ts b/src/packages/ce/src/user/services/User.service.ts index 008a5209..a35b3904 100644 --- a/src/packages/ce/src/user/services/User.service.ts +++ b/src/packages/ce/src/user/services/User.service.ts @@ -3,6 +3,8 @@ import { Mutation, Query, User, + UsersCreateCraterTokenInput, + UsersCreateCraterTokenPayload, UsersCreateInput, UsersCreatePayload, UsersEmailVerificationInput, @@ -36,6 +38,7 @@ import { } from "@code0-tech/sagittarius-graphql-types"; import {GraphqlClient} from "@core/util/graphql-client"; import createMutation from "./mutations/User.create.mutation.graphql"; +import createCraterTokenMutation from "./mutations/User.createCraterToken.mutation.graphql"; import updateMutation from "./mutations/User.update.mutation.graphql"; import loginMutation from "./mutations/User.login.mutation.graphql"; import logoutMutation from "./mutations/User.logout.mutation.graphql"; @@ -185,6 +188,15 @@ export class UserService extends ReactiveArrayService { return result.data?.usersCreate ?? undefined } + async usersCreateCraterToken(): Promise { + const result = await this.client.mutate({ + mutation: createCraterTokenMutation, + variables: {} + }) + + return result.data?.usersCreateCraterToken ?? undefined + } + async usersUpdate(payload: UsersUpdateInput): Promise { const result = await this.client.mutate({ mutation: updateMutation, From a1f3b3fd802579a3ecc69fce62b521e8e04a3226 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Fri, 7 Aug 2026 11:15:14 +0200 Subject: [PATCH 5/5] feat: refactor UserRedirectPage to improve authorization flow and error handling --- .../ce/src/user/pages/UserRedirectPage.tsx | 165 +---------------- .../cloud/src/user/pages/UserRedirectPage.tsx | 172 ++++++++++++++++++ 2 files changed, 175 insertions(+), 162 deletions(-) create mode 100644 src/packages/cloud/src/user/pages/UserRedirectPage.tsx diff --git a/src/packages/ce/src/user/pages/UserRedirectPage.tsx b/src/packages/ce/src/user/pages/UserRedirectPage.tsx index a9e2f2d1..5441bd22 100644 --- a/src/packages/ce/src/user/pages/UserRedirectPage.tsx +++ b/src/packages/ce/src/user/pages/UserRedirectPage.tsx @@ -1,167 +1,8 @@ "use client" import React from "react"; -import { - Alert, - Button, - Card, - Flex, - InputDescription, - InputLabel, - SelectContent, - SelectInput, - SelectItem, - SelectItemText, - SelectPortal, - SelectTrigger, - SelectValue, - SelectViewport, - Spacing, - Text, - TextInput, - useService, - useStore -} from "@code0-tech/pictor"; -import {useUserSession} from "@edition/user/hooks/User.session.hook"; -import {useRouter, useSearchParams} from "next/navigation"; -import {IconApiApp, IconCheck, IconChevronDown} from "@tabler/icons-react"; -import {isValidRedirect} from "@core/util/redirect"; -import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; -import {OrganizationView} from "@edition/organization/services/Organization.view"; +import {notFound} from "next/navigation"; export const UserRedirectPage: React.FC = () => { - - const userSession = useUserSession() - const params = useSearchParams() - const callbackUrl = params.get("callbackUrl") - const selectNamespace = params.get("selectNamespace") - const router = useRouter() - const isValidCallback = isValidRedirect(callbackUrl) - const organizationService = useService(OrganizationService) - const organizationStore = useStore(OrganizationService) - const [namespaceId, setNamespaceId] = React.useState(null); - - const namespaces: ({ - title?: string - id?: string - })[] = React.useMemo( - () => [{ - title: `Personal organization of user @${userSession?.user?.username}`, - id: userSession?.user?.namespace?.id! - }, ...organizationService.values().filter((organization: OrganizationView) => organization.userAbilities).map((organization: OrganizationView) => ({ - id: organization.namespace?.id!, - title: `Organization with name @${organization.name}` - }))], - [organizationStore.length, userSession] - ) - - if (userSession === null) router.push("/login") - - return <> - - Authorize application - - - - Build high-class workflows, endpoints and software without coding - - - { - !isValidCallback && ( - <> - - This is not a valid and verified application. Please go back if you don't trust the source of this - link. - - - - ) - } - Application - The external application you are granting access to. - } disabled value={`${callbackUrl}`}/> - - { - selectNamespace && ( - <> - Organization - Choose the organization this application can access. - - - - - - - - - - - - - { - namespaces.map(namespace => ( - - - {namespace.title} - - - )) - } - - - - - - - ) - } - Permissions - The application is requesting permission to perform the following actions. - - - - - Can access your account information like email, username, and other stored information on your - account - - - - - - Can access, manage and edit organizations - - - - - - Can access, manage and edit licenses - - - - - - - - - - -} \ No newline at end of file + return notFound() +} diff --git a/src/packages/cloud/src/user/pages/UserRedirectPage.tsx b/src/packages/cloud/src/user/pages/UserRedirectPage.tsx new file mode 100644 index 00000000..81b58e4e --- /dev/null +++ b/src/packages/cloud/src/user/pages/UserRedirectPage.tsx @@ -0,0 +1,172 @@ +"use client" + +import React from "react"; +import { + Alert, + Button, + Card, + Flex, + InputDescription, + InputLabel, + SelectContent, + SelectInput, + SelectItem, + SelectItemText, + SelectPortal, + SelectTrigger, + SelectValue, + SelectViewport, + Spacing, + Text, + TextInput, + useService, + useStore +} from "@code0-tech/pictor"; +import {useUserSession} from "@ce-internal/user/hooks/User.session.hook"; +import {useRouter, useSearchParams} from "next/navigation"; +import {IconApiApp, IconCheck, IconChevronDown} from "@tabler/icons-react"; +import {isValidRedirect} from "@core/util/redirect"; +import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; +import {OrganizationService} from "@ce-internal/organization/services/Organization.service"; +import {UserService} from "@ce-internal/user/services/User.service"; +import {OrganizationView} from "@ce-internal/organization/services/Organization.view"; + +export const UserRedirectPage: React.FC = () => { + + const userSession = useUserSession() + const params = useSearchParams() + const callbackUrl = params.get("callbackUrl") + const selectNamespace = params.get("selectNamespace") + const router = useRouter() + const isValidCallback = isValidRedirect(callbackUrl) + const organizationService = useService(OrganizationService) + const organizationStore = useStore(OrganizationService) + const userService = useService(UserService) + const [namespaceId, setNamespaceId] = React.useState(null); + + const namespaces: ({ + title?: string + id?: string + })[] = React.useMemo( + () => [{ + title: `Personal organization of user @${userSession?.user?.username}`, + id: userSession?.user?.namespace?.id! + }, ...organizationService.values().filter((organization: OrganizationView) => organization.userAbilities).map((organization: OrganizationView) => ({ + id: organization.namespace?.id!, + title: `Organization with name @${organization.name}` + }))], + [organizationStore.length, userSession] + ) + + if (userSession === null) router.push("/login") + + return <> + + Authorize application + + + + Build high-class workflows, endpoints and software without coding + + + { + !isValidCallback && ( + <> + + This is not a valid and verified application. Please go back if you don't trust the source of this + link. + + + + ) + } + Application + The external application you are granting access to. + } disabled value={`${callbackUrl}`}/> + + { + selectNamespace && ( + <> + Organization + Choose the organization this application can access. + + + + + + + + + + + + + { + namespaces.map(namespace => ( + + + {namespace.title} + + + )) + } + + + + + + + ) + } + Permissions + The application is requesting permission to perform the following actions. + + + + + Can access your account information like email, username, and other stored information on your + account + + + + + + Can access, manage and edit organizations + + + + + + Can access, manage and edit licenses + + + + + + + + + + +}