create-pxlr 1.0.0

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.
Files changed (153) hide show
  1. package/README.md +160 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +264 -0
  4. package/package.json +51 -0
  5. package/templates/blog/frontend/app/blog/[slug]/page.tsx +175 -0
  6. package/templates/blog/frontend/app/blog/page.tsx +102 -0
  7. package/templates/blog/frontend/app/components/footer.tsx +21 -0
  8. package/templates/blog/frontend/app/components/header.tsx +45 -0
  9. package/templates/blog/frontend/app/globals.css +30 -0
  10. package/templates/blog/frontend/app/layout.tsx +38 -0
  11. package/templates/blog/frontend/app/lib/cms.ts +71 -0
  12. package/templates/blog/frontend/app/page.tsx +155 -0
  13. package/templates/blog/frontend/next.config.ts +16 -0
  14. package/templates/blog/frontend/package.json +24 -0
  15. package/templates/blog/frontend/postcss.config.mjs +7 -0
  16. package/templates/blog/frontend/tsconfig.json +23 -0
  17. package/templates/blog/pxlr-cms/README.md +188 -0
  18. package/templates/blog/pxlr-cms/docker-compose.yml +132 -0
  19. package/templates/blog/pxlr-cms/nginx/nginx.conf +107 -0
  20. package/templates/blog/pxlr-cms/packages/admin/.dockerignore +4 -0
  21. package/templates/blog/pxlr-cms/packages/admin/.env.example +2 -0
  22. package/templates/blog/pxlr-cms/packages/admin/Dockerfile +19 -0
  23. package/templates/blog/pxlr-cms/packages/admin/next-env.d.ts +6 -0
  24. package/templates/blog/pxlr-cms/packages/admin/next.config.ts +22 -0
  25. package/templates/blog/pxlr-cms/packages/admin/package.json +63 -0
  26. package/templates/blog/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
  27. package/templates/blog/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
  28. package/templates/blog/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
  29. package/templates/blog/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
  30. package/templates/blog/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
  31. package/templates/blog/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
  32. package/templates/blog/pxlr-cms/packages/admin/src/app/globals.css +132 -0
  33. package/templates/blog/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
  34. package/templates/blog/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
  35. package/templates/blog/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
  36. package/templates/blog/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
  37. package/templates/blog/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
  38. package/templates/blog/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
  39. package/templates/blog/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
  40. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
  41. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
  42. package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
  43. package/templates/blog/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
  44. package/templates/blog/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
  45. package/templates/blog/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
  46. package/templates/blog/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
  47. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
  48. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
  49. package/templates/blog/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
  50. package/templates/blog/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
  51. package/templates/blog/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
  52. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
  53. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
  54. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
  55. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
  56. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
  57. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
  58. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
  59. package/templates/blog/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
  60. package/templates/blog/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
  61. package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
  62. package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
  63. package/templates/blog/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
  64. package/templates/blog/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
  65. package/templates/blog/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
  66. package/templates/blog/pxlr-cms/packages/admin/tsconfig.json +27 -0
  67. package/templates/blog/pxlr-cms/packages/api/.env.example +23 -0
  68. package/templates/blog/pxlr-cms/packages/api/Dockerfile +26 -0
  69. package/templates/blog/pxlr-cms/packages/api/package.json +42 -0
  70. package/templates/blog/pxlr-cms/packages/api/src/config.ts +39 -0
  71. package/templates/blog/pxlr-cms/packages/api/src/database/index.ts +60 -0
  72. package/templates/blog/pxlr-cms/packages/api/src/database/init.sql +258 -0
  73. package/templates/blog/pxlr-cms/packages/api/src/database/redis.ts +95 -0
  74. package/templates/blog/pxlr-cms/packages/api/src/database/seed.sql +78 -0
  75. package/templates/blog/pxlr-cms/packages/api/src/index.ts +157 -0
  76. package/templates/blog/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
  77. package/templates/blog/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
  78. package/templates/blog/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
  79. package/templates/blog/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
  80. package/templates/blog/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
  81. package/templates/blog/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
  82. package/templates/blog/pxlr-cms/packages/api/tsconfig.json +24 -0
  83. package/templates/blog/pxlr-cms/packages/shared/package.json +14 -0
  84. package/templates/blog/pxlr-cms/packages/shared/src/types/index.ts +139 -0
  85. package/templates/blog/pxlr-cms/packages/shared/tsconfig.json +18 -0
  86. package/templates/clean/pxlr-cms/README.md +188 -0
  87. package/templates/clean/pxlr-cms/docker-compose.yml +132 -0
  88. package/templates/clean/pxlr-cms/nginx/nginx.conf +107 -0
  89. package/templates/clean/pxlr-cms/packages/admin/.dockerignore +4 -0
  90. package/templates/clean/pxlr-cms/packages/admin/.env.example +2 -0
  91. package/templates/clean/pxlr-cms/packages/admin/Dockerfile +19 -0
  92. package/templates/clean/pxlr-cms/packages/admin/next-env.d.ts +6 -0
  93. package/templates/clean/pxlr-cms/packages/admin/next.config.ts +22 -0
  94. package/templates/clean/pxlr-cms/packages/admin/package.json +63 -0
  95. package/templates/clean/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
  96. package/templates/clean/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
  97. package/templates/clean/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
  98. package/templates/clean/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
  99. package/templates/clean/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
  100. package/templates/clean/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
  101. package/templates/clean/pxlr-cms/packages/admin/src/app/globals.css +132 -0
  102. package/templates/clean/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
  103. package/templates/clean/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
  104. package/templates/clean/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
  105. package/templates/clean/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
  106. package/templates/clean/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
  107. package/templates/clean/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
  108. package/templates/clean/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
  109. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
  110. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
  111. package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
  112. package/templates/clean/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
  113. package/templates/clean/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
  114. package/templates/clean/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
  115. package/templates/clean/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
  116. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
  117. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
  118. package/templates/clean/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
  119. package/templates/clean/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
  120. package/templates/clean/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
  121. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
  122. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
  123. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
  124. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
  125. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
  126. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
  127. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
  128. package/templates/clean/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
  129. package/templates/clean/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
  130. package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
  131. package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
  132. package/templates/clean/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
  133. package/templates/clean/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
  134. package/templates/clean/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
  135. package/templates/clean/pxlr-cms/packages/admin/tsconfig.json +27 -0
  136. package/templates/clean/pxlr-cms/packages/api/.env.example +23 -0
  137. package/templates/clean/pxlr-cms/packages/api/Dockerfile +26 -0
  138. package/templates/clean/pxlr-cms/packages/api/package.json +42 -0
  139. package/templates/clean/pxlr-cms/packages/api/src/config.ts +39 -0
  140. package/templates/clean/pxlr-cms/packages/api/src/database/index.ts +60 -0
  141. package/templates/clean/pxlr-cms/packages/api/src/database/init.sql +178 -0
  142. package/templates/clean/pxlr-cms/packages/api/src/database/redis.ts +95 -0
  143. package/templates/clean/pxlr-cms/packages/api/src/index.ts +157 -0
  144. package/templates/clean/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
  145. package/templates/clean/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
  146. package/templates/clean/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
  147. package/templates/clean/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
  148. package/templates/clean/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
  149. package/templates/clean/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
  150. package/templates/clean/pxlr-cms/packages/api/tsconfig.json +24 -0
  151. package/templates/clean/pxlr-cms/packages/shared/package.json +14 -0
  152. package/templates/clean/pxlr-cms/packages/shared/src/types/index.ts +139 -0
  153. package/templates/clean/pxlr-cms/packages/shared/tsconfig.json +18 -0
