nca-ai-cms-astro-plugin 1.0.14 → 1.0.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nca-ai-cms-astro-plugin",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -1,14 +1,35 @@
1
1
  import type { APIRoute } from 'astro';
2
2
  import { z } from 'zod';
3
3
  import { jsonResponse, jsonError } from '../_utils.js';
4
- import { getEnvVariable } from '../../utils/envUtils.js';
4
+ import { verifyCredentials } from '../../utils/credentialUtils.js';
5
+ import { createSession, purgeExpiredSessions } from '../../services/SessionService.js';
6
+ import { loginRateLimiter } from '../../utils/loginRateLimiter.js';
5
7
 
6
8
  const loginSchema = z.object({
7
9
  username: z.string().min(1),
8
10
  password: z.string().min(1),
9
11
  });
10
12
 
11
- export const POST: APIRoute = async ({ request, cookies }) => {
13
+ export const POST: APIRoute = async ({ request, cookies, clientAddress }) => {
14
+ const ip =
15
+ clientAddress ??
16
+ request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??
17
+ 'unknown';
18
+
19
+ const { limited, retryAfter } = loginRateLimiter.check(ip);
20
+ if (limited) {
21
+ return new Response(
22
+ JSON.stringify({ error: 'Too many login attempts. Try again later.' }),
23
+ {
24
+ status: 429,
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ 'Retry-After': String(retryAfter),
28
+ },
29
+ },
30
+ );
31
+ }
32
+
12
33
  let body: unknown;
13
34
  try {
14
35
  body = await request.json();
@@ -22,14 +43,18 @@ export const POST: APIRoute = async ({ request, cookies }) => {
22
43
  }
23
44
 
24
45
  const { username, password } = result.data;
25
- const expectedUsername = getEnvVariable('EDITOR_ADMIN');
26
- const expectedPassword = getEnvVariable('EDITOR_PASSWORD');
27
46
 
28
- if (username !== expectedUsername || password !== expectedPassword) {
47
+ if (!verifyCredentials(username, password)) {
48
+ loginRateLimiter.record(ip);
49
+ console.warn(
50
+ `[nca-ai-cms] Failed login attempt from ${ip} at ${new Date().toISOString()}`,
51
+ );
29
52
  return jsonError('Invalid credentials', 401);
30
53
  }
31
54
 
32
- const token = btoa(`${username}:${password}`);
55
+ loginRateLimiter.clear(ip);
56
+ await purgeExpiredSessions();
57
+ const token = await createSession();
33
58
 
34
59
  cookies.set('editor-auth', token, {
35
60
  httpOnly: true,
@@ -1,6 +1,10 @@
1
1
  import type { APIRoute } from 'astro';
2
+ import { deleteSession } from '../../services/SessionService.js';
2
3
 
3
4
  export const POST: APIRoute = async ({ cookies, redirect }) => {
5
+ const token = cookies.get('editor-auth')?.value;
6
+ if (token) await deleteSession(token);
7
+
4
8
  cookies.delete('editor-auth', { path: '/' });
5
9
  return redirect('/login', 302);
6
10
  };
@@ -0,0 +1,42 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { z } from 'zod';
3
+ import { ContentGenerator } from '../services/ContentGenerator';
4
+ import { PromptService } from '../services/PromptService';
5
+ import { getEnvVariable } from '../utils/envUtils';
6
+ import { jsonResponse, jsonError } from './_utils';
7
+
8
+ const GeneratePageSchema = z.object({
9
+ input: z.string().min(1, 'Input is required'),
10
+ });
11
+
12
+ export const POST: APIRoute = async ({ request }) => {
13
+ try {
14
+ const body = await request.json();
15
+ const parsed = GeneratePageSchema.safeParse(body);
16
+ if (!parsed.success) {
17
+ return jsonError(parsed.error.errors[0]?.message ?? 'Invalid request', 400);
18
+ }
19
+ const { input } = parsed.data;
20
+ const isUrl = /^https?:\/\//.test(input);
21
+
22
+ const apiKey = getEnvVariable('GOOGLE_GEMINI_API_KEY');
23
+ const promptService = new PromptService();
24
+ const generator = new ContentGenerator({ apiKey, promptService });
25
+ const page = isUrl
26
+ ? await generator.generateFromUrl(input)
27
+ : await generator.generateFromKeywords(input);
28
+
29
+ return jsonResponse({
30
+ title: page.title,
31
+ description: page.description,
32
+ content: page.content,
33
+ filepath: page.filepath,
34
+ tags: page.tags,
35
+ date: page.date.toISOString(),
36
+ ...(generator.warnings.length > 0 ? { warnings: generator.warnings } : {}),
37
+ });
38
+ } catch (error) {
39
+ console.error('Page generation error:', error);
40
+ return jsonError(error);
41
+ }
42
+ };
@@ -0,0 +1,61 @@
1
+ import type { APIRoute } from 'astro';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ import matter from 'gray-matter';
5
+ import {
6
+ ArticleService,
7
+ ArticleNotFoundError,
8
+ } from '../../../services/ArticleService';
9
+ import { convertToWebP } from '../../../services/ImageConverter';
10
+ import { pagesPath } from 'virtual:nca-ai-cms/config';
11
+ import { jsonResponse, jsonError } from '../../_utils';
12
+
13
+ interface ApplyRequest {
14
+ title?: string;
15
+ description?: string;
16
+ content?: string;
17
+ tags?: string[];
18
+ imageUrl?: string;
19
+ imageAlt?: string;
20
+ }
21
+
22
+ export const POST: APIRoute = async ({ params, request }) => {
23
+ try {
24
+ const slug = params.id;
25
+ if (!slug) return jsonError('Page ID required', 400);
26
+
27
+ const data: ApplyRequest = await request.json();
28
+ const service = new ArticleService(pagesPath);
29
+ const existingPage = await service.read(slug);
30
+ if (!existingPage) throw new ArticleNotFoundError(slug);
31
+
32
+ if (data.imageUrl) {
33
+ const heroPath = path.join(existingPage.folderPath, 'hero.webp');
34
+ const base64Data = data.imageUrl.replace(/^data:image\/\w+;base64,/, '');
35
+ await convertToWebP(base64Data, heroPath);
36
+
37
+ if (data.imageAlt) {
38
+ const indexPath = path.join(existingPage.folderPath, 'index.md');
39
+ const fileContent = await fs.readFile(indexPath, 'utf-8');
40
+ const { data: frontmatter, content } = matter(fileContent);
41
+ frontmatter.imageAlt = data.imageAlt;
42
+ const updatedContent = matter.stringify(content, frontmatter);
43
+ await fs.writeFile(indexPath, updatedContent);
44
+ }
45
+ }
46
+
47
+ if (data.content || data.title || data.description) {
48
+ await service.updateContent(slug, {
49
+ ...(data.title && { title: data.title }),
50
+ ...(data.description && { description: data.description }),
51
+ ...(data.content && { content: data.content }),
52
+ });
53
+ }
54
+
55
+ return jsonResponse({ success: true, articleId: existingPage.articleId });
56
+ } catch (error) {
57
+ if (error instanceof ArticleNotFoundError) return jsonError(error, 404);
58
+ console.error('Apply page changes error:', error);
59
+ return jsonError(error);
60
+ }
61
+ };
@@ -0,0 +1,35 @@
1
+ import type { APIRoute } from 'astro';
2
+ import {
3
+ ArticleService,
4
+ ArticleNotFoundError,
5
+ } from '../../../services/ArticleService';
6
+ import { ImageGenerator } from '../../../services/ImageGenerator';
7
+ import { getEnvVariable } from '../../../utils/envUtils';
8
+ import { pagesPath } from 'virtual:nca-ai-cms/config';
9
+ import { jsonResponse, jsonError } from '../../_utils';
10
+
11
+ export const POST: APIRoute = async ({ params }) => {
12
+ try {
13
+ const slug = params.id;
14
+ if (!slug) return jsonError('Page ID required', 400);
15
+
16
+ const service = new ArticleService(pagesPath);
17
+ const existingPage = await service.read(slug);
18
+ if (!existingPage) throw new ArticleNotFoundError(slug);
19
+
20
+ const apiKey = getEnvVariable('GOOGLE_GEMINI_API_KEY');
21
+ const generator = new ImageGenerator({ apiKey });
22
+ const image = await generator.generate(existingPage.title);
23
+
24
+ return jsonResponse({
25
+ url: image.url,
26
+ alt: image.alt,
27
+ articleId: existingPage.articleId,
28
+ articleTitle: existingPage.title,
29
+ });
30
+ } catch (error) {
31
+ if (error instanceof ArticleNotFoundError) return jsonError(error, 404);
32
+ console.error('Regenerate page image error:', error);
33
+ return jsonError(error);
34
+ }
35
+ };
@@ -0,0 +1,39 @@
1
+ import type { APIRoute } from 'astro';
2
+ import {
3
+ ArticleService,
4
+ ArticleNotFoundError,
5
+ } from '../../../services/ArticleService';
6
+ import { ContentGenerator } from '../../../services/ContentGenerator';
7
+ import { PromptService } from '../../../services/PromptService';
8
+ import { getEnvVariable } from '../../../utils/envUtils';
9
+ import { pagesPath } from 'virtual:nca-ai-cms/config';
10
+ import { jsonResponse, jsonError } from '../../_utils';
11
+
12
+ export const POST: APIRoute = async ({ params }) => {
13
+ try {
14
+ const slug = params.id;
15
+ if (!slug) return jsonError('Page ID required', 400);
16
+
17
+ const service = new ArticleService(pagesPath);
18
+ const existingPage = await service.read(slug);
19
+ if (!existingPage) throw new ArticleNotFoundError(slug);
20
+
21
+ const apiKey = getEnvVariable('GOOGLE_GEMINI_API_KEY');
22
+ const promptService = new PromptService();
23
+ const generator = new ContentGenerator({ apiKey, promptService });
24
+ const newPage = await generator.generateFromKeywords(existingPage.title);
25
+
26
+ return jsonResponse({
27
+ title: newPage.title,
28
+ description: newPage.description,
29
+ content: newPage.content,
30
+ tags: newPage.tags,
31
+ originalTitle: existingPage.title,
32
+ articleId: existingPage.articleId,
33
+ });
34
+ } catch (error) {
35
+ if (error instanceof ArticleNotFoundError) return jsonError(error, 404);
36
+ console.error('Regenerate page text error:', error);
37
+ return jsonError(error);
38
+ }
39
+ };
@@ -0,0 +1,40 @@
1
+ import type { APIRoute } from 'astro';
2
+ import {
3
+ ArticleService,
4
+ ArticleNotFoundError,
5
+ } from '../../services/ArticleService';
6
+ import { pagesPath } from 'virtual:nca-ai-cms/config';
7
+ import { jsonResponse, jsonError } from '../_utils';
8
+
9
+ // GET /api/pages/[id] - Get page details
10
+ export const GET: APIRoute = async ({ params }) => {
11
+ try {
12
+ const slug = params.id;
13
+ if (!slug) return jsonError('Page ID required', 400);
14
+
15
+ const service = new ArticleService(pagesPath);
16
+ const page = await service.read(slug);
17
+ if (!page) return jsonError('Page not found', 404);
18
+
19
+ return jsonResponse(page);
20
+ } catch (error) {
21
+ console.error('Read page error:', error);
22
+ return jsonError(error);
23
+ }
24
+ };
25
+
26
+ // DELETE /api/pages/[id] - Delete a page by slug
27
+ export const DELETE: APIRoute = async ({ params }) => {
28
+ try {
29
+ const slug = params.id;
30
+ if (!slug) return jsonError('Page ID required', 400);
31
+
32
+ const service = new ArticleService(pagesPath);
33
+ await service.delete(slug);
34
+ return jsonResponse({ success: true });
35
+ } catch (error) {
36
+ if (error instanceof ArticleNotFoundError) return jsonError(error, 404);
37
+ console.error('Delete page error:', error);
38
+ return jsonError(error);
39
+ }
40
+ };
@@ -0,0 +1,16 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { ArticleService } from '../../services/ArticleService';
3
+ import { pagesPath } from 'virtual:nca-ai-cms/config';
4
+ import { jsonResponse, jsonError } from '../_utils';
5
+
6
+ // GET /api/pages - List all pages
7
+ export const GET: APIRoute = async () => {
8
+ try {
9
+ const service = new ArticleService(pagesPath);
10
+ const pages = await service.list();
11
+ return jsonResponse(pages);
12
+ } catch (error) {
13
+ console.error('List pages error:', error);
14
+ return jsonError(error);
15
+ }
16
+ };
@@ -0,0 +1,50 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { z } from 'zod';
3
+ import { Article } from '../domain/entities/Article';
4
+ import { FileWriter } from '../services/FileWriter';
5
+ import { pagesPath } from 'virtual:nca-ai-cms/config';
6
+ import { jsonResponse, jsonError } from './_utils';
7
+
8
+ const SavePageSchema = z.object({
9
+ title: z.string().min(1),
10
+ description: z.string().min(1),
11
+ content: z.string().min(1),
12
+ date: z.string().optional(),
13
+ tags: z.array(z.string()).optional(),
14
+ imageAlt: z.string().optional(),
15
+ });
16
+
17
+ export const POST: APIRoute = async ({ request }) => {
18
+ try {
19
+ const body = await request.json();
20
+ const parsed = SavePageSchema.safeParse(body);
21
+ if (!parsed.success) {
22
+ return jsonError(parsed.error.errors[0]?.message ?? 'Invalid request', 400);
23
+ }
24
+ const data = parsed.data;
25
+
26
+ const page = new Article({
27
+ title: data.title,
28
+ description: data.description,
29
+ content: data.content,
30
+ date: new Date(data.date || Date.now()),
31
+ tags: data.tags || [],
32
+ image: './hero.webp',
33
+ imageAlt: data.imageAlt,
34
+ contentPath: pagesPath,
35
+ flatPath: true,
36
+ });
37
+
38
+ const writer = new FileWriter();
39
+ const result = await writer.write(page);
40
+
41
+ return jsonResponse({
42
+ success: true,
43
+ filepath: result.filepath,
44
+ folderPath: page.folderPath,
45
+ });
46
+ } catch (error) {
47
+ console.error('Save page error:', error);
48
+ return jsonError(error);
49
+ }
50
+ };
@@ -9,10 +9,12 @@ import {
9
9
  } from './editor/GenerateTab';
10
10
  import { SettingsTab } from './editor/SettingsTab';
11
11
  import { PlannerTab } from './editor/PlannerTab';
12
+ import { PagesTab } from './editor/PagesTab';
12
13
 
13
14
  const TABS: { key: TabType; label: string }[] = [
14
15
  { key: 'generate', label: 'Generieren' },
15
16
  { key: 'planner', label: 'Planer' },
17
+ { key: 'pages', label: 'Seiten' },
16
18
  { key: 'settings', label: 'Einstellungen' },
17
19
  ];
18
20
 
@@ -100,6 +102,17 @@ export default function Editor() {
100
102
  </div>
101
103
  )}
102
104
 
105
+ {activeTab === 'pages' && (
106
+ <div
107
+ id="panel-pages"
108
+ role="tabpanel"
109
+ aria-labelledby="tab-pages"
110
+ style={isFullWidth ? styles.panelFullWidth : undefined}
111
+ >
112
+ <PagesTab />
113
+ </div>
114
+ )}
115
+
103
116
  {activeTab === 'settings' && (
104
117
  <div
105
118
  id="panel-settings"
@@ -0,0 +1,231 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { styles } from './styles';
3
+
4
+ interface PageData {
5
+ articleId: string;
6
+ title: string;
7
+ description: string;
8
+ date: string;
9
+ tags: string[];
10
+ image?: string;
11
+ imageAlt?: string;
12
+ }
13
+
14
+ export function PagesTab() {
15
+ const [pages, setPages] = useState<PageData[]>([]);
16
+ const [loading, setLoading] = useState(true);
17
+ const [error, setError] = useState<string | null>(null);
18
+ const [generating, setGenerating] = useState(false);
19
+ const [newInput, setNewInput] = useState('');
20
+ const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
21
+
22
+ const loadPages = useCallback(async () => {
23
+ setLoading(true);
24
+ setError(null);
25
+ try {
26
+ const res = await fetch('/api/pages');
27
+ if (!res.ok) throw new Error('Fehler beim Laden der Seiten');
28
+ const data = (await res.json()) as PageData[];
29
+ setPages(data);
30
+ } catch (err) {
31
+ setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
32
+ } finally {
33
+ setLoading(false);
34
+ }
35
+ }, []);
36
+
37
+ useEffect(() => {
38
+ loadPages();
39
+ }, [loadPages]);
40
+
41
+ const handleGenerate = async () => {
42
+ if (!newInput.trim()) return;
43
+ setGenerating(true);
44
+ setError(null);
45
+ try {
46
+ // Generate content
47
+ const contentRes = await fetch('/api/generate-page', {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json' },
50
+ body: JSON.stringify({ input: newInput }),
51
+ });
52
+ if (!contentRes.ok) throw new Error('Fehler bei der Seitengenerierung');
53
+ const content = await contentRes.json();
54
+
55
+ // Generate image
56
+ const imageRes = await fetch('/api/generate-image', {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify({ title: content.title }),
60
+ });
61
+ const image = imageRes.ok ? await imageRes.json() : null;
62
+
63
+ // Save page
64
+ const saveRes = await fetch('/api/save-page', {
65
+ method: 'POST',
66
+ headers: { 'Content-Type': 'application/json' },
67
+ body: JSON.stringify({
68
+ title: content.title,
69
+ description: content.description,
70
+ content: content.content,
71
+ tags: content.tags,
72
+ date: content.date,
73
+ imageAlt: image?.alt,
74
+ }),
75
+ });
76
+ if (!saveRes.ok) throw new Error('Fehler beim Speichern der Seite');
77
+ const saveResult = await saveRes.json();
78
+
79
+ // Save image if generated
80
+ if (image?.url && saveResult.folderPath) {
81
+ await fetch('/api/save-image', {
82
+ method: 'POST',
83
+ headers: { 'Content-Type': 'application/json' },
84
+ body: JSON.stringify({
85
+ imageUrl: image.url,
86
+ folderPath: saveResult.folderPath,
87
+ }),
88
+ });
89
+ }
90
+
91
+ setNewInput('');
92
+ await loadPages();
93
+ } catch (err) {
94
+ setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
95
+ } finally {
96
+ setGenerating(false);
97
+ }
98
+ };
99
+
100
+ const handleDelete = async (slug: string) => {
101
+ try {
102
+ const res = await fetch(`/api/pages/${slug}`, {
103
+ method: 'DELETE',
104
+ credentials: 'same-origin',
105
+ });
106
+ if (!res.ok) throw new Error('Fehler beim Löschen');
107
+ setDeleteConfirm(null);
108
+ await loadPages();
109
+ } catch (err) {
110
+ setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
111
+ }
112
+ };
113
+
114
+ if (loading) {
115
+ return <div style={styles.loadingBox}>Seiten werden geladen...</div>;
116
+ }
117
+
118
+ return (
119
+ <div style={styles.plannerContent}>
120
+ {/* Generate new page form */}
121
+ <div style={styles.plannerForm}>
122
+ <div style={styles.addFormTitle}>Neue Seite generieren</div>
123
+ <div style={{ display: 'flex', gap: '0.75rem', alignItems: 'end' }}>
124
+ <div style={{ flex: 1 }}>
125
+ <label style={styles.addFormLabel}>Thema, Keywords oder URL</label>
126
+ <input
127
+ type="text"
128
+ value={newInput}
129
+ onChange={(e) => setNewInput(e.target.value)}
130
+ placeholder="z.B. Laserreinigung, Gleitschleifen..."
131
+ style={styles.addFormInput}
132
+ onKeyDown={(e) => e.key === 'Enter' && handleGenerate()}
133
+ />
134
+ </div>
135
+ <button
136
+ type="button"
137
+ style={styles.addButton}
138
+ onClick={handleGenerate}
139
+ disabled={generating || !newInput.trim()}
140
+ >
141
+ {generating ? 'Generiert...' : 'Seite erstellen'}
142
+ </button>
143
+ </div>
144
+ </div>
145
+
146
+ {error && <div style={styles.error}>{error}</div>}
147
+
148
+ {/* Page list */}
149
+ {pages.length === 0 ? (
150
+ <div style={styles.emptyState}>
151
+ Noch keine Seiten vorhanden. Erstellen Sie Ihre erste Seite!
152
+ </div>
153
+ ) : (
154
+ <div style={styles.plannerList}>
155
+ {pages.map((page) => (
156
+ <div key={page.articleId} style={styles.plannerCard}>
157
+ <div style={styles.plannerCardHeader}>
158
+ <div>
159
+ <div style={{ fontSize: '0.95rem', fontWeight: 600, color: 'var(--color-text, #faf9f7)' }}>
160
+ {page.title}
161
+ </div>
162
+ <div style={{ fontSize: '0.8rem', color: 'var(--color-text-muted, #b8b5b0)', marginTop: '0.25rem' }}>
163
+ {page.description}
164
+ </div>
165
+ </div>
166
+ <div style={styles.plannerCardActions}>
167
+ <a
168
+ href={`/services/${page.articleId}`}
169
+ style={{
170
+ ...styles.editButton,
171
+ textDecoration: 'none',
172
+ display: 'inline-flex',
173
+ alignItems: 'center',
174
+ }}
175
+ >
176
+ Ansehen
177
+ </a>
178
+ {deleteConfirm === page.articleId ? (
179
+ <>
180
+ <button
181
+ type="button"
182
+ style={styles.deleteButton}
183
+ onClick={() => handleDelete(page.articleId)}
184
+ >
185
+ Bestätigen
186
+ </button>
187
+ <button
188
+ type="button"
189
+ style={styles.cancelButton}
190
+ onClick={() => setDeleteConfirm(null)}
191
+ >
192
+ Abbrechen
193
+ </button>
194
+ </>
195
+ ) : (
196
+ <button
197
+ type="button"
198
+ style={styles.deleteButton}
199
+ onClick={() => setDeleteConfirm(page.articleId)}
200
+ >
201
+ Löschen
202
+ </button>
203
+ )}
204
+ </div>
205
+ </div>
206
+ {page.tags && page.tags.length > 0 && (
207
+ <div style={{ padding: '0.75rem 1.25rem', display: 'flex', gap: '0.5rem', flexWrap: 'wrap' as const }}>
208
+ {page.tags.map((tag) => (
209
+ <span
210
+ key={tag}
211
+ style={{
212
+ background: 'var(--color-surface-accent, #222226)',
213
+ border: '1px solid var(--color-border, #2a2a2e)',
214
+ borderRadius: '4px',
215
+ padding: '0.15rem 0.5rem',
216
+ fontSize: '0.75rem',
217
+ color: 'var(--color-text-muted, #b8b5b0)',
218
+ }}
219
+ >
220
+ {tag}
221
+ </span>
222
+ ))}
223
+ </div>
224
+ )}
225
+ </div>
226
+ ))}
227
+ </div>
228
+ )}
229
+ </div>
230
+ );
231
+ }
@@ -1,4 +1,4 @@
1
- export type TabType = 'generate' | 'planner' | 'settings';
1
+ export type TabType = 'generate' | 'planner' | 'pages' | 'settings';
2
2
 
3
3
  export type SettingsSubTab =
4
4
  | 'homepage'
package/src/config.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  declare module 'virtual:nca-ai-cms/config' {
2
2
  export const contentPath: string;
3
+ export const pagesPath: string;
3
4
  export const autoPublish: boolean;
4
5
  }
package/src/db/config.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { defineDb } from 'astro:db';
2
- import { SiteSettings, Prompts, ScheduledPosts } from './tables.js';
2
+ import { SiteSettings, Prompts, ScheduledPosts, Sessions } from './tables.js';
3
3
 
4
4
  export default defineDb({
5
- tables: { SiteSettings, Prompts, ScheduledPosts },
5
+ tables: { SiteSettings, Prompts, ScheduledPosts, Sessions },
6
6
  });
package/src/db/tables.ts CHANGED
@@ -37,4 +37,12 @@ const Prompts = defineTable({
37
37
  },
38
38
  });
39
39
 
40
- export { SiteSettings, Prompts, ScheduledPosts };
40
+ const Sessions = defineTable({
41
+ columns: {
42
+ token: column.text({ primaryKey: true }),
43
+ createdAt: column.date({ default: new Date() }),
44
+ expiresAt: column.date(),
45
+ },
46
+ });
47
+
48
+ export { SiteSettings, Prompts, ScheduledPosts, Sessions };