@xexr/blog-platform 1.0.7 → 1.0.8
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/CHANGELOG.md +7 -0
- package/dist/app/blog/[slug]/page.d.ts +68 -0
- package/dist/app/blog/[slug]/page.tsx +155 -113
- package/dist/components/ArticleReadingProgress.tsx +208 -0
- package/dist/components/ArticleTopActions.tsx +179 -0
- package/dist/components/AuthorByline.tsx +569 -0
- package/dist/components/ExpandableArticleImage.tsx +79 -0
- package/dist/components/Pagination.tsx +236 -0
- package/dist/components/TopicArchivePages.tsx +204 -0
- package/dist/components/ui/badge.tsx +36 -0
- package/dist/components/ui/button.tsx +59 -0
- package/dist/components/ui/card.tsx +92 -0
- package/dist/components/ui/pagination.tsx +127 -0
- package/dist/components/ui/separator.tsx +29 -0
- package/dist/index.d.mts +59 -69
- package/dist/index.d.ts +59 -69
- package/dist/index.js +83 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +83 -8
- package/dist/index.mjs.map +1 -1
- package/dist/lib/article-ui.ts +158 -0
- package/dist/lib/auth.ts +354 -0
- package/dist/lib/cors.ts +243 -0
- package/dist/lib/database.ts +277 -0
- package/dist/lib/init.ts +186 -0
- package/dist/lib/mdx.ts +73 -0
- package/dist/lib/metadata.ts +558 -0
- package/dist/lib/ratelimit.ts +224 -0
- package/dist/lib/security-headers.ts +322 -0
- package/dist/lib/seo.ts +295 -0
- package/dist/lib/sitemap.ts +719 -0
- package/dist/lib/storage/drizzle.ts +400 -0
- package/dist/lib/structured-data.ts +608 -0
- package/dist/lib/topics.ts +56 -0
- package/dist/lib/urls.ts +29 -0
- package/dist/lib/utils.ts +6 -0
- package/dist/lib/version.ts +154 -0
- package/dist/types/contracts.ts +256 -0
- package/dist/types/index.d.mts +18 -1
- package/dist/types/index.d.ts +18 -1
- package/dist/types/index.ts +16 -0
- package/package.json +14 -9
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { format, formatDistanceToNow } from "date-fns";
|
|
2
|
+
import type { CSSProperties } from "react";
|
|
3
|
+
|
|
4
|
+
export interface BlogTheme {
|
|
5
|
+
accentColor?: string;
|
|
6
|
+
accentHoverColor?: string;
|
|
7
|
+
textAccentColor?: string;
|
|
8
|
+
textAccentHoverColor?: string;
|
|
9
|
+
accentBackgroundColor?: string;
|
|
10
|
+
accentBorderColor?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TocItem {
|
|
14
|
+
id: string;
|
|
15
|
+
text: string;
|
|
16
|
+
level: 2 | 3;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ArticleDateMeta {
|
|
20
|
+
publishedDateLabel: string;
|
|
21
|
+
publishedRelativeLabel: string;
|
|
22
|
+
updatedDateLabel: string;
|
|
23
|
+
isUpdated: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TocItemState {
|
|
27
|
+
isActive: boolean;
|
|
28
|
+
ariaCurrent: "location" | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type BlogThemeStyle = CSSProperties & {
|
|
32
|
+
"--blog-accent": string;
|
|
33
|
+
"--blog-accent-hover": string;
|
|
34
|
+
"--blog-text-accent": string;
|
|
35
|
+
"--blog-text-accent-hover": string;
|
|
36
|
+
"--blog-accent-bg": string;
|
|
37
|
+
"--blog-accent-border": string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const DEFAULT_THEME: Required<BlogTheme> = {
|
|
41
|
+
accentColor: "hsl(var(--primary))",
|
|
42
|
+
accentHoverColor: "hsl(var(--primary))",
|
|
43
|
+
textAccentColor: "hsl(var(--primary))",
|
|
44
|
+
textAccentHoverColor: "hsl(var(--primary))",
|
|
45
|
+
accentBackgroundColor: "hsl(var(--primary) / 0.08)",
|
|
46
|
+
accentBorderColor: "hsl(var(--primary) / 0.25)",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function getBlogThemeStyle(theme: BlogTheme = {}): BlogThemeStyle {
|
|
50
|
+
return {
|
|
51
|
+
"--blog-accent": theme.accentColor ?? DEFAULT_THEME.accentColor,
|
|
52
|
+
"--blog-accent-hover":
|
|
53
|
+
theme.accentHoverColor ?? DEFAULT_THEME.accentHoverColor,
|
|
54
|
+
"--blog-text-accent":
|
|
55
|
+
theme.textAccentColor ?? theme.accentColor ?? DEFAULT_THEME.textAccentColor,
|
|
56
|
+
"--blog-text-accent-hover":
|
|
57
|
+
theme.textAccentHoverColor ??
|
|
58
|
+
theme.accentHoverColor ??
|
|
59
|
+
DEFAULT_THEME.textAccentHoverColor,
|
|
60
|
+
"--blog-accent-bg":
|
|
61
|
+
theme.accentBackgroundColor ?? DEFAULT_THEME.accentBackgroundColor,
|
|
62
|
+
"--blog-accent-border":
|
|
63
|
+
theme.accentBorderColor ?? DEFAULT_THEME.accentBorderColor,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function formatArticleDateMeta(
|
|
68
|
+
publishedAt: string,
|
|
69
|
+
updatedAt: string,
|
|
70
|
+
): ArticleDateMeta {
|
|
71
|
+
const publishedDate = new Date(publishedAt);
|
|
72
|
+
const updatedDate = new Date(updatedAt);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
publishedDateLabel: format(publishedDate, "d MMMM yyyy"),
|
|
76
|
+
publishedRelativeLabel: formatDistanceToNow(publishedDate, {
|
|
77
|
+
addSuffix: true,
|
|
78
|
+
}),
|
|
79
|
+
updatedDateLabel: format(updatedDate, "d MMMM yyyy"),
|
|
80
|
+
isUpdated: updatedDate.getTime() !== publishedDate.getTime(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function extractTableOfContents(markdown: string): TocItem[] {
|
|
85
|
+
const items: TocItem[] = [];
|
|
86
|
+
let inFence = false;
|
|
87
|
+
|
|
88
|
+
for (const line of markdown.split("\n")) {
|
|
89
|
+
if (/^\s*```/.test(line)) {
|
|
90
|
+
inFence = !inFence;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (inFence) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const match = /^(#{2,3})\s+(.+?)\s*#*\s*$/.exec(line);
|
|
99
|
+
if (!match?.[1] || !match[2]) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const text = stripMarkdownInline(match[2]);
|
|
104
|
+
if (!text) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
items.push({
|
|
109
|
+
id: slugifyHeading(text),
|
|
110
|
+
text,
|
|
111
|
+
level: match[1].length as 2 | 3,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return items;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function formatTableOfContentsSummary(count: number): string {
|
|
119
|
+
return `${count} ${count === 1 ? "section" : "sections"}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function normalizeReadingProgress(progress: number): number {
|
|
123
|
+
if (!Number.isFinite(progress)) {
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return Math.min(100, Math.max(0, progress));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getTocItemState(
|
|
131
|
+
itemId: string,
|
|
132
|
+
activeItemId: string | null,
|
|
133
|
+
): TocItemState {
|
|
134
|
+
const isActive = itemId === activeItemId;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
isActive,
|
|
138
|
+
ariaCurrent: isActive ? "location" : undefined,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function stripMarkdownInline(value: string): string {
|
|
143
|
+
return value
|
|
144
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
145
|
+
.replace(/[*_`~]/g, "")
|
|
146
|
+
.replace(/<[^>]+>/g, "")
|
|
147
|
+
.replace(/\s+/g, " ")
|
|
148
|
+
.trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function slugifyHeading(value: string): string {
|
|
152
|
+
return value
|
|
153
|
+
.toLowerCase()
|
|
154
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
155
|
+
.trim()
|
|
156
|
+
.replace(/\s+/g, "-")
|
|
157
|
+
.replace(/-+/g, "-");
|
|
158
|
+
}
|
package/dist/lib/auth.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { type NextRequest } from "next/server";
|
|
2
|
+
import type { PublishResult } from "@/types/contracts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Authentication configuration interface
|
|
6
|
+
* Customers can implement this to customize authentication behavior
|
|
7
|
+
*/
|
|
8
|
+
export interface AuthConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Validate an authentication token
|
|
11
|
+
* @param token - The token to validate
|
|
12
|
+
* @returns Promise that resolves to validation result
|
|
13
|
+
*/
|
|
14
|
+
validateToken(token: string): Promise<AuthResult>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract token from request
|
|
18
|
+
* @param request - The incoming request
|
|
19
|
+
* @returns The extracted token or null if not found
|
|
20
|
+
*/
|
|
21
|
+
extractToken?(request: NextRequest): string | null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Custom error messages for authentication failures
|
|
25
|
+
*/
|
|
26
|
+
errorMessages?: {
|
|
27
|
+
missing?: string;
|
|
28
|
+
invalid?: string;
|
|
29
|
+
expired?: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Authentication result from token validation
|
|
35
|
+
*/
|
|
36
|
+
export interface AuthResult {
|
|
37
|
+
/** Whether the token is valid */
|
|
38
|
+
valid: boolean;
|
|
39
|
+
|
|
40
|
+
/** Error code if invalid */
|
|
41
|
+
errorCode?: "missing" | "invalid" | "expired";
|
|
42
|
+
|
|
43
|
+
/** Additional user/context information */
|
|
44
|
+
context?: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Default token extraction from Authorization header
|
|
49
|
+
* Supports both "Bearer <token>" and "Token <token>" formats
|
|
50
|
+
*/
|
|
51
|
+
export function extractTokenFromHeader(request: NextRequest): string | null {
|
|
52
|
+
const authHeader = request.headers.get("Authorization");
|
|
53
|
+
if (!authHeader) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Support both "Bearer <token>" and "Token <token>" formats
|
|
58
|
+
const bearerRegex = /^Bearer\s+(.+)$/i;
|
|
59
|
+
const bearerMatch = bearerRegex.exec(authHeader);
|
|
60
|
+
if (bearerMatch?.[1]) {
|
|
61
|
+
return bearerMatch[1];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const tokenRegex = /^Token\s+(.+)$/i;
|
|
65
|
+
const tokenMatch = tokenRegex.exec(authHeader);
|
|
66
|
+
if (tokenMatch?.[1]) {
|
|
67
|
+
return tokenMatch[1];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Simple API key based authentication implementation
|
|
75
|
+
* Customers can extend this or provide their own AuthConfig
|
|
76
|
+
*/
|
|
77
|
+
export class ApiKeyAuth implements AuthConfig {
|
|
78
|
+
private validApiKeys: Set<string>;
|
|
79
|
+
|
|
80
|
+
constructor(apiKeys: string[]) {
|
|
81
|
+
this.validApiKeys = new Set(apiKeys);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async validateToken(token: string): Promise<AuthResult> {
|
|
85
|
+
if (!token?.trim()) {
|
|
86
|
+
return {
|
|
87
|
+
valid: false,
|
|
88
|
+
errorCode: "missing",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (this.validApiKeys.has(token)) {
|
|
93
|
+
return {
|
|
94
|
+
valid: true,
|
|
95
|
+
context: {
|
|
96
|
+
tokenHash: this.hashToken(token),
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
valid: false,
|
|
103
|
+
errorCode: "invalid",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
extractToken = (request: NextRequest): string | null => {
|
|
108
|
+
return extractTokenFromHeader(request);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
private hashToken(token: string): string {
|
|
112
|
+
// Simple hash for logging/debugging (first 8 chars)
|
|
113
|
+
return token.substring(0, 8) + "...";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Environment variable based authentication
|
|
119
|
+
* Reads API keys from environment variables
|
|
120
|
+
*/
|
|
121
|
+
export class EnvAuth extends ApiKeyAuth {
|
|
122
|
+
constructor(envVarName = "BLOG_PLATFORM_API_KEYS") {
|
|
123
|
+
let apiKeysEnv = process.env[envVarName];
|
|
124
|
+
|
|
125
|
+
// Backwards compatibility: fall back to BLOG_AUTH_TOKEN when using default env var
|
|
126
|
+
if (!apiKeysEnv && envVarName === "BLOG_PLATFORM_API_KEYS") {
|
|
127
|
+
apiKeysEnv = process.env.BLOG_AUTH_TOKEN;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const apiKeys = apiKeysEnv ? apiKeysEnv.split(",").map((key) => key.trim()).filter(Boolean) : [];
|
|
131
|
+
|
|
132
|
+
if (apiKeys.length === 0) {
|
|
133
|
+
const hint = envVarName === "BLOG_PLATFORM_API_KEYS"
|
|
134
|
+
? " (set BLOG_PLATFORM_API_KEYS or BLOG_AUTH_TOKEN)"
|
|
135
|
+
: "";
|
|
136
|
+
console.warn(
|
|
137
|
+
`⚠️ No API keys found in environment variable ${envVarName}${hint}. Authentication will fail for all requests.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
super(apiKeys);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* No-op authentication for development/testing
|
|
147
|
+
* WARNING: Never use this in production!
|
|
148
|
+
*/
|
|
149
|
+
export class NoAuth implements AuthConfig {
|
|
150
|
+
async validateToken(): Promise<AuthResult> {
|
|
151
|
+
return {
|
|
152
|
+
valid: true,
|
|
153
|
+
context: {
|
|
154
|
+
development: true,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
extractToken(): string | null {
|
|
160
|
+
return "development-mode";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Uninitialized authentication - rejects all requests
|
|
166
|
+
* This is the secure default before initialization
|
|
167
|
+
*/
|
|
168
|
+
export class UninitializedAuth implements AuthConfig {
|
|
169
|
+
private hasWarned = false;
|
|
170
|
+
|
|
171
|
+
async validateToken(_token: string): Promise<AuthResult> {
|
|
172
|
+
if (!this.hasWarned) {
|
|
173
|
+
console.error(
|
|
174
|
+
"❌ SECURITY WARNING: Blog platform authentication not initialized! " +
|
|
175
|
+
"All requests will be rejected. Call initBlogPlatform() to configure authentication."
|
|
176
|
+
);
|
|
177
|
+
this.hasWarned = true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
valid: false,
|
|
182
|
+
errorCode: "invalid",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Don't override extractToken - use default header extraction
|
|
187
|
+
// Don't override error messages - use defaults so users see
|
|
188
|
+
// "Invalid authentication credentials" instead of confusing
|
|
189
|
+
// "platform not initialized" messages
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
declare global {
|
|
193
|
+
// Using `var` here intentionally to declare a global on Node's globalThis
|
|
194
|
+
// across module boundaries and hot reloads
|
|
195
|
+
var __BLOG_PLATFORM_AUTH_CONFIG__: AuthConfig | undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Global authentication configuration
|
|
200
|
+
* Defaults to UninitializedAuth for security (fail closed)
|
|
201
|
+
* Stored in globalThis to persist across Next.js dev mode hot reloads
|
|
202
|
+
*/
|
|
203
|
+
function getDefaultAuthConfig(): AuthConfig {
|
|
204
|
+
globalThis.__BLOG_PLATFORM_AUTH_CONFIG__ ??= new UninitializedAuth();
|
|
205
|
+
return globalThis.__BLOG_PLATFORM_AUTH_CONFIG__;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Set the authentication configuration
|
|
210
|
+
* Customers should call this in their app initialization
|
|
211
|
+
*/
|
|
212
|
+
export function setAuthConfig(config: AuthConfig): void {
|
|
213
|
+
globalThis.__BLOG_PLATFORM_AUTH_CONFIG__ = config;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get the current authentication configuration
|
|
218
|
+
*/
|
|
219
|
+
export function getAuthConfig(): AuthConfig {
|
|
220
|
+
return globalThis.__BLOG_PLATFORM_AUTH_CONFIG__ ?? getDefaultAuthConfig();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Initialize authentication with environment-based API keys
|
|
225
|
+
* This is a convenience function for common setups
|
|
226
|
+
*/
|
|
227
|
+
export function initEnvAuth(envVarName?: string): void {
|
|
228
|
+
setAuthConfig(new EnvAuth(envVarName));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Authenticate an incoming request
|
|
233
|
+
* @param request - The Next.js request object
|
|
234
|
+
* @returns Authentication result with token validation
|
|
235
|
+
*/
|
|
236
|
+
export async function authenticateRequest(request: NextRequest): Promise<{
|
|
237
|
+
authenticated: boolean;
|
|
238
|
+
result: AuthResult;
|
|
239
|
+
token: string | null;
|
|
240
|
+
}> {
|
|
241
|
+
const config = getAuthConfig();
|
|
242
|
+
|
|
243
|
+
// Extract token using configured method or default
|
|
244
|
+
const token = config.extractToken ? config.extractToken(request) : extractTokenFromHeader(request);
|
|
245
|
+
|
|
246
|
+
if (!token) {
|
|
247
|
+
return {
|
|
248
|
+
authenticated: false,
|
|
249
|
+
result: {
|
|
250
|
+
valid: false,
|
|
251
|
+
errorCode: "missing",
|
|
252
|
+
},
|
|
253
|
+
token: null,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Validate the token
|
|
258
|
+
const result = await config.validateToken(token);
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
authenticated: result.valid,
|
|
262
|
+
result,
|
|
263
|
+
token,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Create an authentication error response
|
|
269
|
+
* @param authResult - The failed authentication result
|
|
270
|
+
* @param idempotencyKey - Optional idempotency key from request
|
|
271
|
+
* @returns NextResponse with appropriate error
|
|
272
|
+
*/
|
|
273
|
+
export function createAuthErrorResponse(
|
|
274
|
+
authResult: AuthResult,
|
|
275
|
+
idempotencyKey = ""
|
|
276
|
+
): PublishResult {
|
|
277
|
+
const config = getAuthConfig();
|
|
278
|
+
const customMessages = config.errorMessages ?? {};
|
|
279
|
+
|
|
280
|
+
let message: string;
|
|
281
|
+
let code: string;
|
|
282
|
+
|
|
283
|
+
switch (authResult.errorCode) {
|
|
284
|
+
case "missing":
|
|
285
|
+
message = customMessages.missing ?? "Authentication token is required";
|
|
286
|
+
code = "PUB005";
|
|
287
|
+
break;
|
|
288
|
+
case "expired":
|
|
289
|
+
message = customMessages.expired ?? "Authentication token has expired";
|
|
290
|
+
code = "PUB005";
|
|
291
|
+
break;
|
|
292
|
+
case "invalid":
|
|
293
|
+
default:
|
|
294
|
+
message = customMessages.invalid ?? "Invalid authentication credentials";
|
|
295
|
+
code = "PUB005";
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
success: false,
|
|
301
|
+
error: {
|
|
302
|
+
code,
|
|
303
|
+
message,
|
|
304
|
+
details: {
|
|
305
|
+
authError: authResult.errorCode ?? "invalid",
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
idempotencyKey,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Middleware function to protect API endpoints
|
|
314
|
+
* Usage in route handlers:
|
|
315
|
+
*
|
|
316
|
+
* ```typescript
|
|
317
|
+
* export async function POST(request: NextRequest) {
|
|
318
|
+
* const authCheck = await requireAuth(request);
|
|
319
|
+
* if (!authCheck.authenticated) {
|
|
320
|
+
* return NextResponse.json(authCheck.error, { status: 401 });
|
|
321
|
+
* }
|
|
322
|
+
*
|
|
323
|
+
* // Continue with authenticated request...
|
|
324
|
+
* }
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
export async function requireAuth(request: NextRequest): Promise<{
|
|
328
|
+
authenticated: boolean;
|
|
329
|
+
error?: PublishResult;
|
|
330
|
+
context?: Record<string, unknown>;
|
|
331
|
+
}> {
|
|
332
|
+
const auth = await authenticateRequest(request);
|
|
333
|
+
|
|
334
|
+
if (!auth.authenticated) {
|
|
335
|
+
return {
|
|
336
|
+
authenticated: false,
|
|
337
|
+
error: createAuthErrorResponse(auth.result),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
authenticated: true,
|
|
343
|
+
context: auth.result.context,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Utility to check if current environment should skip authentication
|
|
349
|
+
* Useful for development and testing
|
|
350
|
+
*/
|
|
351
|
+
export function shouldSkipAuth(): boolean {
|
|
352
|
+
const skipAuth = process.env.SKIP_BLOG_PLATFORM_AUTH;
|
|
353
|
+
return skipAuth === "true" || skipAuth === "1" || process.env.NODE_ENV === "test";
|
|
354
|
+
}
|