@@ -0,0 +1,258 @@
1
+ -- PXLR CMS Database Schema
2
+ -- PostgreSQL initialization script
3
+
4
+ -- Enable UUID extension
5
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
6
+ CREATE EXTENSION IF NOT EXISTS "pgcrypto";
7
+
8
+ -- Users table
9
+ CREATE TABLE IF NOT EXISTS users (
10
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
11
+ email VARCHAR(255) UNIQUE NOT NULL,
12
+ password_hash VARCHAR(255) NOT NULL,
13
+ name VARCHAR(255),
14
+ role VARCHAR(50) DEFAULT 'editor' CHECK (role IN ('admin', 'editor', 'viewer')),
15
+ avatar_url TEXT,
16
+ is_active BOOLEAN DEFAULT true,
17
+ last_login_at TIMESTAMP WITH TIME ZONE,
18
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
19
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
20
+ );
21
+
22
+ -- Schema definitions table
23
+ CREATE TABLE IF NOT EXISTS schemas (
24
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
25
+ name VARCHAR(255) UNIQUE NOT NULL,
26
+ title VARCHAR(255) NOT NULL,
27
+ description TEXT,
28
+ definition JSONB NOT NULL,
29
+ icon VARCHAR(50),
30
+ is_singleton BOOLEAN DEFAULT false,
31
+ sort_order INTEGER DEFAULT 0,
32
+ version INTEGER DEFAULT 1,
33
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
34
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
35
+ );
36
+
37
+ -- Documents table (main content storage)
38
+ CREATE TABLE IF NOT EXISTS documents (
39
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
40
+ schema_name VARCHAR(255) NOT NULL REFERENCES schemas(name) ON DELETE RESTRICT,
41
+ data JSONB NOT NULL DEFAULT '{}',
42
+ locale VARCHAR(10) DEFAULT 'en',
43
+ status VARCHAR(50) DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')),
44
+ published_at TIMESTAMP WITH TIME ZONE,
45
+ created_by UUID REFERENCES users(id),
46
+ updated_by UUID REFERENCES users(id),
47
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
48
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
49
+ );
50
+
51
+ -- Document versions table (for version history)
52
+ CREATE TABLE IF NOT EXISTS document_versions (
53
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
54
+ document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
55
+ version INTEGER NOT NULL,
56
+ data JSONB NOT NULL,
57
+ locale VARCHAR(10) DEFAULT 'en',
58
+ change_summary TEXT,
59
+ created_by UUID REFERENCES users(id),
60
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
61
+ UNIQUE(document_id, version, locale)
62
+ );
63
+
64
+ -- Media files table
65
+ CREATE TABLE IF NOT EXISTS media (
66
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
67
+ filename VARCHAR(255) NOT NULL,
68
+ original_filename VARCHAR(255) NOT NULL,
69
+ mime_type VARCHAR(100) NOT NULL,
70
+ size_bytes BIGINT NOT NULL,
71
+ width INTEGER,
72
+ height INTEGER,
73
+ url TEXT NOT NULL,
74
+ thumbnail_url TEXT,
75
+ alt_text TEXT,
76
+ caption TEXT,
77
+ metadata JSONB DEFAULT '{}',
78
+ folder VARCHAR(255) DEFAULT '/',
79
+ uploaded_by UUID REFERENCES users(id),
80
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
81
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
82
+ );
83
+
84
+ -- Locales table
85
+ CREATE TABLE IF NOT EXISTS locales (
86
+ code VARCHAR(10) PRIMARY KEY,
87
+ name VARCHAR(100) NOT NULL,
88
+ native_name VARCHAR(100),
89
+ is_default BOOLEAN DEFAULT false,
90
+ is_active BOOLEAN DEFAULT true,
91
+ sort_order INTEGER DEFAULT 0,
92
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
93
+ );
94
+
95
+ -- Sessions table (for real-time collaboration)
96
+ CREATE TABLE IF NOT EXISTS active_sessions (
97
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
98
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
99
+ document_id UUID REFERENCES documents(id) ON DELETE CASCADE,
100
+ socket_id VARCHAR(255),
101
+ cursor_position JSONB,
102
+ last_active_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
103
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
104
+ );
105
+
106
+ -- API Keys table (for external access)
107
+ CREATE TABLE IF NOT EXISTS api_keys (
108
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
109
+ name VARCHAR(255) NOT NULL,
110
+ key_hash VARCHAR(255) UNIQUE NOT NULL,
111
+ permissions JSONB DEFAULT '["read"]',
112
+ last_used_at TIMESTAMP WITH TIME ZONE,
113
+ expires_at TIMESTAMP WITH TIME ZONE,
114
+ is_active BOOLEAN DEFAULT true,
115
+ created_by UUID REFERENCES users(id),
116
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
117
+ );
118
+
119
+ -- Webhooks table
120
+ CREATE TABLE IF NOT EXISTS webhooks (
121
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
122
+ name VARCHAR(255) NOT NULL,
123
+ url TEXT NOT NULL,
124
+ events JSONB NOT NULL DEFAULT '[]',
125
+ secret VARCHAR(255),
126
+ is_active BOOLEAN DEFAULT true,
127
+ last_triggered_at TIMESTAMP WITH TIME ZONE,
128
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
129
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
130
+ );
131
+
132
+ -- Indexes for performance
133
+ CREATE INDEX IF NOT EXISTS idx_documents_schema ON documents(schema_name);
134
+ CREATE INDEX IF NOT EXISTS idx_documents_status ON documents(status);
135
+ CREATE INDEX IF NOT EXISTS idx_documents_locale ON documents(locale);
136
+ CREATE INDEX IF NOT EXISTS idx_documents_created_at ON documents(created_at DESC);
137
+ CREATE INDEX IF NOT EXISTS idx_documents_data_gin ON documents USING GIN(data);
138
+ CREATE INDEX IF NOT EXISTS idx_document_versions_document ON document_versions(document_id);
139
+ CREATE INDEX IF NOT EXISTS idx_media_folder ON media(folder);
140
+ CREATE INDEX IF NOT EXISTS idx_media_mime ON media(mime_type);
141
+ CREATE INDEX IF NOT EXISTS idx_active_sessions_document ON active_sessions(document_id);
142
+ CREATE INDEX IF NOT EXISTS idx_active_sessions_user ON active_sessions(user_id);
143
+
144
+ -- Trigger function for updated_at
145
+ CREATE OR REPLACE FUNCTION update_updated_at_column()
146
+ RETURNS TRIGGER AS $$
147
+ BEGIN
148
+ NEW.updated_at = CURRENT_TIMESTAMP;
149
+ RETURN NEW;
150
+ END;
151
+ $$ language 'plpgsql';
152
+
153
+ -- Apply updated_at trigger to tables
154
+ CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
155
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
156
+
157
+ CREATE TRIGGER update_schemas_updated_at BEFORE UPDATE ON schemas
158
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
159
+
160
+ CREATE TRIGGER update_documents_updated_at BEFORE UPDATE ON documents
161
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
162
+
163
+ CREATE TRIGGER update_media_updated_at BEFORE UPDATE ON media
164
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
165
+
166
+ CREATE TRIGGER update_webhooks_updated_at BEFORE UPDATE ON webhooks
167
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
168
+
169
+ -- Insert default locale
170
+ INSERT INTO locales (code, name, native_name, is_default, is_active, sort_order) VALUES
171
+ ('en', 'English', 'English', true, true, 0),
172
+ ('ru', 'Russian', 'Русский', false, true, 1)
173
+ ON CONFLICT (code) DO NOTHING;
174
+
175
+ -- Insert default admin user (password: admin123)
176
+ INSERT INTO users (email, password_hash, name, role) VALUES
177
+ ('admin@pxlr.local', crypt('admin123', gen_salt('bf')), 'Administrator', 'admin')
178
+ ON CONFLICT (email) DO NOTHING;
179
+
180
+ -- ============================================
181
+ -- DEMO DATA FOR BLOG TEMPLATE
182
+ -- ============================================
183
+
184
+ -- Create blog schema
185
+ INSERT INTO schemas (name, title, description, definition, is_singleton, created_by, updated_by)
186
+ VALUES (
187
+ 'blog',
188
+ 'Блог',
189
+ 'Записи блога',
190
+ '{
191
+ "name": "blog",
192
+ "title": "Блог",
193
+ "fields": [
194
+ {"name": "title", "type": "string", "title": "Заголовок", "required": true},
195
+ {"name": "slug", "type": "slug", "title": "URL slug", "required": true},
196
+ {"name": "excerpt", "type": "text", "title": "Краткое описание"},
197
+ {"name": "content", "type": "richText", "title": "Содержимое"},
198
+ {"name": "image", "type": "image", "title": "Изображение"},
199
+ {"name": "publishedAt", "type": "datetime", "title": "Дата публикации"},
200
+ {"name": "author", "type": "string", "title": "Автор"}
201
+ ]
202
+ }'::jsonb,
203
+ false,
204
+ (SELECT id FROM users LIMIT 1),
205
+ (SELECT id FROM users LIMIT 1)
206
+ ) ON CONFLICT (name) DO NOTHING;
207
+
208
+ -- Create demo blog posts
209
+ INSERT INTO documents (schema_name, data, locale, status, published_at, created_by, updated_by)
210
+ VALUES
211
+ (
212
+ 'blog',
213
+ '{
214
+ "title": "Добро пожаловать в PXLR CMS!",
215
+ "slug": "welcome-to-pxlr",
216
+ "excerpt": "Это ваш первый пост в блоге. Он демонстрирует возможности PXLR CMS.",
217
+ "content": "<h2>Что такое PXLR CMS?</h2><p>PXLR CMS — это self-hosted Headless CMS для современных веб-проектов.</p><ul><li>Гибкая система схем контента</li><li>REST API для интеграции</li><li>Современная админ-панель</li><li>Docker для развёртывания</li></ul>",
218
+ "author": "PXLR Team",
219
+ "publishedAt": "2024-01-01T10:00:00Z"
220
+ }'::jsonb,
221
+ 'en',
222
+ 'published',
223
+ NOW(),
224
+ (SELECT id FROM users LIMIT 1),
225
+ (SELECT id FROM users LIMIT 1)
226
+ ),
227
+ (
228
+ 'blog',
229
+ '{
230
+ "title": "Как создать свою первую схему",
231
+ "slug": "create-first-schema",
232
+ "excerpt": "Пошаговое руководство по созданию схем контента в PXLR CMS.",
233
+ "content": "<h2>Создание схемы</h2><p>Схемы определяют структуру вашего контента.</p><h3>Типы полей:</h3><ul><li><strong>string</strong> — короткий текст</li><li><strong>richText</strong> — форматированный текст</li><li><strong>image</strong> — изображение</li></ul>",
234
+ "author": "PXLR Team",
235
+ "publishedAt": "2024-01-02T12:00:00Z"
236
+ }'::jsonb,
237
+ 'en',
238
+ 'published',
239
+ NOW(),
240
+ (SELECT id FROM users LIMIT 1),
241
+ (SELECT id FROM users LIMIT 1)
242
+ ),
243
+ (
244
+ 'blog',
245
+ '{
246
+ "title": "Интеграция с Next.js",
247
+ "slug": "nextjs-integration",
248
+ "excerpt": "Узнайте, как интегрировать PXLR CMS с вашим Next.js проектом.",
249
+ "content": "<h2>REST API</h2><p>PXLR CMS предоставляет REST API для интеграции с любым фронтендом.</p><pre><code>fetch(`${API_URL}/content?schema=blog&status=published`)</code></pre>",
250
+ "author": "PXLR Team",
251
+ "publishedAt": "2024-01-03T14:00:00Z"
252
+ }'::jsonb,
253
+ 'en',
254
+ 'published',
255
+ NOW(),
256
+ (SELECT id FROM users LIMIT 1),
257
+ (SELECT id FROM users LIMIT 1)
258
+ );
@@ -0,0 +1,95 @@
1
+ import Redis from 'ioredis';
2
+ import { config } from '../config.js';
3
+
4
+ class RedisClient {
5
+ private client: Redis | null = null;
6
+ private subscriber: Redis | null = null;
7
+
8
+ async connect() {
9
+ this.client = new Redis(config.redisUrl, {
10
+ maxRetriesPerRequest: 3,
11
+ retryStrategy: (times) => {
12
+ if (times > 3) {
13
+ return null;
14
+ }
15
+ return Math.min(times * 100, 3000);
16
+ },
17
+ });
18
+
19
+ this.subscriber = new Redis(config.redisUrl);
20
+
21
+ // Wait for connection
22
+ await new Promise<void>((resolve, reject) => {
23
+ this.client!.once('ready', resolve);
24
+ this.client!.once('error', reject);
25
+ });
26
+ }
27
+
28
+ async disconnect() {
29
+ if (this.client) {
30
+ await this.client.quit();
31
+ this.client = null;
32
+ }
33
+ if (this.subscriber) {
34
+ await this.subscriber.quit();
35
+ this.subscriber = null;
36
+ }
37
+ }
38
+
39
+ getClient(): Redis {
40
+ if (!this.client) {
41
+ throw new Error('Redis not connected');
42
+ }
43
+ return this.client;
44
+ }
45
+
46
+ getSubscriber(): Redis {
47
+ if (!this.subscriber) {
48
+ throw new Error('Redis subscriber not connected');
49
+ }
50
+ return this.subscriber;
51
+ }
52
+
53
+ // Cache helpers
54
+ async get<T>(key: string): Promise<T | null> {
55
+ const value = await this.getClient().get(key);
56
+ if (!value) return null;
57
+ return JSON.parse(value) as T;
58
+ }
59
+
60
+ async set(key: string, value: any, ttlSeconds?: number): Promise<void> {
61
+ const serialized = JSON.stringify(value);
62
+ if (ttlSeconds) {
63
+ await this.getClient().setex(key, ttlSeconds, serialized);
64
+ } else {
65
+ await this.getClient().set(key, serialized);
66
+ }
67
+ }
68
+
69
+ async del(key: string): Promise<void> {
70
+ await this.getClient().del(key);
71
+ }
72
+
73
+ async invalidatePattern(pattern: string): Promise<void> {
74
+ const keys = await this.getClient().keys(pattern);
75
+ if (keys.length > 0) {
76
+ await this.getClient().del(...keys);
77
+ }
78
+ }
79
+
80
+ // Pub/Sub for real-time
81
+ async publish(channel: string, message: any): Promise<void> {
82
+ await this.getClient().publish(channel, JSON.stringify(message));
83
+ }
84
+
85
+ async subscribe(channel: string, callback: (message: any) => void): Promise<void> {
86
+ await this.getSubscriber().subscribe(channel);
87
+ this.getSubscriber().on('message', (ch, msg) => {
88
+ if (ch === channel) {
89
+ callback(JSON.parse(msg));
90
+ }
91
+ });
92
+ }
93
+ }
94
+
95
+ export const redis = new RedisClient();
@@ -0,0 +1,78 @@
1
+ -- Demo data for Blog template
2
+ -- This file is executed after init.sql
3
+
4
+ -- Create blog schema
5
+ INSERT INTO schemas (name, title, description, definition, is_singleton, created_by, updated_by)
6
+ VALUES (
7
+ 'blog',
8
+ 'Блог',
9
+ 'Записи блога',
10
+ '{
11
+ "name": "blog",
12
+ "title": "Блог",
13
+ "fields": [
14
+ {"name": "title", "type": "string", "title": "Заголовок", "required": true},
15
+ {"name": "slug", "type": "slug", "title": "URL slug", "required": true},
16
+ {"name": "excerpt", "type": "text", "title": "Краткое описание"},
17
+ {"name": "content", "type": "richText", "title": "Содержимое"},
18
+ {"name": "image", "type": "image", "title": "Изображение"},
19
+ {"name": "publishedAt", "type": "datetime", "title": "Дата публикации"},
20
+ {"name": "author", "type": "string", "title": "Автор"}
21
+ ]
22
+ }'::jsonb,
23
+ false,
24
+ (SELECT id FROM users LIMIT 1),
25
+ (SELECT id FROM users LIMIT 1)
26
+ ) ON CONFLICT (name) DO NOTHING;
27
+
28
+ -- Create demo blog posts
29
+ INSERT INTO documents (schema_name, data, locale, status, published_at, created_by, updated_by)
30
+ VALUES
31
+ (
32
+ 'blog',
33
+ '{
34
+ "title": "Добро пожаловать в PXLR CMS!",
35
+ "slug": "welcome-to-pxlr",
36
+ "excerpt": "Это ваш первый пост в блоге. Он демонстрирует возможности PXLR CMS — вашей собственной Headless CMS.",
37
+ "content": "<h2>Что такое PXLR CMS?</h2><p>PXLR CMS — это self-hosted Headless CMS, разработанная для современных веб-проектов. Она предоставляет:</p><ul><li>Гибкую систему схем контента</li><li>REST API для интеграции с любым фронтендом</li><li>Современную админ-панель</li><li>Поддержку Docker для простого развёртывания</li></ul><h2>Начало работы</h2><p>Чтобы начать работу с PXLR CMS, откройте админ-панель и создайте свою первую схему контента. Затем добавьте документы и используйте API для их отображения на фронтенде.</p>",
38
+ "author": "PXLR Team",
39
+ "publishedAt": "2024-01-01T10:00:00Z"
40
+ }'::jsonb,
41
+ 'en',
42
+ 'published',
43
+ NOW(),
44
+ (SELECT id FROM users LIMIT 1),
45
+ (SELECT id FROM users LIMIT 1)
46
+ ),
47
+ (
48
+ 'blog',
49
+ '{
50
+ "title": "Как создать свою первую схему",
51
+ "slug": "create-first-schema",
52
+ "excerpt": "Пошаговое руководство по созданию схем контента в PXLR CMS.",
53
+ "content": "<h2>Создание схемы</h2><p>Схемы определяют структуру вашего контента. Каждая схема содержит набор полей с определёнными типами.</p><h3>Доступные типы полей:</h3><ul><li><strong>string</strong> — короткий текст</li><li><strong>text</strong> — многострочный текст</li><li><strong>richText</strong> — форматированный текст с редактором</li><li><strong>number</strong> — числовое значение</li><li><strong>boolean</strong> — да/нет</li><li><strong>date</strong> — дата</li><li><strong>datetime</strong> — дата и время</li><li><strong>image</strong> — изображение</li><li><strong>slug</strong> — URL-friendly строка</li></ul><h3>Пример схемы</h3><p>Перейдите в раздел \"Схемы\" и нажмите \"Создать схему\". Введите название и добавьте необходимые поля.</p>",
54
+ "author": "PXLR Team",
55
+ "publishedAt": "2024-01-02T12:00:00Z"
56
+ }'::jsonb,
57
+ 'en',
58
+ 'published',
59
+ NOW(),
60
+ (SELECT id FROM users LIMIT 1),
61
+ (SELECT id FROM users LIMIT 1)
62
+ ),
63
+ (
64
+ 'blog',
65
+ '{
66
+ "title": "Интеграция с Next.js",
67
+ "slug": "nextjs-integration",
68
+ "excerpt": "Узнайте, как интегрировать PXLR CMS с вашим Next.js проектом.",
69
+ "content": "<h2>Установка</h2><p>PXLR CMS предоставляет REST API, который легко интегрируется с любым фронтенд-фреймворком, включая Next.js.</p><h3>Базовый пример</h3><p>Создайте файл <code>lib/cms.ts</code> для работы с API:</p><pre><code>const API_URL = process.env.NEXT_PUBLIC_CMS_API_URL;\n\nexport async function getPosts() {\n const res = await fetch(`${API_URL}/content?schema=blog&status=published`);\n const data = await res.json();\n return data.documents;\n}</code></pre><h3>Использование в компонентах</h3><p>Вызывайте функции API в серверных компонентах Next.js для получения данных.</p>",
70
+ "author": "PXLR Team",
71
+ "publishedAt": "2024-01-03T14:00:00Z"
72
+ }'::jsonb,
73
+ 'en',
74
+ 'published',
75
+ NOW(),
76
+ (SELECT id FROM users LIMIT 1),
77
+ (SELECT id FROM users LIMIT 1)
78
+ );
@@ -0,0 +1,157 @@
1
+ import Fastify from 'fastify';
2
+ import cors from '@fastify/cors';
3
+ import helmet from '@fastify/helmet';
4
+ import jwt from '@fastify/jwt';
5
+ import multipart from '@fastify/multipart';
6
+ import rateLimit from '@fastify/rate-limit';
7
+ import websocket from '@fastify/websocket';
8
+ import swagger from '@fastify/swagger';
9
+ import swaggerUi from '@fastify/swagger-ui';
10
+
11
+ import { config } from './config.js';
12
+ import { db } from './database/index.js';
13
+ import { redis } from './database/redis.js';
14
+ import { authRoutes } from './modules/auth/routes.js';
15
+ import { contentRoutes } from './modules/content/routes.js';
16
+ import { schemaRoutes } from './modules/schema/routes.js';
17
+ import { mediaRoutes } from './modules/media/routes.js';
18
+ import { realtimeHandler } from './modules/realtime/handler.js';
19
+
20
+ const fastify = Fastify({
21
+ logger: {
22
+ level: config.isDev ? 'info' : 'warn',
23
+ },
24
+ });
25
+
26
+ async function bootstrap() {
27
+ // Register plugins
28
+ await fastify.register(cors, {
29
+ origin: config.isDev ? true : config.corsOrigins,
30
+ credentials: true,
31
+ });
32
+
33
+ await fastify.register(helmet, {
34
+ contentSecurityPolicy: false,
35
+ });
36
+
37
+ await fastify.register(jwt, {
38
+ secret: config.jwtSecret,
39
+ sign: {
40
+ expiresIn: config.jwtExpiresIn,
41
+ },
42
+ });
43
+
44
+ // Add authenticate decorator
45
+ fastify.decorate('authenticate', async (request: any, reply: any) => {
46
+ try {
47
+ await request.jwtVerify();
48
+ } catch (err) {
49
+ reply.status(401).send({ error: true, message: 'Unauthorized' });
50
+ }
51
+ });
52
+
53
+ await fastify.register(multipart, {
54
+ limits: {
55
+ fileSize: 100 * 1024 * 1024, // 100MB
56
+ },
57
+ });
58
+
59
+ await fastify.register(rateLimit, {
60
+ max: 100,
61
+ timeWindow: '1 minute',
62
+ });
63
+
64
+ await fastify.register(websocket);
65
+
66
+ // Swagger documentation
67
+ await fastify.register(swagger, {
68
+ openapi: {
69
+ info: {
70
+ title: 'PXLR CMS API',
71
+ description: 'Headless CMS API Documentation',
72
+ version: '1.0.0',
73
+ },
74
+ servers: [
75
+ {
76
+ url: `http://localhost:${config.port}`,
77
+ description: 'Development server',
78
+ },
79
+ ],
80
+ components: {
81
+ securitySchemes: {
82
+ bearerAuth: {
83
+ type: 'http',
84
+ scheme: 'bearer',
85
+ bearerFormat: 'JWT',
86
+ },
87
+ },
88
+ },
89
+ },
90
+ });
91
+
92
+ await fastify.register(swaggerUi, {
93
+ routePrefix: '/docs',
94
+ uiConfig: {
95
+ docExpansion: 'list',
96
+ deepLinking: false,
97
+ },
98
+ });
99
+
100
+ // Health check
101
+ fastify.get('/health', async () => {
102
+ return { status: 'ok', timestamp: new Date().toISOString() };
103
+ });
104
+
105
+ // Register routes
106
+ await fastify.register(authRoutes, { prefix: '/auth' });
107
+ await fastify.register(schemaRoutes, { prefix: '/schemas' });
108
+ await fastify.register(contentRoutes, { prefix: '/content' });
109
+ await fastify.register(mediaRoutes, { prefix: '/media' });
110
+
111
+ // WebSocket for real-time collaboration
112
+ fastify.get('/ws', { websocket: true }, realtimeHandler);
113
+
114
+ // Global error handler
115
+ fastify.setErrorHandler((error, request, reply) => {
116
+ fastify.log.error(error);
117
+
118
+ const statusCode = error.statusCode || 500;
119
+ const message = config.isDev ? error.message : 'Internal Server Error';
120
+
121
+ reply.status(statusCode).send({
122
+ error: true,
123
+ message,
124
+ statusCode,
125
+ });
126
+ });
127
+
128
+ // Start server
129
+ try {
130
+ await db.connect();
131
+ fastify.log.info('Database connected');
132
+
133
+ await redis.connect();
134
+ fastify.log.info('Redis connected');
135
+
136
+ await fastify.listen({ port: config.port, host: '0.0.0.0' });
137
+ fastify.log.info(`PXLR CMS API running on port ${config.port}`);
138
+ fastify.log.info(`API Documentation: http://localhost:${config.port}/docs`);
139
+ } catch (err) {
140
+ fastify.log.error(err);
141
+ process.exit(1);
142
+ }
143
+ }
144
+
145
+ // Graceful shutdown
146
+ const signals = ['SIGINT', 'SIGTERM'];
147
+ signals.forEach((signal) => {
148
+ process.on(signal, async () => {
149
+ console.log(`\nReceived ${signal}, shutting down gracefully...`);
150
+ await fastify.close();
151
+ await db.disconnect();
152
+ await redis.disconnect();
153
+ process.exit(0);
154
+ });
155
+ });
156
+
157
+ bootstrap();