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',
|
'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
|
| Allowed Environments
|
||||||
@@ -31,144 +43,160 @@ return [
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Route Configuration
|
| Application Configurations
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| These options control how Nimbus identifies and registers application
|
| Defines the different applications that Nimbus supports. Each application
|
||||||
| routes. The route configuration determines which endpoints will be
|
| can have its own route, authentication, and headers configuration.
|
||||||
| 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.
|
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'auth' => [
|
'applications' => [
|
||||||
|
'main' => [
|
||||||
/*
|
'name' => 'Main',
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| 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
|
| Route Configuration
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| Defines the injector class used to modify outgoing requests with
|
| These options control how Nimbus identifies and registers application
|
||||||
| authentication credentials. The class must implement the
|
| routes. The route configuration determines which endpoints will be
|
||||||
| `SpecialAuthenticationInjectorContract` interface.
|
| analyzed, displayed, or interacted with by Nimbus.
|
||||||
|
|
|
||||||
| 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,
|
'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"
|
v-bind="forwardedProps"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
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,
|
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,
|
AppSidebarMenu,
|
||||||
AppSidebarRail,
|
AppSidebarRail,
|
||||||
} from '@/components/base/sidebar';
|
} from '@/components/base/sidebar';
|
||||||
|
import ApplicationSwitcher from '@/components/domain/RoutesExplorer/ApplicationSwitcher.vue';
|
||||||
import RouteExplorerHeader from '@/components/domain/RoutesExplorer/RouteExplorerHeader.vue';
|
import RouteExplorerHeader from '@/components/domain/RoutesExplorer/RouteExplorerHeader.vue';
|
||||||
import RouteExplorerVersionSelector from '@/components/domain/RoutesExplorer/RouteExplorerVersionSelector.vue';
|
import RouteExplorerVersionSelector from '@/components/domain/RoutesExplorer/RouteExplorerVersionSelector.vue';
|
||||||
import RoutesList from '@/components/domain/RoutesExplorer/RoutesList/RoutesList.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 { useConfigStore } from '@/stores';
|
||||||
import { uniquePersistenceKey } from '@/utils/stores';
|
import { uniquePersistenceKey } from '@/utils/stores';
|
||||||
import { useStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
import { computed } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Props.
|
* Props.
|
||||||
@@ -32,9 +33,22 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const search = useStorage(uniquePersistenceKey('routes-explorer-search-keyword'), '');
|
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.
|
* Computed.
|
||||||
@@ -77,6 +91,10 @@ const filteredRoutes = computed(() => {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
|
|
||||||
|
const hasMultipleApplications = computed(
|
||||||
|
() => Object.keys(configStore.applications).length > 1,
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -89,11 +107,21 @@ const configStore = useConfigStore();
|
|||||||
:disabled="routesInVersion.length === 0"
|
: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"
|
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
|
<div class="h-sub-toolbar flex items-center overflow-hidden border-b">
|
||||||
v-if="configStore.isVersioned && versions.length"
|
<ApplicationSwitcher v-if="hasMultipleApplications" class="flex-1" />
|
||||||
v-model="currentVersion"
|
<div
|
||||||
:versions="versions"
|
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>
|
</div>
|
||||||
<AppSidebarContent>
|
<AppSidebarContent>
|
||||||
<AppSidebarGroup class="p-0">
|
<AppSidebarGroup class="p-0">
|
||||||
|
|||||||
@@ -15,14 +15,19 @@ const model = defineModel<string>();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppSelect v-model="model" class="flex items-center text-sm">
|
<AppSelect v-model="model" class="flex items-center">
|
||||||
<AppSelectTrigger
|
<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" />
|
<AppSelectValue placeholder="Select API Version" />
|
||||||
</AppSelectTrigger>
|
</AppSelectTrigger>
|
||||||
<AppSelectContent>
|
<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 }}
|
{{ value }}
|
||||||
</AppSelectItem>
|
</AppSelectItem>
|
||||||
</AppSelectContent>
|
</AppSelectContent>
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
const currentUser = window.Nimbus?.currentUser
|
const currentUser = window.Nimbus?.currentUser
|
||||||
? JSON.parse(window.Nimbus.currentUser as string)
|
? JSON.parse(window.Nimbus.currentUser as string)
|
||||||
: null;
|
: null;
|
||||||
|
const applications: Record<string, string> = window.Nimbus?.applications
|
||||||
|
? JSON.parse(window.Nimbus.applications as string)
|
||||||
|
: {};
|
||||||
|
const activeApplication = window.Nimbus?.activeApplication || null;
|
||||||
|
|
||||||
// Derived values
|
// Derived values
|
||||||
const isLoggedIn = currentUser !== null;
|
const isLoggedIn = currentUser !== null;
|
||||||
@@ -33,5 +37,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
isVersioned,
|
isVersioned,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
userId,
|
userId,
|
||||||
|
applications,
|
||||||
|
activeApplication,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -672,8 +672,6 @@ describe('SingleDumpRenderer', () => {
|
|||||||
// Should show [] for empty array
|
// Should show [] for empty array
|
||||||
const content = screen.getAllByTestId('collapsible-trigger')[1];
|
const content = screen.getAllByTestId('collapsible-trigger')[1];
|
||||||
|
|
||||||
console.log(content);
|
|
||||||
|
|
||||||
expect(content.textContent).toContain('[]');
|
expect(content.textContent).toContain('[]');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ const mockRoutesStore: {
|
|||||||
const mockConfigStore = {
|
const mockConfigStore = {
|
||||||
apiUrl: 'https://api.example.com',
|
apiUrl: 'https://api.example.com',
|
||||||
headers: [],
|
headers: [],
|
||||||
|
applications: {},
|
||||||
|
isVersioned: false,
|
||||||
|
activeApplication: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockValueGeneratorStore = {
|
const mockValueGeneratorStore = {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ describe('useConfigStore', () => {
|
|||||||
currentUser: JSON.stringify({ id: 99 }),
|
currentUser: JSON.stringify({ id: 99 }),
|
||||||
routes: '',
|
routes: '',
|
||||||
routeExtractorException: null,
|
routeExtractorException: null,
|
||||||
|
applications: JSON.stringify({ main: 'Main API' }),
|
||||||
|
activeApplication: 'main',
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = useConfigStore();
|
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;
|
isVersioned: boolean;
|
||||||
routeExtractorException: string | null;
|
routeExtractorException: string | null;
|
||||||
currentUser: string | null;
|
currentUser: string | null;
|
||||||
|
applications: string | null;
|
||||||
|
activeApplication: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -4,20 +4,20 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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/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="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="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') }}" />
|
<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" />
|
<meta name="apple-mobile-web-app-title" content="Nimbus"/>
|
||||||
|
|
||||||
<title>{{ config('app.name', 'Laravel') }} - Nimbus</title>
|
<title>{{ config('app.name', 'Laravel') }} - Nimbus</title>
|
||||||
|
|
||||||
<!-- Fonts -->
|
<!-- Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
<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>
|
<script>
|
||||||
(function() {
|
(function () {
|
||||||
const appearance = '{{ $appearance ?? "system" }}';
|
const appearance = '{{ $appearance ?? "system" }}';
|
||||||
|
|
||||||
if (appearance === 'system') {
|
if (appearance === 'system') {
|
||||||
@@ -30,21 +30,24 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
@php
|
@php
|
||||||
$config = \Illuminate\Support\Js::from([
|
/** @var \Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver $activeApplicationResolver */
|
||||||
'basePath' => rtrim(\Illuminate\Support\Str::start(config('nimbus.prefix'), '/'), '/'),
|
$config = \Illuminate\Support\Js::from([
|
||||||
'routes' => isset($routes) ? json_encode($routes) : null,
|
'basePath' => rtrim(\Illuminate\Support\Str::start(config('nimbus.prefix'), '/'), '/'),
|
||||||
'headers' => isset($headers) ? json_encode($headers) : null,
|
'routes' => isset($routes) ? json_encode($routes) : null,
|
||||||
'routeExtractorException' => isset($routeExtractorException) ? json_encode($routeExtractorException) : null,
|
'headers' => isset($headers) ? json_encode($headers) : null,
|
||||||
'isVersioned' => config('nimbus.routes.versioned'),
|
'routeExtractorException' => isset($routeExtractorException) ? json_encode($routeExtractorException) : null,
|
||||||
'apiBaseUrl' => config('nimbus.routes.api_base_url', request()->getSchemeAndHttpHost()),
|
'isVersioned' => $activeApplicationResolver->isVersioned(),
|
||||||
'currentUser' => isset($currentUser) ? json_encode($currentUser) : null,
|
'apiBaseUrl' => $activeApplicationResolver->getApiBaseUrl(),
|
||||||
]);
|
'currentUser' => isset($currentUser) ? json_encode($currentUser) : null,
|
||||||
|
'applications' => $activeApplicationResolver->getAvailableApplications(),
|
||||||
|
'activeApplication' => $activeApplicationResolver->getActiveApplicationKey(),
|
||||||
|
]);
|
||||||
|
|
||||||
$configTag = new \Illuminate\Support\HtmlString(<<<HTML
|
$configTag = new \Illuminate\Support\HtmlString(<<<HTML
|
||||||
<script type="module">
|
<script type="module">
|
||||||
window.Nimbus = {$config};
|
window.Nimbus = {$config};
|
||||||
</script>
|
</script>
|
||||||
HTML);
|
HTML);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
{{ $configTag }}
|
{{ $configTag }}
|
||||||
@@ -52,6 +55,6 @@
|
|||||||
@vite(['resources/css/app.css', 'resources/js/app/app.ts'])
|
@vite(['resources/css/app.css', 'resources/js/app/app.ts'])
|
||||||
</head>
|
</head>
|
||||||
<body class="font-sans antialiased">
|
<body class="font-sans antialiased">
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use Illuminate\Http\RedirectResponse;
|
|||||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||||
use Illuminate\Support\Facades\Vite;
|
use Illuminate\Support\Facades\Vite;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||||
use Sunchayn\Nimbus\Modules\Routes\Actions;
|
use Sunchayn\Nimbus\Modules\Routes\Actions;
|
||||||
use Sunchayn\Nimbus\Modules\Routes\Exceptions\RouteExtractionException;
|
use Sunchayn\Nimbus\Modules\Routes\Exceptions\RouteExtractionException;
|
||||||
|
|
||||||
@@ -20,9 +21,16 @@ class NimbusIndexController
|
|||||||
Actions\BuildGlobalHeadersAction $buildGlobalHeadersAction,
|
Actions\BuildGlobalHeadersAction $buildGlobalHeadersAction,
|
||||||
Actions\BuildCurrentUserAction $buildCurrentUserAction,
|
Actions\BuildCurrentUserAction $buildCurrentUserAction,
|
||||||
Actions\DisableThirdPartyUiAction $disableThirdPartyUiAction,
|
Actions\DisableThirdPartyUiAction $disableThirdPartyUiAction,
|
||||||
|
ActiveApplicationResolver $activeApplicationResolver,
|
||||||
): Renderable|RedirectResponse {
|
): Renderable|RedirectResponse {
|
||||||
$disableThirdPartyUiAction->execute();
|
$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::useBuildDirectory('/vendor/nimbus');
|
||||||
Vite::useHotFile(base_path('/vendor/sunchayn/nimbus/resources/dist/hot'));
|
Vite::useHotFile(base_path('/vendor/sunchayn/nimbus/resources/dist/hot'));
|
||||||
|
|
||||||
@@ -42,6 +50,7 @@ class NimbusIndexController
|
|||||||
} catch (RouteExtractionException $routeExtractionException) {
|
} catch (RouteExtractionException $routeExtractionException) {
|
||||||
return view(self::VIEW_NAME, [ // @phpstan-ignore-line it cannot find the view.
|
return view(self::VIEW_NAME, [ // @phpstan-ignore-line it cannot find the view.
|
||||||
'routeExtractorException' => $this->renderExtractorException($routeExtractionException),
|
'routeExtractorException' => $this->renderExtractorException($routeExtractionException),
|
||||||
|
'activeApplicationResolver' => $activeApplicationResolver,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +58,7 @@ class NimbusIndexController
|
|||||||
'routes' => $routes->toFrontendArray(),
|
'routes' => $routes->toFrontendArray(),
|
||||||
'headers' => $buildGlobalHeadersAction->execute(),
|
'headers' => $buildGlobalHeadersAction->execute(),
|
||||||
'currentUser' => $buildCurrentUserAction->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_GUARD_INJECTOR_COMBINATION = 3;
|
||||||
|
|
||||||
|
public const INVALID_DEFAULT_APPLICATION = 4;
|
||||||
|
|
||||||
|
public const INVALID_APPLICATIONS = 5;
|
||||||
|
|
||||||
public static function becauseSpecialAuthenticationInjectorIsInvalid(): self
|
public static function becauseSpecialAuthenticationInjectorIsInvalid(): self
|
||||||
{
|
{
|
||||||
return new self(
|
return new self(
|
||||||
@@ -39,4 +43,20 @@ class MisconfiguredValueException extends Exception
|
|||||||
code: self::INVALID_GUARD_INJECTOR_COMBINATION,
|
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;
|
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Concerns;
|
||||||
|
|
||||||
use Illuminate\Container\Container;
|
use Illuminate\Container\Container;
|
||||||
use Illuminate\Contracts\Config\Repository;
|
|
||||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||||
|
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||||
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
||||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Contracts\SpecialAuthenticationInjectorContract;
|
use Sunchayn\Nimbus\Modules\Relay\Authorization\Contracts\SpecialAuthenticationInjectorContract;
|
||||||
|
|
||||||
@@ -14,10 +14,10 @@ trait UsesSpecialAuthenticationInjector
|
|||||||
* @throws BindingResolutionException
|
* @throws BindingResolutionException
|
||||||
* @throws MisconfiguredValueException
|
* @throws MisconfiguredValueException
|
||||||
*/
|
*/
|
||||||
public function getInjector(Container $container, Repository $configRepository): SpecialAuthenticationInjectorContract
|
public function getInjector(Container $container, ActiveApplicationResolver $activeApplicationResolver): SpecialAuthenticationInjectorContract
|
||||||
{
|
{
|
||||||
/** @var ?class-string $injectorClass */
|
/** @var ?class-string $injectorClass */
|
||||||
$injectorClass = $configRepository->get('nimbus.auth.special.injector');
|
$injectorClass = $this->projectManager->getSpecialAuthInjector();
|
||||||
|
|
||||||
if ($injectorClass === null) {
|
if ($injectorClass === null) {
|
||||||
throw MisconfiguredValueException::becauseSpecialAuthenticationInjectorIsInvalid();
|
throw MisconfiguredValueException::becauseSpecialAuthenticationInjectorIsInvalid();
|
||||||
|
|||||||
@@ -2,15 +2,16 @@
|
|||||||
|
|
||||||
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Handlers;
|
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Handlers;
|
||||||
|
|
||||||
use Illuminate\Config\Repository as ConfigRepository;
|
|
||||||
use Illuminate\Container\Container;
|
use Illuminate\Container\Container;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Contracts\Auth\UserProvider;
|
use Illuminate\Contracts\Auth\UserProvider;
|
||||||
|
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
||||||
use Illuminate\Http\Client\PendingRequest;
|
use Illuminate\Http\Client\PendingRequest;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Session\SessionManager;
|
use Illuminate\Session\SessionManager;
|
||||||
use Illuminate\Session\Store;
|
use Illuminate\Session\Store;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Concerns\UsesSpecialAuthenticationInjector;
|
use Sunchayn\Nimbus\Modules\Relay\Authorization\Concerns\UsesSpecialAuthenticationInjector;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +31,7 @@ class CurrentUserAuthorizationHandler implements AuthorizationHandler
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Request $relayRequest,
|
private readonly Request $relayRequest,
|
||||||
private readonly Container $container,
|
private readonly Container $container,
|
||||||
|
private readonly ActiveApplicationResolver $projectManager,
|
||||||
private readonly ConfigRepository $configRepository,
|
private readonly ConfigRepository $configRepository,
|
||||||
) {
|
) {
|
||||||
$this->userProvider = $this->resolveUserProvider();
|
$this->userProvider = $this->resolveUserProvider();
|
||||||
@@ -44,7 +46,7 @@ class CurrentUserAuthorizationHandler implements AuthorizationHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $this
|
return $this
|
||||||
->getInjector($this->container, $this->configRepository)
|
->getInjector($this->container, $this->projectManager)
|
||||||
->attach($pendingRequest, $user);
|
->attach($pendingRequest, $user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,9 +57,7 @@ class CurrentUserAuthorizationHandler implements AuthorizationHandler
|
|||||||
{
|
{
|
||||||
$authManager = $this->container->get('auth');
|
$authManager = $this->container->get('auth');
|
||||||
|
|
||||||
$guardName = $this->configRepository->get('nimbus.auth.guard');
|
return $authManager->guard($this->projectManager->getAuthGuard())->getProvider();
|
||||||
|
|
||||||
return $authManager->guard($guardName)->getProvider();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,7 +86,7 @@ class CurrentUserAuthorizationHandler implements AuthorizationHandler
|
|||||||
$session->setId(id: $this->extractSessionIdFromCookie($sessionCookie));
|
$session->setId(id: $this->extractSessionIdFromCookie($sessionCookie));
|
||||||
$session->start();
|
$session->start();
|
||||||
|
|
||||||
$tokenPrefix = 'login_'.$this->configRepository->get('nimbus.auth.guard');
|
$tokenPrefix = 'login_'.($this->projectManager->getAuthGuard());
|
||||||
|
|
||||||
$userId = Arr::first(
|
$userId = Arr::first(
|
||||||
$session->all(),
|
$session->all(),
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Handlers;
|
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Handlers;
|
||||||
|
|
||||||
use Illuminate\Config\Repository as ConfigRepository;
|
|
||||||
use Illuminate\Container\Container;
|
use Illuminate\Container\Container;
|
||||||
use Illuminate\Contracts\Auth\UserProvider;
|
use Illuminate\Contracts\Auth\UserProvider;
|
||||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||||
use Illuminate\Http\Client\PendingRequest;
|
use Illuminate\Http\Client\PendingRequest;
|
||||||
|
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||||
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
||||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Concerns\UsesSpecialAuthenticationInjector;
|
use Sunchayn\Nimbus\Modules\Relay\Authorization\Concerns\UsesSpecialAuthenticationInjector;
|
||||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Exceptions\InvalidAuthorizationValueException;
|
use Sunchayn\Nimbus\Modules\Relay\Authorization\Exceptions\InvalidAuthorizationValueException;
|
||||||
@@ -20,7 +20,7 @@ class ImpersonateUserAuthorizationHandler implements AuthorizationHandler
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly int $userId,
|
public readonly int $userId,
|
||||||
private readonly Container $container,
|
private readonly Container $container,
|
||||||
private readonly ConfigRepository $configRepository,
|
private readonly ActiveApplicationResolver $projectManager,
|
||||||
) {
|
) {
|
||||||
if ($userId <= 0) {
|
if ($userId <= 0) {
|
||||||
throw InvalidAuthorizationValueException::becauseUserIsNotFound();
|
throw InvalidAuthorizationValueException::becauseUserIsNotFound();
|
||||||
@@ -28,7 +28,7 @@ class ImpersonateUserAuthorizationHandler implements AuthorizationHandler
|
|||||||
|
|
||||||
$this->userProvider = $this
|
$this->userProvider = $this
|
||||||
->container->get('auth')
|
->container->get('auth')
|
||||||
->guard(name: config('nimbus.auth.guard'))
|
->guard(name: $this->projectManager->getAuthGuard())
|
||||||
->getProvider();
|
->getProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ class ImpersonateUserAuthorizationHandler implements AuthorizationHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
return $this
|
return $this
|
||||||
->getInjector($this->container, $this->configRepository)
|
->getInjector($this->container, $this->projectManager)
|
||||||
->attach($pendingRequest, $user);
|
->attach($pendingRequest, $user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors;
|
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors;
|
||||||
|
|
||||||
use Illuminate\Config\Repository as ConfigRepository;
|
|
||||||
use Illuminate\Container\Container;
|
use Illuminate\Container\Container;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Contracts\Auth\Guard;
|
use Illuminate\Contracts\Auth\Guard;
|
||||||
@@ -15,6 +14,7 @@ use Illuminate\Http\Request;
|
|||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Psr\Container\ContainerExceptionInterface;
|
use Psr\Container\ContainerExceptionInterface;
|
||||||
use Psr\Container\NotFoundExceptionInterface;
|
use Psr\Container\NotFoundExceptionInterface;
|
||||||
|
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||||
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
||||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Contracts\SpecialAuthenticationInjectorContract;
|
use Sunchayn\Nimbus\Modules\Relay\Authorization\Contracts\SpecialAuthenticationInjectorContract;
|
||||||
|
|
||||||
@@ -34,12 +34,12 @@ class RememberMeCookieInjector implements SpecialAuthenticationInjectorContract
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Request $relayRequest,
|
private readonly Request $relayRequest,
|
||||||
private readonly Container $container,
|
private readonly Container $container,
|
||||||
ConfigRepository $configRepository,
|
ActiveApplicationResolver $activeApplicationResolver,
|
||||||
) {
|
) {
|
||||||
$this->encrypter = $this->container->get('encrypter');
|
$this->encrypter = $this->container->get('encrypter');
|
||||||
|
|
||||||
$this->authGuard = $this->container->get('auth')->guard(
|
$this->authGuard = $this->container->get('auth')->guard(
|
||||||
$configRepository->get('nimbus.auth.guard'),
|
$activeApplicationResolver->getAuthGuard(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (! $this->authGuard instanceof StatefulGuard) {
|
if (! $this->authGuard instanceof StatefulGuard) {
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors;
|
namespace Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors;
|
||||||
|
|
||||||
use Illuminate\Config\Repository as ConfigRepository;
|
|
||||||
use Illuminate\Container\Container;
|
use Illuminate\Container\Container;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Contracts\Auth\Guard;
|
use Illuminate\Contracts\Auth\Guard;
|
||||||
use Illuminate\Http\Client\PendingRequest;
|
use Illuminate\Http\Client\PendingRequest;
|
||||||
|
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||||
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
||||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Contracts\SpecialAuthenticationInjectorContract;
|
use Sunchayn\Nimbus\Modules\Relay\Authorization\Contracts\SpecialAuthenticationInjectorContract;
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ class TymonJwtTokenInjector implements SpecialAuthenticationInjectorContract
|
|||||||
* @throws MisconfiguredValueException
|
* @throws MisconfiguredValueException
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ConfigRepository $configRepository,
|
private readonly ActiveApplicationResolver $activeApplicationResolver,
|
||||||
private readonly Container $container,
|
private readonly Container $container,
|
||||||
) {
|
) {
|
||||||
if (! class_exists(\Tymon\JWTAuth\JWTGuard::class)) {
|
if (! class_exists(\Tymon\JWTAuth\JWTGuard::class)) {
|
||||||
@@ -27,7 +27,7 @@ class TymonJwtTokenInjector implements SpecialAuthenticationInjectorContract
|
|||||||
|
|
||||||
$this->guard = $this
|
$this->guard = $this
|
||||||
->container->make('auth')
|
->container->make('auth')
|
||||||
->guard(name: $this->configRepository->get('nimbus.auth.guard'));
|
->guard(name: $this->activeApplicationResolver->getAuthGuard());
|
||||||
|
|
||||||
if (! $this->guard instanceof \Tymon\JWTAuth\JWTGuard) {
|
if (! $this->guard instanceof \Tymon\JWTAuth\JWTGuard) {
|
||||||
throw MisconfiguredValueException::becauseOfInvalidGuardInjectorCombination("Please use a `\Tymon\JWTAuth\JWTGuard` guard.");
|
throw MisconfiguredValueException::becauseOfInvalidGuardInjectorCombination("Please use a `\Tymon\JWTAuth\JWTGuard` guard.");
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
namespace Sunchayn\Nimbus\Modules\Routes\Actions;
|
namespace Sunchayn\Nimbus\Modules\Routes\Actions;
|
||||||
|
|
||||||
use Illuminate\Config\Repository as ConfigRepository;
|
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||||
use Sunchayn\Nimbus\Modules\Config\GlobalHeaderGeneratorTypeEnum;
|
use Sunchayn\Nimbus\Modules\Config\GlobalHeaderGeneratorTypeEnum;
|
||||||
|
|
||||||
class BuildGlobalHeadersAction
|
class BuildGlobalHeadersAction
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ConfigRepository $configRepository,
|
private readonly ActiveApplicationResolver $activeApplicationResolver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,7 +18,7 @@ class BuildGlobalHeadersAction
|
|||||||
public function execute(): array
|
public function execute(): array
|
||||||
{
|
{
|
||||||
/** @var array<array-key, mixed> $headers */
|
/** @var array<array-key, mixed> $headers */
|
||||||
$headers = $this->configRepository->get('nimbus.headers');
|
$headers = $this->activeApplicationResolver->getHeaders();
|
||||||
|
|
||||||
return array_values(
|
return array_values(
|
||||||
Arr::map(
|
Arr::map(
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
namespace Sunchayn\Nimbus\Modules\Routes\Actions;
|
namespace Sunchayn\Nimbus\Modules\Routes\Actions;
|
||||||
|
|
||||||
use Illuminate\Contracts\Config\Repository;
|
|
||||||
use Illuminate\Routing\Route;
|
use Illuminate\Routing\Route;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||||
use Sunchayn\Nimbus\Modules\Routes\Collections\ExtractedRoutesCollection;
|
use Sunchayn\Nimbus\Modules\Routes\Collections\ExtractedRoutesCollection;
|
||||||
use Sunchayn\Nimbus\Modules\Routes\DataTransferObjects\ExtractedRoute;
|
use Sunchayn\Nimbus\Modules\Routes\DataTransferObjects\ExtractedRoute;
|
||||||
use Sunchayn\Nimbus\Modules\Routes\Exceptions\RouteExtractionException;
|
use Sunchayn\Nimbus\Modules\Routes\Exceptions\RouteExtractionException;
|
||||||
@@ -29,7 +29,7 @@ class ExtractRoutesAction
|
|||||||
protected SchemaExtractor $schemaExtractor,
|
protected SchemaExtractor $schemaExtractor,
|
||||||
protected ExtractableRouteFactory $routeFactory,
|
protected ExtractableRouteFactory $routeFactory,
|
||||||
protected IgnoredRoutesService $ignoredRoutesService,
|
protected IgnoredRoutesService $ignoredRoutesService,
|
||||||
protected Repository $config,
|
protected ActiveApplicationResolver $activeApplicationResolver,
|
||||||
protected LoggerInterface $logger,
|
protected LoggerInterface $logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ class ExtractRoutesAction
|
|||||||
*/
|
*/
|
||||||
public function execute(array $routes): ExtractedRoutesCollection
|
public function execute(array $routes): ExtractedRoutesCollection
|
||||||
{
|
{
|
||||||
$prefix = $this->config->get('nimbus.routes.prefix');
|
$prefix = $this->activeApplicationResolver->getRoutesPrefix();
|
||||||
|
|
||||||
$configs = collect($routes)
|
$configs = collect($routes)
|
||||||
->filter(function (Route $route) use ($prefix): bool {
|
->filter(function (Route $route) use ($prefix): bool {
|
||||||
@@ -95,8 +95,8 @@ class ExtractRoutesAction
|
|||||||
return new ExtractedRoute(
|
return new ExtractedRoute(
|
||||||
uri: Endpoint::fromRaw(
|
uri: Endpoint::fromRaw(
|
||||||
$route->uri(),
|
$route->uri(),
|
||||||
routesPrefix: $this->config->get('nimbus.routes.prefix'),
|
routesPrefix: $this->activeApplicationResolver->getRoutesPrefix(),
|
||||||
isVersioned: $this->config->get('nimbus.routes.versioned'),
|
isVersioned: $this->activeApplicationResolver->isVersioned(),
|
||||||
),
|
),
|
||||||
methods: $methods,
|
methods: $methods,
|
||||||
schema: $schema,
|
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\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Http\Client\PendingRequest;
|
use Illuminate\Http\Client\PendingRequest;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Mockery\MockInterface;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Handlers\CurrentUserAuthorizationHandler;
|
use Sunchayn\Nimbus\Modules\Relay\Authorization\Handlers\CurrentUserAuthorizationHandler;
|
||||||
use Sunchayn\Nimbus\Tests\App\Modules\Relay\Authorization\Handlers\Shared\HandlesRecallerCookies;
|
use Sunchayn\Nimbus\Tests\App\Modules\Relay\Authorization\Handlers\Shared\HandlesRecallerCookies;
|
||||||
@@ -21,11 +22,10 @@ class CurrentUserAuthorizationHandlerFunctionalTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
||||||
config([
|
$this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class, function (MockInterface $mock) use (&$guardName) {
|
||||||
'nimbus.auth.guard' => $guardName = fake()->word(),
|
$mock->shouldReceive('getAuthGuard')->andReturn($guardName = fake()->word());
|
||||||
'nimbus.auth.special.injector' => DummySpecialAuthenticationInjector::class,
|
$mock->shouldReceive('getSpecialAuthInjector')->andReturn(DummySpecialAuthenticationInjector::class);
|
||||||
]);
|
});
|
||||||
|
|
||||||
$dummyAuthenticatable = new DummyAuthenticatable(id: $userId = fake()->randomNumber());
|
$dummyAuthenticatable = new DummyAuthenticatable(id: $userId = fake()->randomNumber());
|
||||||
|
|
||||||
$this->mockAuthManagerToUseDummyModel($userId, $dummyAuthenticatable, $guardName);
|
$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\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Http\Client\PendingRequest;
|
use Illuminate\Http\Client\PendingRequest;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Mockery\MockInterface;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use PHPUnit\Framework\Attributes\CoversMethod;
|
use PHPUnit\Framework\Attributes\CoversMethod;
|
||||||
use PHPUnit\Framework\Attributes\TestWith;
|
use PHPUnit\Framework\Attributes\TestWith;
|
||||||
@@ -25,11 +26,10 @@ class ImpersonateUserAuthorizationHandlerFunctionalTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
||||||
config([
|
$this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class, function (MockInterface $mock) use (&$guardName) {
|
||||||
'nimbus.auth.guard' => $guardName = fake()->word(),
|
$mock->shouldReceive('getAuthGuard')->andReturn($guardName = fake()->word());
|
||||||
'nimbus.auth.special.injector' => DummySpecialAuthenticationInjector::class,
|
$mock->shouldReceive('getSpecialAuthInjector')->andReturn(DummySpecialAuthenticationInjector::class);
|
||||||
]);
|
});
|
||||||
|
|
||||||
$dummyAuthenticatable = new DummyAuthenticatable(id: $userId = fake()->randomNumber());
|
$dummyAuthenticatable = new DummyAuthenticatable(id: $userId = fake()->randomNumber());
|
||||||
|
|
||||||
$this->mockAuthManagerToUseDummyModel($userId, $dummyAuthenticatable, $guardName);
|
$this->mockAuthManagerToUseDummyModel($userId, $dummyAuthenticatable, $guardName);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace Sunchayn\Nimbus\Tests\App\Modules\Relay\Authorization\Injectors;
|
namespace Sunchayn\Nimbus\Tests\App\Modules\Relay\Authorization\Injectors;
|
||||||
|
|
||||||
use Illuminate\Auth\SessionGuard;
|
use Illuminate\Auth\SessionGuard;
|
||||||
use Illuminate\Config\Repository as ConfigRepository;
|
|
||||||
use Illuminate\Container\Container;
|
use Illuminate\Container\Container;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Contracts\Auth\UserProvider;
|
use Illuminate\Contracts\Auth\UserProvider;
|
||||||
@@ -12,6 +11,7 @@ use Illuminate\Http\Client\PendingRequest;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors\RememberMeCookieInjector;
|
use Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors\RememberMeCookieInjector;
|
||||||
use Sunchayn\Nimbus\Tests\TestCase;
|
use Sunchayn\Nimbus\Tests\TestCase;
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ class RememberMeCookieInjectorUnitTest extends TestCase
|
|||||||
{
|
{
|
||||||
private Container $containerMock;
|
private Container $containerMock;
|
||||||
|
|
||||||
private ConfigRepository $configMock;
|
private ActiveApplicationResolver|MockInterface $projectManagerMock;
|
||||||
|
|
||||||
private SessionGuard $authGuardMock;
|
private SessionGuard $authGuardMock;
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ class RememberMeCookieInjectorUnitTest extends TestCase
|
|||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->containerMock = Mockery::mock(Container::class);
|
$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->authGuardMock = Mockery::mock(SessionGuard::class);
|
||||||
$this->userProviderMock = Mockery::mock(UserProvider::class);
|
$this->userProviderMock = Mockery::mock(UserProvider::class);
|
||||||
$this->encrypterMock = Mockery::mock(Encrypter::class);
|
$this->encrypterMock = Mockery::mock(Encrypter::class);
|
||||||
@@ -360,9 +360,8 @@ class RememberMeCookieInjectorUnitTest extends TestCase
|
|||||||
|
|
||||||
private function instantiateInjector(Request $relayRequest): RememberMeCookieInjector
|
private function instantiateInjector(Request $relayRequest): RememberMeCookieInjector
|
||||||
{
|
{
|
||||||
$this->configMock
|
$this->projectManagerMock
|
||||||
->shouldReceive('get')
|
->shouldReceive('getAuthGuard')
|
||||||
->with('nimbus.auth.guard')
|
|
||||||
->andReturn('web');
|
->andReturn('web');
|
||||||
|
|
||||||
$authManagerMock = Mockery::mock(\Illuminate\Auth\AuthManager::class);
|
$authManagerMock = Mockery::mock(\Illuminate\Auth\AuthManager::class);
|
||||||
@@ -388,7 +387,7 @@ class RememberMeCookieInjectorUnitTest extends TestCase
|
|||||||
return new RememberMeCookieInjector(
|
return new RememberMeCookieInjector(
|
||||||
relayRequest: $relayRequest,
|
relayRequest: $relayRequest,
|
||||||
container: $this->containerMock,
|
container: $this->containerMock,
|
||||||
configRepository: $this->configMock,
|
activeApplicationResolver: $this->projectManagerMock,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
namespace Sunchayn\Nimbus\Tests\App\Modules\Relay\Authorization\Injectors;
|
namespace Sunchayn\Nimbus\Tests\App\Modules\Relay\Authorization\Injectors;
|
||||||
|
|
||||||
use Illuminate\Config\Repository as ConfigRepository;
|
|
||||||
use Illuminate\Container\Container;
|
use Illuminate\Container\Container;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Contracts\Auth\Guard;
|
use Illuminate\Contracts\Auth\Guard;
|
||||||
use Illuminate\Http\Client\PendingRequest;
|
use Illuminate\Http\Client\PendingRequest;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver;
|
||||||
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
use Sunchayn\Nimbus\Modules\Config\Exceptions\MisconfiguredValueException;
|
||||||
use Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors\TymonJwtTokenInjector;
|
use Sunchayn\Nimbus\Modules\Relay\Authorization\Injectors\TymonJwtTokenInjector;
|
||||||
use Sunchayn\Nimbus\Tests\TestCase;
|
use Sunchayn\Nimbus\Tests\TestCase;
|
||||||
@@ -16,7 +16,7 @@ use Sunchayn\Nimbus\Tests\TestCase;
|
|||||||
#[CoversClass(TymonJwtTokenInjector::class)]
|
#[CoversClass(TymonJwtTokenInjector::class)]
|
||||||
class TymonJwtTokenInjectorUnitTest extends TestCase
|
class TymonJwtTokenInjectorUnitTest extends TestCase
|
||||||
{
|
{
|
||||||
private ConfigRepository $configMock;
|
private ActiveApplicationResolver|MockInterface $projectManagerMock;
|
||||||
|
|
||||||
private Container $containerMock;
|
private Container $containerMock;
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ class TymonJwtTokenInjectorUnitTest extends TestCase
|
|||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->configMock = Mockery::mock(ConfigRepository::class);
|
$this->projectManagerMock = Mockery::mock(ActiveApplicationResolver::class);
|
||||||
$this->containerMock = Mockery::mock(Container::class);
|
$this->containerMock = Mockery::mock(Container::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ class TymonJwtTokenInjectorUnitTest extends TestCase
|
|||||||
// Act
|
// Act
|
||||||
|
|
||||||
new TymonJwtTokenInjector(
|
new TymonJwtTokenInjector(
|
||||||
configRepository: $this->configMock,
|
activeApplicationResolver: $this->projectManagerMock,
|
||||||
container: $this->containerMock,
|
container: $this->containerMock,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace Sunchayn\Nimbus\Tests\App\Modules\Routes\Actions;
|
namespace Sunchayn\Nimbus\Tests\App\Modules\Routes\Actions;
|
||||||
|
|
||||||
use Illuminate\Config\Repository;
|
|
||||||
use Mockery\MockInterface;
|
|
||||||
use PHPUnit\Framework\Attributes\CoversClass;
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
use Sunchayn\Nimbus\Modules\Config\GlobalHeaderGeneratorTypeEnum;
|
use Sunchayn\Nimbus\Modules\Config\GlobalHeaderGeneratorTypeEnum;
|
||||||
use Sunchayn\Nimbus\Modules\Routes\Actions\BuildGlobalHeadersAction;
|
use Sunchayn\Nimbus\Modules\Routes\Actions\BuildGlobalHeadersAction;
|
||||||
@@ -23,15 +21,9 @@ class BuildGlobalHeadersActionFunctionalTest extends TestCase
|
|||||||
'X-Custom-Header' => '::value::',
|
'X-Custom-Header' => '::value::',
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->mock(
|
$this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class, function (\Mockery\MockInterface $mock) use ($globalHeadersConfig) {
|
||||||
Repository::class,
|
$mock->shouldReceive('getHeaders')->andReturn($globalHeadersConfig);
|
||||||
function (MockInterface $mock) use ($globalHeadersConfig) {
|
});
|
||||||
$mock
|
|
||||||
->shouldReceive('get')
|
|
||||||
->with('nimbus.headers')
|
|
||||||
->andReturn($globalHeadersConfig);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
$action = resolve(BuildGlobalHeadersAction::class);
|
$action = resolve(BuildGlobalHeadersAction::class);
|
||||||
|
|
||||||
@@ -72,15 +64,9 @@ class BuildGlobalHeadersActionFunctionalTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
||||||
$this->mock(
|
$this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class, function (\Mockery\MockInterface $mock) {
|
||||||
Repository::class,
|
$mock->shouldReceive('getHeaders')->andReturn([]);
|
||||||
function (MockInterface $mock) {
|
});
|
||||||
$mock
|
|
||||||
->shouldReceive('get')
|
|
||||||
->with('nimbus.headers')
|
|
||||||
->andReturn([]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
$action = resolve(BuildGlobalHeadersAction::class);
|
$action = resolve(BuildGlobalHeadersAction::class);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace Sunchayn\Nimbus\Tests\App\Modules\Routes\Services;
|
namespace Sunchayn\Nimbus\Tests\App\Modules\Routes\Services;
|
||||||
|
|
||||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
|
||||||
use Illuminate\Routing\Route;
|
use Illuminate\Routing\Route;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Facades\Route as RouteFacade;
|
use Illuminate\Support\Facades\Route as RouteFacade;
|
||||||
@@ -47,9 +46,9 @@ class RouteExtractorServiceFunctionalTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Anticipate
|
// Anticipate
|
||||||
|
|
||||||
$this->mock(ConfigRepository::class, function (MockInterface $mock) {
|
$this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class, function (MockInterface $mock) {
|
||||||
$mock->shouldReceive('get')->with('nimbus.routes.prefix')->andReturn('api');
|
$mock->shouldReceive('getRoutesPrefix')->andReturn('api');
|
||||||
$mock->shouldReceive('get')->with('nimbus.routes.versioned')->andReturn(fake()->boolean());
|
$mock->shouldReceive('isVersioned')->andReturn($this->isVersioned = fake()->boolean());
|
||||||
});
|
});
|
||||||
|
|
||||||
$routeFactoryMock = $this->mock(ExtractableRouteFactory::class, function (MockInterface $mock) {
|
$routeFactoryMock = $this->mock(ExtractableRouteFactory::class, function (MockInterface $mock) {
|
||||||
@@ -205,19 +204,19 @@ class RouteExtractorServiceFunctionalTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
||||||
$config = $this->mock(ConfigRepository::class);
|
|
||||||
|
|
||||||
RouteFacade::post('/custom/test', fn () => response()->json(['test' => true]))
|
RouteFacade::post('/custom/test', fn () => response()->json(['test' => true]))
|
||||||
->name('custom.test');
|
->name('custom.test');
|
||||||
|
|
||||||
|
$activeApplicationResolverMock = $this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class);
|
||||||
|
|
||||||
$routeExtractorService = resolve(ExtractRoutesAction::class);
|
$routeExtractorService = resolve(ExtractRoutesAction::class);
|
||||||
|
|
||||||
$routes = RouteFacade::getRoutes()->getRoutes();
|
$routes = RouteFacade::getRoutes()->getRoutes();
|
||||||
|
|
||||||
// Anticipate
|
// Anticipate
|
||||||
|
|
||||||
$config->shouldReceive('get')->with('nimbus.routes.prefix')->andReturn('custom');
|
$activeApplicationResolverMock->shouldReceive('getRoutesPrefix')->andReturn('custom');
|
||||||
$config->shouldReceive('get')->with('nimbus.routes.versioned')->andReturnFalse();
|
$activeApplicationResolverMock->shouldReceive('isVersioned')->andReturn(false);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
||||||
@@ -239,9 +238,9 @@ class RouteExtractorServiceFunctionalTest extends TestCase
|
|||||||
{
|
{
|
||||||
// Anticipate
|
// Anticipate
|
||||||
|
|
||||||
$this->mock(ConfigRepository::class, function (MockInterface $mock) {
|
$this->mock(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::class, function (MockInterface $mock) {
|
||||||
$mock->shouldReceive('get')->with('nimbus.routes.prefix')->andReturn('api');
|
$mock->shouldReceive('getRoutesPrefix')->andReturn('api');
|
||||||
$mock->shouldReceive('get')->with('nimbus.routes.versioned')->andReturn(fake()->boolean());
|
$mock->shouldReceive('isVersioned')->andReturn(fake()->boolean());
|
||||||
});
|
});
|
||||||
|
|
||||||
$dummyFailingException = new RuntimeException(message: $dummyFailingExceptionMessage = fake()->sentence());
|
$dummyFailingException = new RuntimeException(message: $dummyFailingExceptionMessage = fake()->sentence());
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ test("Dump and Die visualization sanity checklist", async ({ page }) => {
|
|||||||
.click();
|
.click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator("#reka-collapsible-content-v-113"),
|
page.locator("#reka-collapsible-content-v-119"),
|
||||||
).toMatchAriaSnapshot(`- text: "0: \\"/\\" (1) 1: \\"\\\\\\" (1)"`);
|
).toMatchAriaSnapshot(`- text: "0: \\"/\\" (1) 1: \\"\\\\\\" (1)"`);
|
||||||
|
|
||||||
// dump #3 (runtime object)
|
// 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\Modules\Routes\Services\IgnoredRoutesService;
|
||||||
use Sunchayn\Nimbus\Tests\TestCase;
|
use Sunchayn\Nimbus\Tests\TestCase;
|
||||||
|
|
||||||
#[CoversClass(NimbusIndexController::class)]
|
|
||||||
#[CoversClass(NimbusIndexController::class)]
|
#[CoversClass(NimbusIndexController::class)]
|
||||||
class NimbusIndexTest extends TestCase
|
class NimbusIndexTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -32,16 +31,20 @@ class NimbusIndexTest extends TestCase
|
|||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
// Mock Vite to prevent asset loading issues
|
// Mock Vite to prevent asset loading issues
|
||||||
Vite::shouldReceive('useBuildDirectory')->with('/vendor/nimbus')->once();
|
Vite::shouldReceive('useBuildDirectory')->with('/vendor/nimbus')->atMost()->once();
|
||||||
Vite::shouldReceive('useHotFile')->with(base_path('/vendor/sunchayn/nimbus/resources/dist/hot'))->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>');
|
Vite::shouldReceive('__invoke')->andReturn('<script src="/nimbus/app.js"></script>')->atMost()->once();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[DataProvider('indexRouteProvider')]
|
#[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
|
// Arrange
|
||||||
|
|
||||||
|
if ($applicationCookie) {
|
||||||
|
$this->withCookie(\Sunchayn\Nimbus\Modules\Config\ActiveApplicationResolver::CURRENT_APPLICATION_COOKIE_NAME, $applicationCookie);
|
||||||
|
}
|
||||||
|
|
||||||
$disableThirdPartyUiActionSpy = $this->spy(DisableThirdPartyUiAction::class);
|
$disableThirdPartyUiActionSpy = $this->spy(DisableThirdPartyUiAction::class);
|
||||||
$ignoreRoutesServiceSpy = $this->spy(IgnoredRoutesService::class);
|
$ignoreRoutesServiceSpy = $this->spy(IgnoredRoutesService::class);
|
||||||
$buildGlobalHeadersActionMock = $this->mock(BuildGlobalHeadersAction::class);
|
$buildGlobalHeadersActionMock = $this->mock(BuildGlobalHeadersAction::class);
|
||||||
@@ -54,11 +57,13 @@ class NimbusIndexTest extends TestCase
|
|||||||
|
|
||||||
$buildCurrentUserActionMock->shouldReceive('execute')->andReturn(['::current-user::']);
|
$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
|
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->assertViewIs('nimbus::app');
|
||||||
|
|
||||||
$response->assertViewHas('routes', ['::extracted-routes::']);
|
$response->assertViewHas('routes', ["routes for $expectedApplicationKey"]);
|
||||||
|
|
||||||
$response->assertViewHas('headers', ['::global-headers::']);
|
$response->assertViewHas('headers', ['::global-headers::']);
|
||||||
|
|
||||||
$response->assertViewHas('currentUser', ['::current-user::']);
|
$response->assertViewHas('currentUser', ['::current-user::']);
|
||||||
|
|
||||||
|
$response->assertViewHas('activeApplicationResolver', function ($resolver) use ($expectedApplicationKey) {
|
||||||
|
return $resolver->getActiveApplicationKey() === $expectedApplicationKey;
|
||||||
|
});
|
||||||
|
|
||||||
$disableThirdPartyUiActionSpy->shouldHaveReceived('execute')->once();
|
$disableThirdPartyUiActionSpy->shouldHaveReceived('execute')->once();
|
||||||
|
|
||||||
$ignoreRoutesServiceSpy->shouldNotHaveReceived('execute');
|
$ignoreRoutesServiceSpy->shouldNotHaveReceived('execute');
|
||||||
@@ -87,12 +96,22 @@ class NimbusIndexTest extends TestCase
|
|||||||
|
|
||||||
public static function indexRouteProvider(): Generator
|
public static function indexRouteProvider(): Generator
|
||||||
{
|
{
|
||||||
yield 'index route handles route extraction exception' => [
|
yield 'index route with default application' => [
|
||||||
'uri' => '/',
|
'uri' => '/',
|
||||||
|
'applicationCookie' => null,
|
||||||
|
'expectedApplicationKey' => 'main',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'index route with application from cookie' => [
|
||||||
|
'uri' => '/',
|
||||||
|
'applicationCookie' => 'other',
|
||||||
|
'expectedApplicationKey' => 'other',
|
||||||
];
|
];
|
||||||
|
|
||||||
yield 'index route with catch all parameter' => [
|
yield 'index route with catch all parameter' => [
|
||||||
'uri' => '/some/deep/path',
|
'uri' => '/some/deep/path',
|
||||||
|
'applicationCookie' => null,
|
||||||
|
'expectedApplicationKey' => 'main',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,4 +202,24 @@ class NimbusIndexTest extends TestCase
|
|||||||
|
|
||||||
$response->assertViewHas(['routes', 'headers', 'currentUser']);
|
$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();
|
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();
|
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
|
protected function tearDown(): void
|
||||||
{
|
{
|
||||||
if ($container = Mockery::getContainer()) {
|
if ($container = Mockery::getContainer()) {
|
||||||
|
|||||||
@@ -90,10 +90,12 @@ The Route Explorer automatically discovers your Laravel API routes and organizes
|
|||||||

|

|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- Routes are grouped by resource (e.g., all user-related endpoints together).
|
- **Search**: Quickly filter routes by endpoint path.
|
||||||
- Each route shows its HTTP methods.
|
- **Application Switcher**: Switch between multiple API applications (e.g., Rest API, CMS API) if configured.
|
||||||
- Click any route to load it in the Request Builder.
|
- **Version Selector**: If the active application is versioned, a version picker appears inline with the application name.
|
||||||
- Routes are extracted based on your configured API prefix.
|
- **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
|
### Request Builder
|
||||||
|
|
||||||
@@ -359,16 +361,44 @@ return [
|
|||||||
|
|
||||||
### Configuration Options
|
### Configuration Options
|
||||||
|
|
||||||
| Option | Description | Default | Example |
|
| Option | Description | Default | Example |
|
||||||
|--------|--------------|----------|----------|
|
|--------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------|-----------------------------------------------------------|
|
||||||
| **`prefix`** | The URI segment under which Nimbus is accessible. | `'nimbus'` | `'api-client'` |
|
| **`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']` |
|
| **`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'` |
|
| **`default_application`** | The base default application to load when no other application is found in the storage. | n/a | `rest-api` |
|
||||||
| **`routes.versioned`** | Enables version parsing for routes like `/api/v1/...`. | `false` | `true` |
|
| **`applications.*.routes.prefix`** | The base path used to detect application routes. Only routes starting with this prefix are analyzed. | `'api'` | `'api/v1'` |
|
||||||
| **`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.*.routes.versioned`** | Enables version parsing for routes like `/api/v1/...`. | `false` | `true` |
|
||||||
| **`auth.guard`** | The Laravel guard used for the API requests authentication. | `'api'` | `'web'` |
|
| **`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` |
|
||||||
| **`auth.special.injector`** | Injector class used to attach authentication credentials to outgoing requests. Must implement `SpecialAuthenticationInjectorContract`. | `RememberMeCookieInjector::class` | `TymonJwtTokenInjector::class` |
|
| **`applications.*.auth.guard`** | The Laravel guard used for the API requests authentication. | `'api'` | `'web'` |
|
||||||
| **`headers`** | Global headers applied to all outgoing requests. Supports static values or enum generators. | `[]` | `['x-request-id' => GlobalHeaderGeneratorTypeEnum::UUID]` |
|
| **`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
|
#### 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