feat(routes): support multi-applications in config [breaking] (#34)
* chore: remove leftover console log * chore: don't show a ring over dropdowns * feat(routes): support multi-applications in config * test: update snapshot
This commit is contained in:
@@ -15,6 +15,18 @@ return [
|
||||
|
||||
'prefix' => 'nimbus',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Application
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value defines the default application that Nimbus will load when
|
||||
| no application is specified in the request.
|
||||
|
|
||||
*/
|
||||
|
||||
'default_application' => 'main',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Allowed Environments
|
||||
@@ -31,144 +43,160 @@ return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Route Configuration
|
||||
| Application Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options control how Nimbus identifies and registers application
|
||||
| routes. The route configuration determines which endpoints will be
|
||||
| analyzed, displayed, or interacted with by Nimbus.
|
||||
*/
|
||||
|
||||
'routes' => [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Route Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The prefix used to discover and filter the routes for inclusion in Nimbus.
|
||||
| Only routes whose URIs begin with this prefix will be loaded in the UI.
|
||||
| Adjust this value if your API endpoints use a different root segment.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => 'api',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Versioned Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Determines whether the routes identified by the prefix should be
|
||||
| treated as versioned (for example: /api/v1/users). If enabled, Nimbus
|
||||
| automatically detects version segments and handles them separately
|
||||
| in the schema representation. Disable this if your routes are flat
|
||||
| or non-versioned.
|
||||
|
|
||||
*/
|
||||
|
||||
'versioned' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Base URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value defines the base URL that Nimbus will use when relaying
|
||||
| API requests from the UI. It is useful in cases where your API is
|
||||
| hosted on a different domain, port, or subpath than the UI itself.
|
||||
|
|
||||
| If left null, Nimbus will automatically use the same host and scheme
|
||||
| as the incoming request that triggered the relay. This is the
|
||||
| recommended default for most deployments where the API and UI share
|
||||
| the same origin.
|
||||
|
|
||||
*/
|
||||
|
||||
'api_base_url' => null,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Defines how Nimbus authenticates API requests when interacting with your
|
||||
| application routes. The authentication configuration determines which
|
||||
| Laravel guard is used and how special authentication modes—such as
|
||||
| “login as current user” or “impersonate user” are handled.
|
||||
| Defines the different applications that Nimbus supports. Each application
|
||||
| can have its own route, authentication, and headers configuration.
|
||||
|
|
||||
*/
|
||||
|
||||
'auth' => [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guard
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The name of the Laravel authentication guard used for the api endpoints.
|
||||
| This guard must be the guard used to authenticate the requests to
|
||||
| the API endpoints from the prefix above (nimbus.routes.prefix).
|
||||
*/
|
||||
|
||||
'guard' => 'web',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Special Authentication
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These settings control Nimbus's advanced authentication modes,
|
||||
| such as impersonation or logging in as the current user. Each mode
|
||||
| modifies outgoing HTTP requests by injecting appropriate credentials
|
||||
| or tokens before they are sent.
|
||||
|
|
||||
*/
|
||||
|
||||
'special' => [
|
||||
'applications' => [
|
||||
'main' => [
|
||||
'name' => 'Main',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Injector
|
||||
| Route Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Defines the injector class used to modify outgoing requests with
|
||||
| authentication credentials. The class must implement the
|
||||
| `SpecialAuthenticationInjectorContract` interface.
|
||||
|
|
||||
| Included implementations:
|
||||
| - RememberMeCookieInjector::class:
|
||||
| Forwards or generates a Laravel "remember me" cookie.
|
||||
| - TymonJwtTokenInjector::class:
|
||||
| Injects a Bearer token using the `tymon/jwt-aut` package.
|
||||
|
|
||||
| P.S. You may provide a custom implementation to support alternative authentication mechanisms.
|
||||
| These options control how Nimbus identifies and registers application
|
||||
| routes. The route configuration determines which endpoints will be
|
||||
| analyzed, displayed, or interacted with by Nimbus.
|
||||
*/
|
||||
|
||||
'injector' => \Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors\RememberMeCookieInjector::class,
|
||||
'routes' => [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Route Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The prefix used to discover and filter the routes for inclusion in Nimbus.
|
||||
| Only routes whose URIs begin with this prefix will be loaded in the UI.
|
||||
| Adjust this value if your API endpoints use a different root segment.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => 'api',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Versioned Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Determines whether the routes identified by the prefix should be
|
||||
| treated as versioned (for example: /api/v1/users). If enabled, Nimbus
|
||||
| automatically detects version segments and handles them separately
|
||||
| in the schema representation. Disable this if your routes are flat
|
||||
| or non-versioned.
|
||||
|
|
||||
*/
|
||||
|
||||
'versioned' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Base URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value defines the base URL that Nimbus will use when relaying
|
||||
| API requests from the UI. It is useful in cases where your API is
|
||||
| hosted on a different domain, port, or subpath than the UI itself.
|
||||
|
|
||||
| If left null, Nimbus will automatically use the same host and scheme
|
||||
| as the incoming request that triggered the relay. This is the
|
||||
| recommended default for most deployments where the API and UI share
|
||||
| the same origin.
|
||||
|
|
||||
*/
|
||||
|
||||
'api_base_url' => null,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Defines how Nimbus authenticates API requests when interacting with your
|
||||
| application routes. The authentication configuration determines which
|
||||
| Laravel guard is used and how special authentication modes—such as
|
||||
| “login as current user” or “impersonate user” are handled.
|
||||
|
|
||||
*/
|
||||
|
||||
'auth' => [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guard
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The name of the Laravel authentication guard used for the api endpoints.
|
||||
| This guard must be the guard used to authenticate the requests to
|
||||
| the API endpoints from the prefix above (nimbus.routes.prefix).
|
||||
*/
|
||||
|
||||
'guard' => 'web',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Special Authentication
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These settings control Nimbus's advanced authentication modes,
|
||||
| such as impersonation or logging in as the current user. Each mode
|
||||
| modifies outgoing HTTP requests by injecting appropriate credentials
|
||||
| or tokens before they are sent.
|
||||
|
|
||||
*/
|
||||
|
||||
'special' => [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Injector
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Defines the injector class used to modify outgoing requests with
|
||||
| authentication credentials. The class must implement the
|
||||
| `SpecialAuthenticationInjectorContract` interface.
|
||||
|
|
||||
| Included implementations:
|
||||
| - RememberMeCookieInjector::class:
|
||||
| Forwards or generates a Laravel "remember me" cookie.
|
||||
| - TymonJwtTokenInjector::class:
|
||||
| Injects a Bearer token using the `tymon/jwt-aut` package.
|
||||
|
|
||||
| P.S. You may provide a custom implementation to support alternative authentication mechanisms.
|
||||
*/
|
||||
|
||||
'injector' => \Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors\RememberMeCookieInjector::class,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global Headers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define any global headers that should be applied to every Nimbus request.
|
||||
| Each header may be defined as either:
|
||||
| - A value from GlobalHeaderGeneratorTypeEnum::class, or
|
||||
| - A raw primitive value (string, integer, or boolean).
|
||||
|
|
||||
| Example:
|
||||
| 'headers' => [
|
||||
| 'X-Request-ID' => GlobalHeaderGeneratorTypeEnum::UUID,
|
||||
| 'X-App-Version' => '1.0.0',
|
||||
| ],
|
||||
|
|
||||
*/
|
||||
|
||||
'headers' => [
|
||||
/** @see \Sunchayn\Nimbus\Modules\Config\GlobalHeaderGeneratorTypeEnum */
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global Headers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Define any global headers that should be applied to every Nimbus request.
|
||||
| Each header may be defined as either:
|
||||
| - A value from GlobalHeaderGeneratorTypeEnum::class, or
|
||||
| - A raw primitive value (string, integer, or boolean).
|
||||
|
|
||||
| Example:
|
||||
| 'headers' => [
|
||||
| 'X-Request-ID' => GlobalHeaderGeneratorTypeEnum::UUID,
|
||||
| 'X-App-Version' => '1.0.0',
|
||||
| ],
|
||||
|
|
||||
*/
|
||||
|
||||
'headers' => [
|
||||
/** @see \Sunchayn\Nimbus\Modules\Config\GlobalHeaderGeneratorTypeEnum */
|
||||
],
|
||||
];
|
||||
|
||||
@@ -25,7 +25,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-9 items-center justify-between rounded-md border border-zinc-200 bg-transparent px-3 py-2 text-start text-sm whitespace-nowrap shadow-sm ring-offset-white focus-visible:ring-1 focus-visible:ring-zinc-950 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-zinc-500 dark:border-zinc-800 dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300 dark:data-[placeholder]:text-zinc-400 [&>span]:truncate',
|
||||
'flex h-9 items-center justify-between rounded-md border border-zinc-200 bg-transparent px-3 py-2 text-start text-sm whitespace-nowrap shadow-sm ring-offset-white focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-zinc-500 dark:border-zinc-800 dark:data-[placeholder]:text-zinc-400 [&>span]:truncate',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AppSelect,
|
||||
AppSelectContent,
|
||||
AppSelectGroup,
|
||||
AppSelectItem,
|
||||
AppSelectLabel,
|
||||
AppSelectTrigger,
|
||||
AppSelectValue,
|
||||
} from '@/components/base/select';
|
||||
import { useConfigStore } from '@/stores';
|
||||
import { LayersIcon } from 'lucide-vue-next';
|
||||
import { AcceptableValue } from 'reka-ui';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const handleApplicationChange = (applicationKey: AcceptableValue) => {
|
||||
if (applicationKey === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We reload the page with the new application query parameter to trigger the backend switch.
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('application', String(applicationKey));
|
||||
window.location.href = url.toString();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<AppSelect
|
||||
:model-value="configStore.activeApplication || ''"
|
||||
@update:model-value="handleApplicationChange"
|
||||
>
|
||||
<AppSelectTrigger
|
||||
class="px-panel w-full border-none text-xs shadow-none focus:ring-0 active:ring-0"
|
||||
>
|
||||
<div class="flex items-center gap-2 overflow-hidden">
|
||||
<LayersIcon class="text-muted-foreground size-3.5 shrink-0" />
|
||||
<AppSelectValue
|
||||
placeholder="Select an application"
|
||||
class="truncate"
|
||||
/>
|
||||
</div>
|
||||
</AppSelectTrigger>
|
||||
<AppSelectContent>
|
||||
<AppSelectGroup>
|
||||
<AppSelectLabel>Available Applications</AppSelectLabel>
|
||||
<AppSelectItem
|
||||
v-for="(name, key) in configStore.applications"
|
||||
:key="key"
|
||||
:value="key"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ name }}
|
||||
</AppSelectItem>
|
||||
</AppSelectGroup>
|
||||
</AppSelectContent>
|
||||
</AppSelect>
|
||||
</div>
|
||||
</template>
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
AppSidebarMenu,
|
||||
AppSidebarRail,
|
||||
} from '@/components/base/sidebar';
|
||||
import ApplicationSwitcher from '@/components/domain/RoutesExplorer/ApplicationSwitcher.vue';
|
||||
import RouteExplorerHeader from '@/components/domain/RoutesExplorer/RouteExplorerHeader.vue';
|
||||
import RouteExplorerVersionSelector from '@/components/domain/RoutesExplorer/RouteExplorerVersionSelector.vue';
|
||||
import RoutesList from '@/components/domain/RoutesExplorer/RoutesList/RoutesList.vue';
|
||||
@@ -16,7 +17,7 @@ import { RouteDefinition, RoutesGroup } from '@/interfaces/routes/routes';
|
||||
import { useConfigStore } from '@/stores';
|
||||
import { uniquePersistenceKey } from '@/utils/stores';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
/*
|
||||
* Props.
|
||||
@@ -32,9 +33,22 @@ const props = defineProps<{
|
||||
|
||||
const search = useStorage(uniquePersistenceKey('routes-explorer-search-keyword'), '');
|
||||
|
||||
const versions = computed(() => Object.keys(props.routes || []));
|
||||
const versions = computed(() => Object.keys(props.routes || {}));
|
||||
|
||||
const currentVersion = computed(() => versions.value[0]);
|
||||
const currentVersion = ref('');
|
||||
|
||||
// Initialize or update current version when versions list changes (e.g. after project switch)
|
||||
watch(
|
||||
versions,
|
||||
newVersions => {
|
||||
if (newVersions.length > 0 && !newVersions.includes(currentVersion.value)) {
|
||||
currentVersion.value = newVersions[0];
|
||||
} else if (newVersions.length === 0) {
|
||||
currentVersion.value = '';
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/*
|
||||
* Computed.
|
||||
@@ -77,6 +91,10 @@ const filteredRoutes = computed(() => {
|
||||
*/
|
||||
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const hasMultipleApplications = computed(
|
||||
() => Object.keys(configStore.applications).length > 1,
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -89,11 +107,21 @@ const configStore = useConfigStore();
|
||||
:disabled="routesInVersion.length === 0"
|
||||
class="h-[calc(var(--toolbar-height)+1px)] w-full rounded-none border-0 border-b text-xs shadow-none focus:ring-0 focus-visible:ring-0"
|
||||
/>
|
||||
<RouteExplorerVersionSelector
|
||||
v-if="configStore.isVersioned && versions.length"
|
||||
v-model="currentVersion"
|
||||
:versions="versions"
|
||||
/>
|
||||
<div class="h-sub-toolbar flex items-center overflow-hidden border-b">
|
||||
<ApplicationSwitcher v-if="hasMultipleApplications" class="flex-1" />
|
||||
<div
|
||||
v-if="configStore.isVersioned && versions.length"
|
||||
class="h-full w-[80px] shrink-0 border-l"
|
||||
:class="{
|
||||
'flex-1': !hasMultipleApplications,
|
||||
}"
|
||||
>
|
||||
<RouteExplorerVersionSelector
|
||||
v-model="currentVersion"
|
||||
:versions="versions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AppSidebarContent>
|
||||
<AppSidebarGroup class="p-0">
|
||||
|
||||
@@ -15,14 +15,19 @@ const model = defineModel<string>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppSelect v-model="model" class="flex items-center text-sm">
|
||||
<AppSelect v-model="model" class="flex items-center">
|
||||
<AppSelectTrigger
|
||||
class="h-sub-toolbar w-full rounded-none border-0 border-b shadow-none focus:ring-0 focus-visible:outline-none"
|
||||
class="px-panel w-full rounded-none border-0 text-xs shadow-none focus:ring-0 focus-visible:outline-none active:ring-0"
|
||||
>
|
||||
<AppSelectValue placeholder="Select API Version" />
|
||||
</AppSelectTrigger>
|
||||
<AppSelectContent>
|
||||
<AppSelectItem v-for="value in versions" :key="value" :value="value">
|
||||
<AppSelectItem
|
||||
v-for="value in versions"
|
||||
:key="value"
|
||||
:value="value"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ value }}
|
||||
</AppSelectItem>
|
||||
</AppSelectContent>
|
||||
|
||||
@@ -21,6 +21,10 @@ export const useConfigStore = defineStore('config', () => {
|
||||
const currentUser = window.Nimbus?.currentUser
|
||||
? JSON.parse(window.Nimbus.currentUser as string)
|
||||
: null;
|
||||
const applications: Record<string, string> = window.Nimbus?.applications
|
||||
? JSON.parse(window.Nimbus.applications as string)
|
||||
: {};
|
||||
const activeApplication = window.Nimbus?.activeApplication || null;
|
||||
|
||||
// Derived values
|
||||
const isLoggedIn = currentUser !== null;
|
||||
@@ -33,5 +37,7 @@ export const useConfigStore = defineStore('config', () => {
|
||||
isVersioned,
|
||||
isLoggedIn,
|
||||
userId,
|
||||
applications,
|
||||
activeApplication,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -672,8 +672,6 @@ describe('SingleDumpRenderer', () => {
|
||||
// Should show [] for empty array
|
||||
const content = screen.getAllByTestId('collapsible-trigger')[1];
|
||||
|
||||
console.log(content);
|
||||
|
||||
expect(content.textContent).toContain('[]');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,9 @@ const mockRoutesStore: {
|
||||
const mockConfigStore = {
|
||||
apiUrl: 'https://api.example.com',
|
||||
headers: [],
|
||||
applications: {},
|
||||
isVersioned: false,
|
||||
activeApplication: null,
|
||||
};
|
||||
|
||||
const mockValueGeneratorStore = {
|
||||
|
||||
@@ -29,6 +29,8 @@ describe('useConfigStore', () => {
|
||||
currentUser: JSON.stringify({ id: 99 }),
|
||||
routes: '',
|
||||
routeExtractorException: null,
|
||||
applications: JSON.stringify({ main: 'Main API' }),
|
||||
activeApplication: 'main',
|
||||
};
|
||||
|
||||
const store = useConfigStore();
|
||||
|
||||
2
resources/types/global.d.ts
vendored
2
resources/types/global.d.ts
vendored
@@ -8,6 +8,8 @@ interface NimbusConfig {
|
||||
isVersioned: boolean;
|
||||
routeExtractorException: string | null;
|
||||
currentUser: string | null;
|
||||
applications: string | null;
|
||||
activeApplication: string | null;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -4,20 +4,20 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="icon" type="image/png" href="{{ asset('/vendor/nimbus/favicon/favicon-96x96.png') }}" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="{{ asset('/vendor/nimbus/favicon/favicon.svg') }}" />
|
||||
<link rel="shortcut icon" href="{{ asset('/vendor/nimbus/favicon/favicon.ico') }}" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('/vendor/nimbus/favicon/apple-touch-icon.png') }}" />
|
||||
<meta name="apple-mobile-web-app-title" content="Nimbus" />
|
||||
<link rel="icon" type="image/png" href="{{ asset('/vendor/nimbus/favicon/favicon-96x96.png') }}" sizes="96x96"/>
|
||||
<link rel="icon" type="image/svg+xml" href="{{ asset('/vendor/nimbus/favicon/favicon.svg') }}"/>
|
||||
<link rel="shortcut icon" href="{{ asset('/vendor/nimbus/favicon/favicon.ico') }}"/>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('/vendor/nimbus/favicon/apple-touch-icon.png') }}"/>
|
||||
<meta name="apple-mobile-web-app-title" content="Nimbus"/>
|
||||
|
||||
<title>{{ config('app.name', 'Laravel') }} - Nimbus</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet"/>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
(function () {
|
||||
const appearance = '{{ $appearance ?? "system" }}';
|
||||
|
||||
if (appearance === 'system') {
|
||||
@@ -30,21 +30,24 @@
|
||||
})();
|
||||
</script>
|
||||
@php
|
||||
$config = \Illuminate\Support\Js::from([
|
||||
'basePath' => rtrim(\Illuminate\Support\Str::start(config('nimbus.prefix'), '/'), '/'),
|
||||
'routes' => isset($routes) ? json_encode($routes) : null,
|
||||
'headers' => isset($headers) ? json_encode($headers) : null,
|
||||
'routeExtractorException' => isset($routeExtractorException) ? json_encode($routeExtractorException) : null,
|
||||
'isVersioned' => config('nimbus.routes.versioned'),
|
||||
'apiBaseUrl' => config('nimbus.routes.api_base_url', request()->getSchemeAndHttpHost()),
|
||||
'currentUser' => isset($currentUser) ? json_encode($currentUser) : null,
|
||||
]);
|
||||
/** @var \Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver $activeApplicationResolver */
|
||||
$config = \Illuminate\Support\Js::from([
|
||||
'basePath' => rtrim(\Illuminate\Support\Str::start(config('nimbus.prefix'), '/'), '/'),
|
||||
'routes' => isset($routes) ? json_encode($routes) : null,
|
||||
'headers' => isset($headers) ? json_encode($headers) : null,
|
||||
'routeExtractorException' => isset($routeExtractorException) ? json_encode($routeExtractorException) : null,
|
||||
'isVersioned' => $activeApplicationResolver->isVersioned(),
|
||||
'apiBaseUrl' => $activeApplicationResolver->getApiBaseUrl(),
|
||||
'currentUser' => isset($currentUser) ? json_encode($currentUser) : null,
|
||||
'applications' => $activeApplicationResolver->getAvailableApplications(),
|
||||
'activeApplication' => $activeApplicationResolver->getActiveApplicationKey(),
|
||||
]);
|
||||
|
||||
$configTag = new \Illuminate\Support\HtmlString(<<<HTML
|
||||
<script type="module">
|
||||
window.Nimbus = {$config};
|
||||
</script>
|
||||
HTML);
|
||||
$configTag = new \Illuminate\Support\HtmlString(<<<HTML
|
||||
<script type="module">
|
||||
window.Nimbus = {$config};
|
||||
</script>
|
||||
HTML);
|
||||
@endphp
|
||||
|
||||
{{ $configTag }}
|
||||
@@ -52,6 +55,6 @@
|
||||
@vite(['resources/css/app.css', 'resources/js/app/app.ts'])
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
<div id="app"></div>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,6 +7,7 @@ use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
use Illuminate\Support\Facades\Vite;
|
||||
use Illuminate\Support\Str;
|
||||
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||
use Sunchayn\Nimbus\Modules\Routes\Actions;
|
||||
use Sunchayn\Nimbus\Modules\Routes\Exceptions\RouteExtractionException;
|
||||
|
||||
@@ -20,9 +21,16 @@ class NimbusIndexController
|
||||
Actions\BuildGlobalHeadersAction $buildGlobalHeadersAction,
|
||||
Actions\BuildCurrentUserAction $buildCurrentUserAction,
|
||||
Actions\DisableThirdPartyUiAction $disableThirdPartyUiAction,
|
||||
ActiveApplicationResolver $activeApplicationResolver,
|
||||
): Renderable|RedirectResponse {
|
||||
$disableThirdPartyUiAction->execute();
|
||||
|
||||
if (request()->has('application')) {
|
||||
return redirect()
|
||||
->to(request()->fullUrlWithQuery(['application' => null]))
|
||||
->withCookie(cookie()->forever(ActiveApplicationResolver::CURRENT_APPLICATION_COOKIE_NAME, request()->get('application')));
|
||||
}
|
||||
|
||||
Vite::useBuildDirectory('/vendor/nimbus');
|
||||
Vite::useHotFile(base_path('/vendor/sunchayn/nimbus/resources/dist/hot'));
|
||||
|
||||
@@ -42,6 +50,7 @@ class NimbusIndexController
|
||||
} catch (RouteExtractionException $routeExtractionException) {
|
||||
return view(self::VIEW_NAME, [ // @phpstan-ignore-line it cannot find the view.
|
||||
'routeExtractorException' => $this->renderExtractorException($routeExtractionException),
|
||||
'activeApplicationResolver' => $activeApplicationResolver,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -49,6 +58,7 @@ class NimbusIndexController
|
||||
'routes' => $routes->toFrontendArray(),
|
||||
'headers' => $buildGlobalHeadersAction->execute(),
|
||||
'currentUser' => $buildCurrentUserAction->execute(),
|
||||
'activeApplicationResolver' => $activeApplicationResolver,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
118
src/Modules/Config/ActiveApplicationResolver.php
Normal file
118
src/Modules/Config/ActiveApplicationResolver.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Config;
|
||||
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Contracts\SpecialAuthenticationInjectorContract;
|
||||
|
||||
class ActiveApplicationResolver
|
||||
{
|
||||
public const CURRENT_APPLICATION_COOKIE_NAME = 'nimbus:application';
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
protected readonly array $activeApplicationConfig;
|
||||
|
||||
protected readonly string $activeApplicationKey;
|
||||
|
||||
/**
|
||||
* @throws MisconfiguredValueException
|
||||
*/
|
||||
public function __construct(
|
||||
protected Repository $config,
|
||||
protected Request $request
|
||||
) {
|
||||
$this->activeApplicationKey = $this->determineActiveApplicationKey();
|
||||
|
||||
$this->activeApplicationConfig = $this->getActiveApplicationConfig();
|
||||
}
|
||||
|
||||
public function getActiveApplicationKey(): string
|
||||
{
|
||||
return $this->activeApplicationKey;
|
||||
}
|
||||
|
||||
public function isVersioned(): bool
|
||||
{
|
||||
return $this->activeApplicationConfig['routes']['versioned'] ?? false;
|
||||
}
|
||||
|
||||
public function getApiBaseUrl(): string
|
||||
{
|
||||
return $this->activeApplicationConfig['routes']['api_base_url'] ?? $this->request->getSchemeAndHttpHost();
|
||||
}
|
||||
|
||||
public function getRoutesPrefix(): string
|
||||
{
|
||||
return $this->activeApplicationConfig['routes']['prefix'] ?? 'api';
|
||||
}
|
||||
|
||||
public function getAuthGuard(): string
|
||||
{
|
||||
return $this->activeApplicationConfig['auth']['guard'] ?? 'web';
|
||||
}
|
||||
|
||||
/** @return ?class-string<SpecialAuthenticationInjectorContract> */
|
||||
public function getSpecialAuthInjector(): ?string
|
||||
{
|
||||
return $this->activeApplicationConfig['auth']['special']['injector'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->activeApplicationConfig['headers'] ?? [];
|
||||
}
|
||||
|
||||
public function getAvailableApplications(): string
|
||||
{
|
||||
$applications = $this->config->get('nimbus.applications', []);
|
||||
|
||||
return (string) json_encode(
|
||||
Arr::mapWithKeys(
|
||||
$applications,
|
||||
fn (array $config, string $key): array => [
|
||||
$key => $config['name'] ?? $key,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MisconfiguredValueException
|
||||
*/
|
||||
protected function determineActiveApplicationKey(): string
|
||||
{
|
||||
$applications = $this->config->get('nimbus.applications', []);
|
||||
|
||||
if ($applications === []) {
|
||||
throw MisconfiguredValueException::becauseApplicationsAreNotDefined();
|
||||
}
|
||||
|
||||
$applicationKey = $this->request->cookie(self::CURRENT_APPLICATION_COOKIE_NAME);
|
||||
|
||||
if (is_string($applicationKey) && array_key_exists($applicationKey, $applications)) {
|
||||
return $applicationKey;
|
||||
}
|
||||
|
||||
$applicationKey = $this->config->get('nimbus.default_application');
|
||||
|
||||
if (! array_key_exists($applicationKey, $applications)) {
|
||||
throw MisconfiguredValueException::becauseDefaultApplicationIsInvalid($applicationKey);
|
||||
}
|
||||
|
||||
return (string) $applicationKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getActiveApplicationConfig(): array
|
||||
{
|
||||
return $this->config->get('nimbus.applications.'.$this->activeApplicationKey, []);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,10 @@ class MisconfiguredValueException extends Exception
|
||||
|
||||
public const INVALID_GUARD_INJECTOR_COMBINATION = 3;
|
||||
|
||||
public const INVALID_DEFAULT_APPLICATION = 4;
|
||||
|
||||
public const INVALID_APPLICATIONS = 5;
|
||||
|
||||
public static function becauseSpecialAuthenticationInjectorIsInvalid(): self
|
||||
{
|
||||
return new self(
|
||||
@@ -39,4 +43,20 @@ class MisconfiguredValueException extends Exception
|
||||
code: self::INVALID_GUARD_INJECTOR_COMBINATION,
|
||||
);
|
||||
}
|
||||
|
||||
public static function becauseDefaultApplicationIsInvalid(string $key): self
|
||||
{
|
||||
return new self(
|
||||
message: sprintf("The default application `%s` doesn't have a matching configuration.", $key),
|
||||
code: self::INVALID_DEFAULT_APPLICATION,
|
||||
);
|
||||
}
|
||||
|
||||
public static function becauseApplicationsAreNotDefined(): self
|
||||
{
|
||||
return new self(
|
||||
message: 'There are no applications defined.',
|
||||
code: self::INVALID_APPLICATIONS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Concerns;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Contracts\SpecialAuthenticationInjectorContract;
|
||||
|
||||
@@ -14,10 +14,10 @@ trait UsesSpecialAuthenticationInjector
|
||||
* @throws BindingResolutionException
|
||||
* @throws MisconfiguredValueException
|
||||
*/
|
||||
public function getInjector(Container $container, Repository $configRepository): SpecialAuthenticationInjectorContract
|
||||
public function getInjector(Container $container, ActiveApplicationResolver $activeApplicationResolver): SpecialAuthenticationInjectorContract
|
||||
{
|
||||
/** @var ?class-string $injectorClass */
|
||||
$injectorClass = $configRepository->get('nimbus.auth.special.injector');
|
||||
$injectorClass = $this->projectManager->getSpecialAuthInjector();
|
||||
|
||||
if ($injectorClass === null) {
|
||||
throw MisconfiguredValueException::becauseSpecialAuthenticationInjectorIsInvalid();
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Handlers;
|
||||
|
||||
use Illuminate\Config\Repository as ConfigRepository;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Session\SessionManager;
|
||||
use Illuminate\Session\Store;
|
||||
use Illuminate\Support\Arr;
|
||||
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Concerns\UsesSpecialAuthenticationInjector;
|
||||
|
||||
/**
|
||||
@@ -30,6 +31,7 @@ class CurrentUserAuthorizationHandler implements AuthorizationHandler
|
||||
public function __construct(
|
||||
private readonly Request $relayRequest,
|
||||
private readonly Container $container,
|
||||
private readonly ActiveApplicationResolver $projectManager,
|
||||
private readonly ConfigRepository $configRepository,
|
||||
) {
|
||||
$this->userProvider = $this->resolveUserProvider();
|
||||
@@ -44,7 +46,7 @@ class CurrentUserAuthorizationHandler implements AuthorizationHandler
|
||||
}
|
||||
|
||||
return $this
|
||||
->getInjector($this->container, $this->configRepository)
|
||||
->getInjector($this->container, $this->projectManager)
|
||||
->attach($pendingRequest, $user);
|
||||
}
|
||||
|
||||
@@ -55,9 +57,7 @@ class CurrentUserAuthorizationHandler implements AuthorizationHandler
|
||||
{
|
||||
$authManager = $this->container->get('auth');
|
||||
|
||||
$guardName = $this->configRepository->get('nimbus.auth.guard');
|
||||
|
||||
return $authManager->guard($guardName)->getProvider();
|
||||
return $authManager->guard($this->projectManager->getAuthGuard())->getProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,7 +86,7 @@ class CurrentUserAuthorizationHandler implements AuthorizationHandler
|
||||
$session->setId(id: $this->extractSessionIdFromCookie($sessionCookie));
|
||||
$session->start();
|
||||
|
||||
$tokenPrefix = 'login_'.$this->configRepository->get('nimbus.auth.guard');
|
||||
$tokenPrefix = 'login_'.($this->projectManager->getAuthGuard());
|
||||
|
||||
$userId = Arr::first(
|
||||
$session->all(),
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Handlers;
|
||||
|
||||
use Illuminate\Config\Repository as ConfigRepository;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Concerns\UsesSpecialAuthenticationInjector;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Exceptions\InvalidAuthorizationValueException;
|
||||
@@ -20,7 +20,7 @@ class ImpersonateUserAuthorizationHandler implements AuthorizationHandler
|
||||
public function __construct(
|
||||
public readonly int $userId,
|
||||
private readonly Container $container,
|
||||
private readonly ConfigRepository $configRepository,
|
||||
private readonly ActiveApplicationResolver $projectManager,
|
||||
) {
|
||||
if ($userId <= 0) {
|
||||
throw InvalidAuthorizationValueException::becauseUserIsNotFound();
|
||||
@@ -28,7 +28,7 @@ class ImpersonateUserAuthorizationHandler implements AuthorizationHandler
|
||||
|
||||
$this->userProvider = $this
|
||||
->container->get('auth')
|
||||
->guard(name: config('nimbus.auth.guard'))
|
||||
->guard(name: $this->projectManager->getAuthGuard())
|
||||
->getProvider();
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class ImpersonateUserAuthorizationHandler implements AuthorizationHandler
|
||||
}
|
||||
|
||||
return $this
|
||||
->getInjector($this->container, $this->configRepository)
|
||||
->getInjector($this->container, $this->projectManager)
|
||||
->attach($pendingRequest, $user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors;
|
||||
|
||||
use Illuminate\Config\Repository as ConfigRepository;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
@@ -15,6 +14,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Contracts\SpecialAuthenticationInjectorContract;
|
||||
|
||||
@@ -34,12 +34,12 @@ class RememberMeCookieInjector implements SpecialAuthenticationInjectorContract
|
||||
public function __construct(
|
||||
private readonly Request $relayRequest,
|
||||
private readonly Container $container,
|
||||
ConfigRepository $configRepository,
|
||||
ActiveApplicationResolver $activeApplicationResolver,
|
||||
) {
|
||||
$this->encrypter = $this->container->get('encrypter');
|
||||
|
||||
$this->authGuard = $this->container->get('auth')->guard(
|
||||
$configRepository->get('nimbus.auth.guard'),
|
||||
$activeApplicationResolver->getAuthGuard(),
|
||||
);
|
||||
|
||||
if (! $this->authGuard instanceof StatefulGuard) {
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors;
|
||||
|
||||
use Illuminate\Config\Repository as ConfigRepository;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Contracts\SpecialAuthenticationInjectorContract;
|
||||
|
||||
@@ -18,7 +18,7 @@ class TymonJwtTokenInjector implements SpecialAuthenticationInjectorContract
|
||||
* @throws MisconfiguredValueException
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ConfigRepository $configRepository,
|
||||
private readonly ActiveApplicationResolver $activeApplicationResolver,
|
||||
private readonly Container $container,
|
||||
) {
|
||||
if (! class_exists(\Tymon\JWTAuth\JWTGuard::class)) {
|
||||
@@ -27,7 +27,7 @@ class TymonJwtTokenInjector implements SpecialAuthenticationInjectorContract
|
||||
|
||||
$this->guard = $this
|
||||
->container->make('auth')
|
||||
->guard(name: $this->configRepository->get('nimbus.auth.guard'));
|
||||
->guard(name: $this->activeApplicationResolver->getAuthGuard());
|
||||
|
||||
if (! $this->guard instanceof \Tymon\JWTAuth\JWTGuard) {
|
||||
throw MisconfiguredValueException::becauseOfInvalidGuardInjectorCombination("Please use a `\Tymon\JWTAuth\JWTGuard` guard.");
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Routes\Actions;
|
||||
|
||||
use Illuminate\Config\Repository as ConfigRepository;
|
||||
use Illuminate\Support\Arr;
|
||||
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||
use Sunchayn\Nimbus\Modules\Config\GlobalHeaderGeneratorTypeEnum;
|
||||
|
||||
class BuildGlobalHeadersAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ConfigRepository $configRepository,
|
||||
private readonly ActiveApplicationResolver $activeApplicationResolver,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -18,7 +18,7 @@ class BuildGlobalHeadersAction
|
||||
public function execute(): array
|
||||
{
|
||||
/** @var array<array-key, mixed> $headers */
|
||||
$headers = $this->configRepository->get('nimbus.headers');
|
||||
$headers = $this->activeApplicationResolver->getHeaders();
|
||||
|
||||
return array_values(
|
||||
Arr::map(
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
namespace Sunchayn\Nimbus\Modules\Routes\Actions;
|
||||
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||
use Sunchayn\Nimbus\Modules\Routes\Collections\ExtractedRoutesCollection;
|
||||
use Sunchayn\Nimbus\Modules\Routes\DataTransferObjects\ExtractedRoute;
|
||||
use Sunchayn\Nimbus\Modules\Routes\Exceptions\RouteExtractionException;
|
||||
@@ -29,7 +29,7 @@ class ExtractRoutesAction
|
||||
protected SchemaExtractor $schemaExtractor,
|
||||
protected ExtractableRouteFactory $routeFactory,
|
||||
protected IgnoredRoutesService $ignoredRoutesService,
|
||||
protected Repository $config,
|
||||
protected ActiveApplicationResolver $activeApplicationResolver,
|
||||
protected LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
@@ -40,7 +40,7 @@ class ExtractRoutesAction
|
||||
*/
|
||||
public function execute(array $routes): ExtractedRoutesCollection
|
||||
{
|
||||
$prefix = $this->config->get('nimbus.routes.prefix');
|
||||
$prefix = $this->activeApplicationResolver->getRoutesPrefix();
|
||||
|
||||
$configs = collect($routes)
|
||||
->filter(function (Route $route) use ($prefix): bool {
|
||||
@@ -95,8 +95,8 @@ class ExtractRoutesAction
|
||||
return new ExtractedRoute(
|
||||
uri: Endpoint::fromRaw(
|
||||
$route->uri(),
|
||||
routesPrefix: $this->config->get('nimbus.routes.prefix'),
|
||||
isVersioned: $this->config->get('nimbus.routes.versioned'),
|
||||
routesPrefix: $this->activeApplicationResolver->getRoutesPrefix(),
|
||||
isVersioned: $this->activeApplicationResolver->isVersioned(),
|
||||
),
|
||||
methods: $methods,
|
||||
schema: $schema,
|
||||
|
||||
189
tests/App/Modules/Config/ActiveApplicationResolverUnitTest.php
Normal file
189
tests/App/Modules/Config/ActiveApplicationResolverUnitTest.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Tests\App\Modules\Config;
|
||||
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Illuminate\Http\Request;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
||||
use Sunchayn\Nimbus\Tests\TestCase;
|
||||
|
||||
#[CoversClass(ActiveApplicationResolver::class)]
|
||||
class ActiveApplicationResolverUnitTest extends TestCase
|
||||
{
|
||||
private Repository|MockInterface $configMock;
|
||||
|
||||
private Request|MockInterface $requestMock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->configMock = Mockery::mock(Repository::class);
|
||||
$this->requestMock = Mockery::mock(Request::class);
|
||||
}
|
||||
|
||||
public function test_it_throws_exception_if_no_applications_defined(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.applications', [])->andReturn([]);
|
||||
|
||||
// Anticipate
|
||||
|
||||
$this->expectException(MisconfiguredValueException::class);
|
||||
$this->expectExceptionMessage('There are no applications defined.');
|
||||
|
||||
// Act
|
||||
|
||||
new ActiveApplicationResolver($this->configMock, $this->requestMock);
|
||||
}
|
||||
|
||||
public function test_it_throws_exception_if_default_application_is_invalid(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.applications', [])->andReturn(['p1' => []]);
|
||||
$this->requestMock->shouldReceive('cookie')->with(ActiveApplicationResolver::CURRENT_APPLICATION_COOKIE_NAME)->andReturn(null);
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.default_application')->andReturn('invalid');
|
||||
|
||||
// Anticipate
|
||||
|
||||
$this->expectException(MisconfiguredValueException::class);
|
||||
$this->expectExceptionMessage("The default application `invalid` doesn't have a matching configuration.");
|
||||
|
||||
// Act
|
||||
|
||||
new ActiveApplicationResolver($this->configMock, $this->requestMock);
|
||||
}
|
||||
|
||||
public function test_it_resolves_application_from_cookie(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$applications = [
|
||||
'p1' => ['name' => 'Project 1'],
|
||||
'p2' => ['name' => 'Project 2'],
|
||||
];
|
||||
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.applications', [])->andReturn($applications);
|
||||
$this->requestMock->shouldReceive('cookie')->with(ActiveApplicationResolver::CURRENT_APPLICATION_COOKIE_NAME)->andReturn('p2');
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.applications.p2', [])->andReturn($applications['p2']);
|
||||
|
||||
// Act
|
||||
|
||||
$resolver = new ActiveApplicationResolver($this->configMock, $this->requestMock);
|
||||
|
||||
// Assert
|
||||
|
||||
$this->assertEquals('p2', $resolver->getActiveApplicationKey());
|
||||
}
|
||||
|
||||
public function test_it_falls_back_to_default_if_cookie_is_invalid(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$applications = [
|
||||
'p1' => ['name' => 'Project 1'],
|
||||
];
|
||||
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.applications', [])->andReturn($applications);
|
||||
$this->requestMock->shouldReceive('cookie')->with(ActiveApplicationResolver::CURRENT_APPLICATION_COOKIE_NAME)->andReturn('invalid');
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.default_application')->andReturn('p1');
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.applications.p1', [])->andReturn($applications['p1']);
|
||||
|
||||
// Act
|
||||
|
||||
$resolver = new ActiveApplicationResolver($this->configMock, $this->requestMock);
|
||||
|
||||
// Assert
|
||||
|
||||
$this->assertEquals('p1', $resolver->getActiveApplicationKey());
|
||||
}
|
||||
|
||||
public function test_it_provides_specialized_getters_with_defaults(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$applications = ['main' => []];
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.applications', [])->andReturn($applications);
|
||||
$this->requestMock->shouldReceive('cookie')->with(ActiveApplicationResolver::CURRENT_APPLICATION_COOKIE_NAME)->andReturn(null);
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.default_application')->andReturn('main');
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.applications.main', [])->andReturn([]);
|
||||
$this->requestMock->shouldReceive('getSchemeAndHttpHost')->andReturn('http://localhost');
|
||||
|
||||
$resolver = new ActiveApplicationResolver($this->configMock, $this->requestMock);
|
||||
|
||||
// Act & Assert
|
||||
|
||||
$this->assertFalse($resolver->isVersioned());
|
||||
$this->assertEquals('http://localhost', $resolver->getApiBaseUrl());
|
||||
$this->assertEquals('api', $resolver->getRoutesPrefix());
|
||||
$this->assertEquals('web', $resolver->getAuthGuard());
|
||||
$this->assertNull($resolver->getSpecialAuthInjector());
|
||||
$this->assertEquals([], $resolver->getHeaders());
|
||||
}
|
||||
|
||||
public function test_it_provides_specialized_getters_from_config(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$config = [
|
||||
'routes' => [
|
||||
'versioned' => true,
|
||||
'api_base_url' => 'https://api.example.com',
|
||||
'prefix' => 'v1',
|
||||
],
|
||||
'auth' => [
|
||||
'guard' => 'api',
|
||||
'special' => ['injector' => 'SomeInjector'],
|
||||
],
|
||||
'headers' => ['X-Test' => 'value'],
|
||||
];
|
||||
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.applications', [])->andReturn(['main' => $config]);
|
||||
$this->requestMock->shouldReceive('cookie')->with(ActiveApplicationResolver::CURRENT_APPLICATION_COOKIE_NAME)->andReturn(null);
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.default_application')->andReturn('main');
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.applications.main', [])->andReturn($config);
|
||||
|
||||
$resolver = new ActiveApplicationResolver($this->configMock, $this->requestMock);
|
||||
|
||||
// Act & Assert
|
||||
|
||||
$this->assertTrue($resolver->isVersioned());
|
||||
$this->assertEquals('https://api.example.com', $resolver->getApiBaseUrl());
|
||||
$this->assertEquals('v1', $resolver->getRoutesPrefix());
|
||||
$this->assertEquals('api', $resolver->getAuthGuard());
|
||||
$this->assertEquals('SomeInjector', $resolver->getSpecialAuthInjector());
|
||||
$this->assertEquals(['X-Test' => 'value'], $resolver->getHeaders());
|
||||
}
|
||||
|
||||
public function test_it_returns_available_applications_as_json(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$applications = [
|
||||
'p1' => ['name' => 'Project 1'],
|
||||
'p2' => [], // Should fallback to key if name missing
|
||||
];
|
||||
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.applications', [])->andReturn($applications);
|
||||
$this->requestMock->shouldReceive('cookie')->with(ActiveApplicationResolver::CURRENT_APPLICATION_COOKIE_NAME)->andReturn(null);
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.default_application')->andReturn('p1');
|
||||
$this->configMock->shouldReceive('get')->with('nimbus.applications.p1', [])->andReturn($applications['p1']);
|
||||
|
||||
$resolver = new ActiveApplicationResolver($this->configMock, $this->requestMock);
|
||||
|
||||
// Act
|
||||
|
||||
$available = $resolver->getAvailableApplications();
|
||||
|
||||
// Assert
|
||||
|
||||
$this->assertJson($available);
|
||||
$this->assertEquals(['p1' => 'Project 1', 'p2' => 'p2'], json_decode($available, true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Sunchayn\Nimbus\Tests\App\Modules\Config\Exceptions;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Contracts\SpecialAuthenticationInjectorContract;
|
||||
use Sunchayn\Nimbus\Tests\TestCase;
|
||||
|
||||
#[CoversClass(MisconfiguredValueException::class)]
|
||||
class MisconfiguredValueExceptionUnitTest extends TestCase
|
||||
{
|
||||
public function test_because_special_authentication_injector_is_invalid(): void
|
||||
{
|
||||
// Act
|
||||
|
||||
$exception = MisconfiguredValueException::becauseSpecialAuthenticationInjectorIsInvalid();
|
||||
|
||||
// Assert
|
||||
|
||||
$this->assertEquals(
|
||||
'The config value for `nimbus.auth.special.injector` MUST be a class string of type <'.SpecialAuthenticationInjectorContract::class.'>',
|
||||
$exception->getMessage(),
|
||||
);
|
||||
|
||||
$this->assertEquals(MisconfiguredValueException::SPECIAL_AUTHENTICATION_INJECTOR, $exception->getCode());
|
||||
}
|
||||
|
||||
public function test_because_of_missing_dependency(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$dependency = 'some/package';
|
||||
|
||||
// Act
|
||||
|
||||
$exception = MisconfiguredValueException::becauseOfMissingDependency($dependency);
|
||||
|
||||
// Assert
|
||||
|
||||
$this->assertEquals(
|
||||
'The config value for `nimbus.auth.special.injector` is an injector that requires the following dependency <some/package>',
|
||||
$exception->getMessage(),
|
||||
);
|
||||
|
||||
$this->assertEquals(MisconfiguredValueException::MISSING_DEPENDENCIES, $exception->getCode());
|
||||
}
|
||||
|
||||
public function test_because_of_invalid_guard_injector_combination(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$suggestion = 'Try using another guard.';
|
||||
|
||||
// Act
|
||||
|
||||
$exception = MisconfiguredValueException::becauseOfInvalidGuardInjectorCombination($suggestion);
|
||||
|
||||
// Assert
|
||||
|
||||
$this->assertEquals(
|
||||
"The config value for `nimbus.auth.guard` doesn't work with the selected injector. Try using another guard.",
|
||||
$exception->getMessage(),
|
||||
);
|
||||
|
||||
$this->assertEquals(MisconfiguredValueException::INVALID_GUARD_INJECTOR_COMBINATION, $exception->getCode());
|
||||
}
|
||||
|
||||
public function test_because_default_application_is_invalid(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$key = 'missing-app';
|
||||
|
||||
// Act
|
||||
|
||||
$exception = MisconfiguredValueException::becauseDefaultApplicationIsInvalid($key);
|
||||
|
||||
// Assert
|
||||
|
||||
$this->assertEquals(
|
||||
"The default application `missing-app` doesn't have a matching configuration.",
|
||||
$exception->getMessage(),
|
||||
);
|
||||
|
||||
$this->assertEquals(MisconfiguredValueException::INVALID_DEFAULT_APPLICATION, $exception->getCode());
|
||||
}
|
||||
|
||||
public function test_because_applications_are_not_defined(): void
|
||||
{
|
||||
// Act
|
||||
|
||||
$exception = MisconfiguredValueException::becauseApplicationsAreNotDefined();
|
||||
|
||||
// Assert
|
||||
|
||||
$this->assertEquals(
|
||||
'There are no applications defined.',
|
||||
$exception->getMessage(),
|
||||
);
|
||||
|
||||
$this->assertEquals(MisconfiguredValueException::INVALID_APPLICATIONS, $exception->getCode());
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace Sunchayn\Nimbus\Tests\App\Modules\Relay\Authorization\Handlers;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Handlers\CurrentUserAuthorizationHandler;
|
||||
use Sunchayn\Nimbus\Tests\App\Modules\Relay\Authorization\Handlers\Shared\HandlesRecallerCookies;
|
||||
@@ -21,11 +22,10 @@ class CurrentUserAuthorizationHandlerFunctionalTest extends TestCase
|
||||
{
|
||||
// Arrange
|
||||
|
||||
config([
|
||||
'nimbus.auth.guard' => $guardName = fake()->word(),
|
||||
'nimbus.auth.special.injector' => DummySpecialAuthenticationInjector::class,
|
||||
]);
|
||||
|
||||
$this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class, function (MockInterface $mock) use (&$guardName) {
|
||||
$mock->shouldReceive('getAuthGuard')->andReturn($guardName = fake()->word());
|
||||
$mock->shouldReceive('getSpecialAuthInjector')->andReturn(DummySpecialAuthenticationInjector::class);
|
||||
});
|
||||
$dummyAuthenticatable = new DummyAuthenticatable(id: $userId = fake()->randomNumber());
|
||||
|
||||
$this->mockAuthManagerToUseDummyModel($userId, $dummyAuthenticatable, $guardName);
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Sunchayn\Nimbus\Tests\App\Modules\Relay\Authorization\Handlers;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||
use PHPUnit\Framework\Attributes\TestWith;
|
||||
@@ -25,11 +26,10 @@ class ImpersonateUserAuthorizationHandlerFunctionalTest extends TestCase
|
||||
{
|
||||
// Arrange
|
||||
|
||||
config([
|
||||
'nimbus.auth.guard' => $guardName = fake()->word(),
|
||||
'nimbus.auth.special.injector' => DummySpecialAuthenticationInjector::class,
|
||||
]);
|
||||
|
||||
$this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class, function (MockInterface $mock) use (&$guardName) {
|
||||
$mock->shouldReceive('getAuthGuard')->andReturn($guardName = fake()->word());
|
||||
$mock->shouldReceive('getSpecialAuthInjector')->andReturn(DummySpecialAuthenticationInjector::class);
|
||||
});
|
||||
$dummyAuthenticatable = new DummyAuthenticatable(id: $userId = fake()->randomNumber());
|
||||
|
||||
$this->mockAuthManagerToUseDummyModel($userId, $dummyAuthenticatable, $guardName);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Sunchayn\Nimbus\Tests\App\Modules\Relay\Authorization\Injectors;
|
||||
|
||||
use Illuminate\Auth\SessionGuard;
|
||||
use Illuminate\Config\Repository as ConfigRepository;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
@@ -12,6 +11,7 @@ use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors\RememberMeCookieInjector;
|
||||
use Sunchayn\Nimbus\Tests\TestCase;
|
||||
|
||||
@@ -20,7 +20,7 @@ class RememberMeCookieInjectorUnitTest extends TestCase
|
||||
{
|
||||
private Container $containerMock;
|
||||
|
||||
private ConfigRepository $configMock;
|
||||
private ActiveApplicationResolver|MockInterface $projectManagerMock;
|
||||
|
||||
private SessionGuard $authGuardMock;
|
||||
|
||||
@@ -33,7 +33,7 @@ class RememberMeCookieInjectorUnitTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
$this->containerMock = Mockery::mock(Container::class);
|
||||
$this->configMock = Mockery::mock(ConfigRepository::class);
|
||||
$this->projectManagerMock = Mockery::mock(ActiveApplicationResolver::class);
|
||||
$this->authGuardMock = Mockery::mock(SessionGuard::class);
|
||||
$this->userProviderMock = Mockery::mock(UserProvider::class);
|
||||
$this->encrypterMock = Mockery::mock(Encrypter::class);
|
||||
@@ -360,9 +360,8 @@ class RememberMeCookieInjectorUnitTest extends TestCase
|
||||
|
||||
private function instantiateInjector(Request $relayRequest): RememberMeCookieInjector
|
||||
{
|
||||
$this->configMock
|
||||
->shouldReceive('get')
|
||||
->with('nimbus.auth.guard')
|
||||
$this->projectManagerMock
|
||||
->shouldReceive('getAuthGuard')
|
||||
->andReturn('web');
|
||||
|
||||
$authManagerMock = Mockery::mock(\Illuminate\Auth\AuthManager::class);
|
||||
@@ -388,7 +387,7 @@ class RememberMeCookieInjectorUnitTest extends TestCase
|
||||
return new RememberMeCookieInjector(
|
||||
relayRequest: $relayRequest,
|
||||
container: $this->containerMock,
|
||||
configRepository: $this->configMock,
|
||||
activeApplicationResolver: $this->projectManagerMock,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
namespace Sunchayn\Nimbus\Tests\App\Modules\Relay\Authorization\Injectors;
|
||||
|
||||
use Illuminate\Config\Repository as ConfigRepository;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\Guard;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors\TymonJwtTokenInjector;
|
||||
use Sunchayn\Nimbus\Tests\TestCase;
|
||||
@@ -16,7 +16,7 @@ use Sunchayn\Nimbus\Tests\TestCase;
|
||||
#[CoversClass(TymonJwtTokenInjector::class)]
|
||||
class TymonJwtTokenInjectorUnitTest extends TestCase
|
||||
{
|
||||
private ConfigRepository $configMock;
|
||||
private ActiveApplicationResolver|MockInterface $projectManagerMock;
|
||||
|
||||
private Container $containerMock;
|
||||
|
||||
@@ -24,7 +24,7 @@ class TymonJwtTokenInjectorUnitTest extends TestCase
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->configMock = Mockery::mock(ConfigRepository::class);
|
||||
$this->projectManagerMock = Mockery::mock(ActiveApplicationResolver::class);
|
||||
$this->containerMock = Mockery::mock(Container::class);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class TymonJwtTokenInjectorUnitTest extends TestCase
|
||||
// Act
|
||||
|
||||
new TymonJwtTokenInjector(
|
||||
configRepository: $this->configMock,
|
||||
activeApplicationResolver: $this->projectManagerMock,
|
||||
container: $this->containerMock,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace Sunchayn\Nimbus\Tests\App\Modules\Routes\Actions;
|
||||
|
||||
use Illuminate\Config\Repository;
|
||||
use Mockery\MockInterface;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use Sunchayn\Nimbus\Modules\Config\GlobalHeaderGeneratorTypeEnum;
|
||||
use Sunchayn\Nimbus\Modules\Routes\Actions\BuildGlobalHeadersAction;
|
||||
@@ -23,15 +21,9 @@ class BuildGlobalHeadersActionFunctionalTest extends TestCase
|
||||
'X-Custom-Header' => '::value::',
|
||||
];
|
||||
|
||||
$this->mock(
|
||||
Repository::class,
|
||||
function (MockInterface $mock) use ($globalHeadersConfig) {
|
||||
$mock
|
||||
->shouldReceive('get')
|
||||
->with('nimbus.headers')
|
||||
->andReturn($globalHeadersConfig);
|
||||
},
|
||||
);
|
||||
$this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class, function (\Mockery\MockInterface $mock) use ($globalHeadersConfig) {
|
||||
$mock->shouldReceive('getHeaders')->andReturn($globalHeadersConfig);
|
||||
});
|
||||
|
||||
$action = resolve(BuildGlobalHeadersAction::class);
|
||||
|
||||
@@ -72,15 +64,9 @@ class BuildGlobalHeadersActionFunctionalTest extends TestCase
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$this->mock(
|
||||
Repository::class,
|
||||
function (MockInterface $mock) {
|
||||
$mock
|
||||
->shouldReceive('get')
|
||||
->with('nimbus.headers')
|
||||
->andReturn([]);
|
||||
},
|
||||
);
|
||||
$this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class, function (\Mockery\MockInterface $mock) {
|
||||
$mock->shouldReceive('getHeaders')->andReturn([]);
|
||||
});
|
||||
|
||||
$action = resolve(BuildGlobalHeadersAction::class);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Sunchayn\Nimbus\Tests\App\Modules\Routes\Services;
|
||||
|
||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||
@@ -47,9 +46,9 @@ class RouteExtractorServiceFunctionalTest extends TestCase
|
||||
{
|
||||
// Anticipate
|
||||
|
||||
$this->mock(ConfigRepository::class, function (MockInterface $mock) {
|
||||
$mock->shouldReceive('get')->with('nimbus.routes.prefix')->andReturn('api');
|
||||
$mock->shouldReceive('get')->with('nimbus.routes.versioned')->andReturn(fake()->boolean());
|
||||
$this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class, function (MockInterface $mock) {
|
||||
$mock->shouldReceive('getRoutesPrefix')->andReturn('api');
|
||||
$mock->shouldReceive('isVersioned')->andReturn($this->isVersioned = fake()->boolean());
|
||||
});
|
||||
|
||||
$routeFactoryMock = $this->mock(ExtractableRouteFactory::class, function (MockInterface $mock) {
|
||||
@@ -205,19 +204,19 @@ class RouteExtractorServiceFunctionalTest extends TestCase
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$config = $this->mock(ConfigRepository::class);
|
||||
|
||||
RouteFacade::post('/custom/test', fn () => response()->json(['test' => true]))
|
||||
->name('custom.test');
|
||||
|
||||
$activeApplicationResolverMock = $this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class);
|
||||
|
||||
$routeExtractorService = resolve(ExtractRoutesAction::class);
|
||||
|
||||
$routes = RouteFacade::getRoutes()->getRoutes();
|
||||
|
||||
// Anticipate
|
||||
|
||||
$config->shouldReceive('get')->with('nimbus.routes.prefix')->andReturn('custom');
|
||||
$config->shouldReceive('get')->with('nimbus.routes.versioned')->andReturnFalse();
|
||||
$activeApplicationResolverMock->shouldReceive('getRoutesPrefix')->andReturn('custom');
|
||||
$activeApplicationResolverMock->shouldReceive('isVersioned')->andReturn(false);
|
||||
|
||||
// Act
|
||||
|
||||
@@ -239,9 +238,9 @@ class RouteExtractorServiceFunctionalTest extends TestCase
|
||||
{
|
||||
// Anticipate
|
||||
|
||||
$this->mock(ConfigRepository::class, function (MockInterface $mock) {
|
||||
$mock->shouldReceive('get')->with('nimbus.routes.prefix')->andReturn('api');
|
||||
$mock->shouldReceive('get')->with('nimbus.routes.versioned')->andReturn(fake()->boolean());
|
||||
$this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class, function (MockInterface $mock) {
|
||||
$mock->shouldReceive('getRoutesPrefix')->andReturn('api');
|
||||
$mock->shouldReceive('isVersioned')->andReturn(fake()->boolean());
|
||||
});
|
||||
|
||||
$dummyFailingException = new RuntimeException(message: $dummyFailingExceptionMessage = fake()->sentence());
|
||||
|
||||
@@ -114,7 +114,7 @@ test("Dump and Die visualization sanity checklist", async ({ page }) => {
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.locator("#reka-collapsible-content-v-113"),
|
||||
page.locator("#reka-collapsible-content-v-119"),
|
||||
).toMatchAriaSnapshot(`- text: "0: \\"/\\" (1) 1: \\"\\\\\\" (1)"`);
|
||||
|
||||
// dump #3 (runtime object)
|
||||
|
||||
@@ -16,7 +16,6 @@ use Sunchayn\Nimbus\Modules\Routes\Exceptions\RouteExtractionException;
|
||||
use Sunchayn\Nimbus\Modules\Routes\Services\IgnoredRoutesService;
|
||||
use Sunchayn\Nimbus\Tests\TestCase;
|
||||
|
||||
#[CoversClass(NimbusIndexController::class)]
|
||||
#[CoversClass(NimbusIndexController::class)]
|
||||
class NimbusIndexTest extends TestCase
|
||||
{
|
||||
@@ -32,16 +31,20 @@ class NimbusIndexTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
// Mock Vite to prevent asset loading issues
|
||||
Vite::shouldReceive('useBuildDirectory')->with('/vendor/nimbus')->once();
|
||||
Vite::shouldReceive('useHotFile')->with(base_path('/vendor/sunchayn/nimbus/resources/dist/hot'))->once();
|
||||
Vite::shouldReceive('__invoke')->andReturn('<script src="/nimbus/app.js"></script>');
|
||||
Vite::shouldReceive('useBuildDirectory')->with('/vendor/nimbus')->atMost()->once();
|
||||
Vite::shouldReceive('useHotFile')->with(base_path('/vendor/sunchayn/nimbus/resources/dist/hot'))->atMost()->once();
|
||||
Vite::shouldReceive('__invoke')->andReturn('<script src="/nimbus/app.js"></script>')->atMost()->once();
|
||||
}
|
||||
|
||||
#[DataProvider('indexRouteProvider')]
|
||||
public function test_it_loads_view_correctly(string $uri): void
|
||||
public function test_it_loads_view_correctly(string $uri, ?string $applicationCookie = null, string $expectedApplicationKey = 'main'): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
if ($applicationCookie) {
|
||||
$this->withCookie(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::CURRENT_APPLICATION_COOKIE_NAME, $applicationCookie);
|
||||
}
|
||||
|
||||
$disableThirdPartyUiActionSpy = $this->spy(DisableThirdPartyUiAction::class);
|
||||
$ignoreRoutesServiceSpy = $this->spy(IgnoredRoutesService::class);
|
||||
$buildGlobalHeadersActionMock = $this->mock(BuildGlobalHeadersAction::class);
|
||||
@@ -54,11 +57,13 @@ class NimbusIndexTest extends TestCase
|
||||
|
||||
$buildCurrentUserActionMock->shouldReceive('execute')->andReturn(['::current-user::']);
|
||||
|
||||
$extractedRoutesCollectionStub = new class extends ExtractedRoutesCollection
|
||||
$extractedRoutesCollectionStub = new class($expectedApplicationKey) extends ExtractedRoutesCollection
|
||||
{
|
||||
public function __construct(private string $key) {}
|
||||
|
||||
public function toFrontendArray(): array
|
||||
{
|
||||
return ['::extracted-routes::'];
|
||||
return ["routes for $this->key"];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -74,12 +79,16 @@ class NimbusIndexTest extends TestCase
|
||||
|
||||
$response->assertViewIs('nimbus::app');
|
||||
|
||||
$response->assertViewHas('routes', ['::extracted-routes::']);
|
||||
$response->assertViewHas('routes', ["routes for $expectedApplicationKey"]);
|
||||
|
||||
$response->assertViewHas('headers', ['::global-headers::']);
|
||||
|
||||
$response->assertViewHas('currentUser', ['::current-user::']);
|
||||
|
||||
$response->assertViewHas('activeApplicationResolver', function ($resolver) use ($expectedApplicationKey) {
|
||||
return $resolver->getActiveApplicationKey() === $expectedApplicationKey;
|
||||
});
|
||||
|
||||
$disableThirdPartyUiActionSpy->shouldHaveReceived('execute')->once();
|
||||
|
||||
$ignoreRoutesServiceSpy->shouldNotHaveReceived('execute');
|
||||
@@ -87,12 +96,22 @@ class NimbusIndexTest extends TestCase
|
||||
|
||||
public static function indexRouteProvider(): Generator
|
||||
{
|
||||
yield 'index route handles route extraction exception' => [
|
||||
yield 'index route with default application' => [
|
||||
'uri' => '/',
|
||||
'applicationCookie' => null,
|
||||
'expectedApplicationKey' => 'main',
|
||||
];
|
||||
|
||||
yield 'index route with application from cookie' => [
|
||||
'uri' => '/',
|
||||
'applicationCookie' => 'other',
|
||||
'expectedApplicationKey' => 'other',
|
||||
];
|
||||
|
||||
yield 'index route with catch all parameter' => [
|
||||
'uri' => '/some/deep/path',
|
||||
'applicationCookie' => null,
|
||||
'expectedApplicationKey' => 'main',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -183,4 +202,24 @@ class NimbusIndexTest extends TestCase
|
||||
|
||||
$response->assertViewHas(['routes', 'headers', 'currentUser']);
|
||||
}
|
||||
|
||||
public function test_it_redirects_to_new_application(): void
|
||||
{
|
||||
// Arrange
|
||||
|
||||
$url = route('nimbus.index', ['application' => 'new-app']);
|
||||
|
||||
// Act
|
||||
|
||||
$response = $this->get($url);
|
||||
|
||||
// Assert
|
||||
|
||||
$response->assertRedirect(route('nimbus.index').'?');
|
||||
|
||||
$response->assertCookie(
|
||||
\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::CURRENT_APPLICATION_COOKIE_NAME,
|
||||
'new-app',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,20 +13,36 @@ class TestCase extends BaseTestCase
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'nimbus.prefix' => 'nimbus',
|
||||
'nimbus.routes.prefix' => 'api',
|
||||
'nimbus.routes.versioned' => false,
|
||||
'nimbus.headers' => [
|
||||
'x-request-id' => 'uuid',
|
||||
'x-session-id' => 'uuid',
|
||||
],
|
||||
'force',
|
||||
]);
|
||||
|
||||
Http::preventStrayRequests();
|
||||
}
|
||||
|
||||
protected function getEnvironmentSetUp($app): void
|
||||
{
|
||||
$app['config']->set('app.key', 'base64:'.base64_encode(random_bytes(32)));
|
||||
$app['config']->set('nimbus.prefix', 'nimbus');
|
||||
$app['config']->set('nimbus.default_application', 'main');
|
||||
$app['config']->set('nimbus.applications', [
|
||||
'main' => [
|
||||
'name' => 'Main Application',
|
||||
'routes' => [
|
||||
'prefix' => 'api',
|
||||
'versioned' => false,
|
||||
],
|
||||
'headers' => [
|
||||
'x-request-id' => 'uuid',
|
||||
'x-session-id' => 'uuid',
|
||||
],
|
||||
],
|
||||
'other' => [
|
||||
'name' => 'Other Application',
|
||||
'routes' => [
|
||||
'prefix' => 'other-api',
|
||||
'versioned' => true,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if ($container = Mockery::getContainer()) {
|
||||
|
||||
@@ -90,10 +90,12 @@ The Route Explorer automatically discovers your Laravel API routes and organizes
|
||||

|
||||
|
||||
**Features:**
|
||||
- Routes are grouped by resource (e.g., all user-related endpoints together).
|
||||
- Each route shows its HTTP methods.
|
||||
- Click any route to load it in the Request Builder.
|
||||
- Routes are extracted based on your configured API prefix.
|
||||
- **Search**: Quickly filter routes by endpoint path.
|
||||
- **Application Switcher**: Switch between multiple API applications (e.g., Rest API, CMS API) if configured.
|
||||
- **Version Selector**: If the active application is versioned, a version picker appears inline with the application name.
|
||||
- **Resource Groups**: Routes are automatically grouped by resource (e.g., `users`, `products`).
|
||||
- **HTTP Methods**: Each route explicitly shows its supported methods.
|
||||
- **Quick Load**: Click any route to immediately load it into the Request Builder.
|
||||
|
||||
### Request Builder
|
||||
|
||||
@@ -359,16 +361,44 @@ return [
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description | Default | Example |
|
||||
|--------|--------------|----------|----------|
|
||||
| **`prefix`** | The URI segment under which Nimbus is accessible. | `'nimbus'` | `'api-client'` |
|
||||
| **`allowed_envs`** | Environments where Nimbus is enabled. Avoid production for security reasons. | `['local', 'staging']` | `['testing', 'local']` |
|
||||
| **`routes.prefix`** | The base path used to detect application routes. Only routes starting with this prefix are analyzed. | `'api'` | `'api/v1'` |
|
||||
| **`routes.versioned`** | Enables version parsing for routes like `/api/v1/...`. | `false` | `true` |
|
||||
| **`routes.api_base_url`** | The base URL used when Nimbus relays API requests from the UI. Useful when the API runs on a different domain or port. If set to null, Nimbus will default to the same host and scheme as the incoming request.| null | `http://127.0.0.1:8001` |
|
||||
| **`auth.guard`** | The Laravel guard used for the API requests authentication. | `'api'` | `'web'` |
|
||||
| **`auth.special.injector`** | Injector class used to attach authentication credentials to outgoing requests. Must implement `SpecialAuthenticationInjectorContract`. | `RememberMeCookieInjector::class` | `TymonJwtTokenInjector::class` |
|
||||
| **`headers`** | Global headers applied to all outgoing requests. Supports static values or enum generators. | `[]` | `['x-request-id' => GlobalHeaderGeneratorTypeEnum::UUID]` |
|
||||
| Option | Description | Default | Example |
|
||||
|--------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------|-----------------------------------------------------------|
|
||||
| **`prefix`** | The URI segment under which Nimbus is accessible. | `'nimbus'` | `'api-client'` |
|
||||
| **`allowed_envs`** | Environments where Nimbus is enabled. Avoid production for security reasons. | `['local', 'staging']` | `['testing', 'local']` |
|
||||
| **`default_application`** | The base default application to load when no other application is found in the storage. | n/a | `rest-api` |
|
||||
| **`applications.*.routes.prefix`** | The base path used to detect application routes. Only routes starting with this prefix are analyzed. | `'api'` | `'api/v1'` |
|
||||
| **`applications.*.routes.versioned`** | Enables version parsing for routes like `/api/v1/...`. | `false` | `true` |
|
||||
| **`applications.*.routes.api_base_url`** | The base URL used when Nimbus relays API requests from the UI. Useful when the API runs on a different domain or port. If set to null, Nimbus will default to the same host and scheme as the incoming request. | null | `http://127.0.0.1:8001` |
|
||||
| **`applications.*.auth.guard`** | The Laravel guard used for the API requests authentication. | `'api'` | `'web'` |
|
||||
| **`applications.*.auth.special.injector`** | Injector class used to attach authentication credentials to outgoing requests. Must implement `SpecialAuthenticationInjectorContract`. | `RememberMeCookieInjector::class` | `TymonJwtTokenInjector::class` |
|
||||
| **`headers`** | Global headers applied to all outgoing requests. Supports static values or enum generators. | `[]` | `['x-request-id' => GlobalHeaderGeneratorTypeEnum::UUID]` |
|
||||
|
||||
### Multi-Application Support
|
||||
|
||||
Nimbus allows you to define multiple distinct application in your configuration. This is ideal for projects with multiple APIs like a REST api + CMS APIs, or different microservices within the same monolith.
|
||||
|
||||
```php
|
||||
'applications' => [
|
||||
'main' => [
|
||||
'name' => 'Public API',
|
||||
'routes' => [
|
||||
'prefix' => 'api/v1',
|
||||
'versioned' => false,
|
||||
],
|
||||
],
|
||||
'admin' => [
|
||||
'name' => 'Admin API',
|
||||
'routes' => [
|
||||
'prefix' => 'api/admin',
|
||||
'versioned' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
When multiple applications are defined, a Project Switcher appears in the sidebar, allowing you to quickly toggle between them.
|
||||
|
||||

|
||||
|
||||
#### Special Authentication Modes
|
||||
|
||||
|
||||
BIN
wiki/user-guide/assets/applications-switcher.png
Normal file
BIN
wiki/user-guide/assets/applications-switcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 343 KiB After Width: | Height: | Size: 200 KiB |
Reference in New Issue
Block a user