@lukoweb/apitogo 0.1.49 → 0.1.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +1124 -892
- package/dist/declarations/config/apitogo-config-loader.d.ts +7 -0
- package/dist/declarations/config/build-apitogo-config.d.ts +7 -0
- package/dist/declarations/config/loader.d.ts +2 -0
- package/dist/declarations/config/local-manifest.d.ts +8 -1
- package/dist/declarations/config/validators/HeaderNavigationSchema.d.ts +24 -24
- package/dist/declarations/config/validators/ZudokuConfig.d.ts +9 -9
- package/dist/declarations/index.d.ts +2 -1
- package/dist/declarations/lib/authentication/auth-header-nav.d.ts +10 -0
- package/dist/declarations/lib/ui/Button.d.ts +1 -1
- package/dist/declarations/lib/ui/Item.d.ts +1 -1
- package/docs/configuration/authentication.md +6 -5
- package/docs/configuration/manifest.md +60 -71
- package/package.json +17 -1
- package/src/config/apitogo-config-loader.ts +224 -0
- package/src/config/build-apitogo-config.ts +43 -0
- package/src/config/loader.ts +40 -1
- package/src/config/local-manifest.ts +60 -8
- package/src/index.ts +8 -1
- package/src/lib/authentication/auth-header-nav.ts +56 -0
- package/src/lib/components/Header.tsx +48 -60
- package/src/lib/components/MobileTopNavigation.tsx +29 -40
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { access, readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import {
|
|
5
|
+
APITOGO_CONFIG_FILENAME,
|
|
6
|
+
extractManifest,
|
|
7
|
+
loadApitogoConfig,
|
|
8
|
+
} from "./apitogo-config-loader.js";
|
|
4
9
|
|
|
5
|
-
/** @deprecated Use {@link
|
|
10
|
+
/** @deprecated Use {@link APITOGO_CONFIG_FILENAME}. */
|
|
6
11
|
export const DEV_PORTAL_MANIFEST_FILENAME = "apitogo.local.json";
|
|
7
12
|
|
|
13
|
+
/** @deprecated Use {@link APITOGO_CONFIG_FILENAME}. */
|
|
8
14
|
export const APITOGO_MANIFEST_BASE_FILENAME = "apitogo.json";
|
|
9
15
|
|
|
16
|
+
/** @deprecated Per-environment manifest overrides were replaced by apitogo.config.json + .env. */
|
|
10
17
|
export const APITOGO_MANIFEST_ENV_FILES = {
|
|
11
18
|
development: "apitogo.local.json",
|
|
12
19
|
production: "apitogo.prod.json",
|
|
13
20
|
} as const;
|
|
14
21
|
|
|
22
|
+
export { APITOGO_CONFIG_FILENAME };
|
|
23
|
+
|
|
15
24
|
export type ApitogoManifestEnv = keyof typeof APITOGO_MANIFEST_ENV_FILES;
|
|
16
25
|
|
|
17
26
|
const BillingPeriodSchema = z.enum(["month", "year", "monthly", "annual"]);
|
|
@@ -40,6 +49,11 @@ export const DevPortalLocalManifestSchema = z.object({
|
|
|
40
49
|
enabled: z.boolean().optional(),
|
|
41
50
|
})
|
|
42
51
|
.optional(),
|
|
52
|
+
authentication: z
|
|
53
|
+
.object({
|
|
54
|
+
enabled: z.boolean().optional(),
|
|
55
|
+
})
|
|
56
|
+
.optional(),
|
|
43
57
|
plans: z.array(ManifestPlanInputSchema).optional(),
|
|
44
58
|
backend: z
|
|
45
59
|
.object({
|
|
@@ -83,6 +97,7 @@ export type DevPortalPreviewPlan = {
|
|
|
83
97
|
|
|
84
98
|
export type DevPortalPreviewPlanCatalog = {
|
|
85
99
|
pricingEnabled: boolean;
|
|
100
|
+
authenticationEnabled: boolean;
|
|
86
101
|
plans: DevPortalPreviewPlan[];
|
|
87
102
|
};
|
|
88
103
|
|
|
@@ -92,6 +107,12 @@ export function isPricingEnabled(
|
|
|
92
107
|
return manifest?.pricing?.enabled === true;
|
|
93
108
|
}
|
|
94
109
|
|
|
110
|
+
export function isAuthenticationEnabled(
|
|
111
|
+
manifest: DevPortalLocalManifest | null | undefined,
|
|
112
|
+
): boolean {
|
|
113
|
+
return manifest?.authentication?.enabled === true;
|
|
114
|
+
}
|
|
115
|
+
|
|
95
116
|
export function manifestToPreviewCatalog(
|
|
96
117
|
manifest: DevPortalLocalManifest | null | undefined,
|
|
97
118
|
): DevPortalPreviewPlanCatalog {
|
|
@@ -99,6 +120,7 @@ export function manifestToPreviewCatalog(
|
|
|
99
120
|
const catalog = plansToPreviewCatalog(plans);
|
|
100
121
|
return {
|
|
101
122
|
pricingEnabled: isPricingEnabled(manifest),
|
|
123
|
+
authenticationEnabled: isAuthenticationEnabled(manifest),
|
|
102
124
|
plans: catalog.plans,
|
|
103
125
|
};
|
|
104
126
|
}
|
|
@@ -170,6 +192,12 @@ export function mergeManifest(
|
|
|
170
192
|
if (override.pricing) {
|
|
171
193
|
merged.pricing = { ...base.pricing, ...override.pricing };
|
|
172
194
|
}
|
|
195
|
+
if (override.authentication) {
|
|
196
|
+
merged.authentication = {
|
|
197
|
+
...base.authentication,
|
|
198
|
+
...override.authentication,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
173
201
|
if (override.backend) {
|
|
174
202
|
merged.backend = { ...base.backend, ...override.backend };
|
|
175
203
|
}
|
|
@@ -192,16 +220,15 @@ export function manifestFilePath(rootDir: string, filename?: string): string {
|
|
|
192
220
|
|
|
193
221
|
export function manifestPathsForEnv(
|
|
194
222
|
rootDir: string,
|
|
195
|
-
|
|
223
|
+
_env: ApitogoManifestEnv,
|
|
196
224
|
): { basePath: string; overridePath: string; watchPaths: string[] } {
|
|
197
225
|
const resolvedRoot = path.resolve(rootDir);
|
|
198
|
-
const
|
|
199
|
-
const overridePath = path.join(resolvedRoot, APITOGO_MANIFEST_ENV_FILES[env]);
|
|
226
|
+
const configPath = path.join(resolvedRoot, APITOGO_CONFIG_FILENAME);
|
|
200
227
|
|
|
201
228
|
return {
|
|
202
|
-
basePath,
|
|
203
|
-
overridePath,
|
|
204
|
-
watchPaths: [
|
|
229
|
+
basePath: configPath,
|
|
230
|
+
overridePath: configPath,
|
|
231
|
+
watchPaths: [configPath],
|
|
205
232
|
};
|
|
206
233
|
}
|
|
207
234
|
|
|
@@ -243,11 +270,36 @@ export async function readManifestFile(
|
|
|
243
270
|
}
|
|
244
271
|
}
|
|
245
272
|
|
|
273
|
+
function parseManifestRecord(
|
|
274
|
+
manifest: ReturnType<typeof extractManifest>,
|
|
275
|
+
): DevPortalLocalManifest | null {
|
|
276
|
+
if (!manifest) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const result = DevPortalLocalManifestSchema.safeParse(manifest);
|
|
281
|
+
return result.success ? result.data : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
246
284
|
export async function readEffectiveManifest(
|
|
247
285
|
rootDir: string,
|
|
248
286
|
env: ApitogoManifestEnv,
|
|
249
287
|
): Promise<DevPortalLocalManifest | null> {
|
|
250
|
-
|
|
288
|
+
try {
|
|
289
|
+
const config = loadApitogoConfig(rootDir);
|
|
290
|
+
return parseManifestRecord(extractManifest(config));
|
|
291
|
+
} catch {
|
|
292
|
+
return readLegacyEffectiveManifest(rootDir, env);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function readLegacyEffectiveManifest(
|
|
297
|
+
rootDir: string,
|
|
298
|
+
env: ApitogoManifestEnv,
|
|
299
|
+
): Promise<DevPortalLocalManifest | null> {
|
|
300
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
301
|
+
const basePath = path.join(resolvedRoot, APITOGO_MANIFEST_BASE_FILENAME);
|
|
302
|
+
const overridePath = path.join(resolvedRoot, APITOGO_MANIFEST_ENV_FILES[env]);
|
|
251
303
|
const hasBase = await fileExists(basePath);
|
|
252
304
|
const hasOverride = await fileExists(overridePath);
|
|
253
305
|
|
package/src/index.ts
CHANGED
|
@@ -71,4 +71,11 @@ export {
|
|
|
71
71
|
type ProblemJson,
|
|
72
72
|
throwIfProblemJson,
|
|
73
73
|
} from "./lib/util/problemJson.js";
|
|
74
|
-
export
|
|
74
|
+
export { buildApitogoConfig } from "./config/build-apitogo-config.js";
|
|
75
|
+
export {
|
|
76
|
+
APITOGO_CONFIG_FILENAME,
|
|
77
|
+
applyEnvOverrides,
|
|
78
|
+
extractManifest,
|
|
79
|
+
extractSiteConfig,
|
|
80
|
+
loadApitogoConfig,
|
|
81
|
+
} from "./config/apitogo-config-loader.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { HeaderNavigation } from "../../config/validators/HeaderNavigationSchema.js";
|
|
2
|
+
import type { ZudokuConfig } from "../../config/validators/ZudokuConfig.js";
|
|
3
|
+
|
|
4
|
+
export const LOGIN_NAV_PATH = "/signin";
|
|
5
|
+
|
|
6
|
+
export const LOGIN_HEADER_NAV_ITEM = {
|
|
7
|
+
label: "Login",
|
|
8
|
+
to: LOGIN_NAV_PATH,
|
|
9
|
+
display: "anon" as const,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function withoutLoginNavigation(
|
|
13
|
+
navigation: HeaderNavigation,
|
|
14
|
+
): HeaderNavigation {
|
|
15
|
+
return navigation.filter(
|
|
16
|
+
(item) => !("to" in item && item.to === LOGIN_NAV_PATH),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function applyAuthenticationHeaderNav<T extends ZudokuConfig>(
|
|
21
|
+
config: T,
|
|
22
|
+
): T {
|
|
23
|
+
if (!config.authentication) {
|
|
24
|
+
return config;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const authNavEnabled = config.__meta?.authenticationEnabled === true;
|
|
28
|
+
const navigation = config.header?.navigation ?? [];
|
|
29
|
+
|
|
30
|
+
if (!authNavEnabled) {
|
|
31
|
+
const filtered = withoutLoginNavigation(navigation);
|
|
32
|
+
if (filtered.length === navigation.length) {
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
...config,
|
|
38
|
+
header: {
|
|
39
|
+
...config.header,
|
|
40
|
+
navigation: filtered,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (navigation.some((item) => "to" in item && item.to === LOGIN_NAV_PATH)) {
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
...config,
|
|
51
|
+
header: {
|
|
52
|
+
...config.header,
|
|
53
|
+
navigation: [...navigation, LOGIN_HEADER_NAV_ITEM],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
2
|
-
import { Skeleton } from "@lukoweb/apitogo/ui/Skeleton.js";
|
|
3
2
|
import { LogOutIcon } from "lucide-react";
|
|
4
3
|
import { lazy, memo, Suspense } from "react";
|
|
5
4
|
import { Link } from "react-router";
|
|
@@ -20,7 +19,6 @@ import {
|
|
|
20
19
|
import { cn } from "../util/cn.js";
|
|
21
20
|
import { joinUrl } from "../util/joinUrl.js";
|
|
22
21
|
import { Banner } from "./Banner.js";
|
|
23
|
-
import { ClientOnly } from "./ClientOnly.js";
|
|
24
22
|
import { useZudoku } from "./context/ZudokuContext.js";
|
|
25
23
|
import { MobileTopNavigation } from "./MobileTopNavigation.js";
|
|
26
24
|
import { PageProgress } from "./PageProgress.js";
|
|
@@ -73,68 +71,58 @@ const ProfileMenu = () => {
|
|
|
73
71
|
const context = useZudoku();
|
|
74
72
|
const profileItems = context.getProfileMenuItems();
|
|
75
73
|
const auth = useAuth();
|
|
76
|
-
const { isAuthEnabled, isAuthenticated,
|
|
74
|
+
const { isAuthEnabled, isAuthenticated, profile } = auth;
|
|
77
75
|
|
|
78
|
-
if (!isAuthEnabled) return null;
|
|
76
|
+
if (!isAuthEnabled || !isAuthenticated) return null;
|
|
79
77
|
|
|
80
78
|
return (
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
<Button size="lg" variant="ghost" onClick={() => auth.login()}>
|
|
86
|
-
Login
|
|
79
|
+
<DropdownMenu modal={false}>
|
|
80
|
+
<DropdownMenuTrigger asChild>
|
|
81
|
+
<Button size="lg" variant="ghost">
|
|
82
|
+
{profile?.name ?? "My Account"}
|
|
87
83
|
</Button>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
{
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
<LogOutIcon size={16} strokeWidth={1} absoluteStrokeWidth />
|
|
131
|
-
Logout
|
|
132
|
-
</DropdownMenuItem>
|
|
133
|
-
</Link>
|
|
134
|
-
</DropdownMenuContent>
|
|
135
|
-
</DropdownMenu>
|
|
136
|
-
)}
|
|
137
|
-
</ClientOnly>
|
|
84
|
+
</DropdownMenuTrigger>
|
|
85
|
+
<DropdownMenuContent className="w-56">
|
|
86
|
+
<DropdownMenuLabel>
|
|
87
|
+
{profile?.name ?? "My Account"}
|
|
88
|
+
{profile?.email && profile.email !== profile?.name && (
|
|
89
|
+
<div className="font-normal text-muted-foreground">
|
|
90
|
+
{profile.email}
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</DropdownMenuLabel>
|
|
94
|
+
{profileItems.filter((i) => i.category === "top").length > 0 && (
|
|
95
|
+
<DropdownMenuSeparator />
|
|
96
|
+
)}
|
|
97
|
+
{profileItems
|
|
98
|
+
.filter((i) => i.category === "top")
|
|
99
|
+
.map((i) => (
|
|
100
|
+
<RecursiveMenu key={i.label} item={i} />
|
|
101
|
+
))}
|
|
102
|
+
{profileItems.filter((i) => !i.category || i.category === "middle")
|
|
103
|
+
.length > 0 && <DropdownMenuSeparator />}
|
|
104
|
+
{profileItems
|
|
105
|
+
.filter((i) => !i.category || i.category === "middle")
|
|
106
|
+
.map((i) => (
|
|
107
|
+
<RecursiveMenu key={i.label} item={i} />
|
|
108
|
+
))}
|
|
109
|
+
{profileItems.filter((i) => i.category === "bottom").length > 0 && (
|
|
110
|
+
<DropdownMenuSeparator />
|
|
111
|
+
)}
|
|
112
|
+
{profileItems
|
|
113
|
+
.filter((i) => i.category === "bottom")
|
|
114
|
+
.map((i) => (
|
|
115
|
+
<RecursiveMenu key={i.label} item={i} />
|
|
116
|
+
))}
|
|
117
|
+
<DropdownMenuSeparator />
|
|
118
|
+
<Link to="/signout">
|
|
119
|
+
<DropdownMenuItem className="flex gap-2">
|
|
120
|
+
<LogOutIcon size={16} strokeWidth={1} absoluteStrokeWidth />
|
|
121
|
+
Logout
|
|
122
|
+
</DropdownMenuItem>
|
|
123
|
+
</Link>
|
|
124
|
+
</DropdownMenuContent>
|
|
125
|
+
</DropdownMenu>
|
|
138
126
|
);
|
|
139
127
|
};
|
|
140
128
|
export const Header = memo(function HeaderInner() {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
2
2
|
import { Separator } from "@lukoweb/apitogo/ui/Separator.js";
|
|
3
|
-
import { Skeleton } from "@lukoweb/apitogo/ui/Skeleton.js";
|
|
4
3
|
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
|
5
4
|
import { deepEqual } from "fast-equals";
|
|
6
5
|
import {
|
|
@@ -28,7 +27,6 @@ import {
|
|
|
28
27
|
DrawerTitle,
|
|
29
28
|
DrawerTrigger,
|
|
30
29
|
} from "../ui/Drawer.js";
|
|
31
|
-
import { ClientOnly } from "./ClientOnly.js";
|
|
32
30
|
import { useCurrentNavigation, useZudoku } from "./context/ZudokuContext.js";
|
|
33
31
|
import { PoweredByZudoku } from "./navigation/PoweredByZudoku.js";
|
|
34
32
|
import { getFirstMatchingPath, shouldShowItem } from "./navigation/utils.js";
|
|
@@ -130,7 +128,7 @@ export const MobileTopNavigation = () => {
|
|
|
130
128
|
getProfileMenuItems,
|
|
131
129
|
} = context;
|
|
132
130
|
const headerNavigation = header?.navigation ?? [];
|
|
133
|
-
const { isAuthenticated, profile, isAuthEnabled
|
|
131
|
+
const { isAuthenticated, profile, isAuthEnabled } = authState;
|
|
134
132
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
135
133
|
|
|
136
134
|
const accountItems = getProfileMenuItems();
|
|
@@ -195,9 +193,7 @@ export const MobileTopNavigation = () => {
|
|
|
195
193
|
);
|
|
196
194
|
})}
|
|
197
195
|
{isAuthEnabled && isAuthenticated && (
|
|
198
|
-
|
|
199
|
-
fallback={<Skeleton className="rounded-sm h-5 w-24" />}
|
|
200
|
-
>
|
|
196
|
+
<>
|
|
201
197
|
<Separator className="my-2" />
|
|
202
198
|
<li className="py-2">
|
|
203
199
|
<div className="text-base font-medium">
|
|
@@ -226,45 +222,38 @@ export const MobileTopNavigation = () => {
|
|
|
226
222
|
</Link>
|
|
227
223
|
</li>
|
|
228
224
|
))}
|
|
229
|
-
|
|
225
|
+
</>
|
|
230
226
|
)}
|
|
231
227
|
</ul>
|
|
232
228
|
</div>
|
|
233
229
|
<div className="border-t shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)] px-4 pt-3 flex flex-col gap-2">
|
|
234
230
|
<div className="flex items-center justify-between">
|
|
235
|
-
{isAuthEnabled &&
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
>
|
|
262
|
-
Login
|
|
263
|
-
</Link>
|
|
264
|
-
</Button>
|
|
265
|
-
)}
|
|
266
|
-
</ClientOnly>
|
|
267
|
-
)}
|
|
231
|
+
{isAuthEnabled &&
|
|
232
|
+
(isAuthenticated ? (
|
|
233
|
+
<Button asChild variant="outline">
|
|
234
|
+
<Link
|
|
235
|
+
to="/signout"
|
|
236
|
+
onClick={() => setDrawerOpen(false)}
|
|
237
|
+
className="flex items-center gap-2"
|
|
238
|
+
>
|
|
239
|
+
<LogOutIcon
|
|
240
|
+
size={16}
|
|
241
|
+
strokeWidth={1}
|
|
242
|
+
absoluteStrokeWidth
|
|
243
|
+
/>
|
|
244
|
+
Logout
|
|
245
|
+
</Link>
|
|
246
|
+
</Button>
|
|
247
|
+
) : (
|
|
248
|
+
<Button asChild variant="outline">
|
|
249
|
+
<Link
|
|
250
|
+
to={`/signin?redirect=${encodeURIComponent(location.pathname)}`}
|
|
251
|
+
onClick={() => setDrawerOpen(false)}
|
|
252
|
+
>
|
|
253
|
+
Login
|
|
254
|
+
</Link>
|
|
255
|
+
</Button>
|
|
256
|
+
))}
|
|
268
257
|
<ThemeSwitch />
|
|
269
258
|
</div>
|
|
270
259
|
{site?.showPoweredBy !== false && (
|