pxlr-cms 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,178 @@
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;
@@ -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,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();
@@ -0,0 +1,256 @@
1
+ import { FastifyPluginAsync } from 'fastify';
2
+ import bcrypt from 'bcryptjs';
3
+ import { z } from 'zod';
4
+ import { db } from '../../database/index.js';
5
+
6
+ const loginSchema = z.object({
7
+ email: z.string().email(),
8
+ password: z.string().min(6),
9
+ });
10
+
11
+ const registerSchema = z.object({
12
+ email: z.string().email(),
13
+ password: z.string().min(6),
14
+ name: z.string().min(2),
15
+ });
16
+
17
+ export const authRoutes: FastifyPluginAsync = async (fastify) => {
18
+ // Login
19
+ fastify.post('/login', {
20
+ schema: {
21
+ tags: ['Auth'],
22
+ summary: 'Login to PXLR CMS',
23
+ body: {
24
+ type: 'object',
25
+ required: ['email', 'password'],
26
+ properties: {
27
+ email: { type: 'string', format: 'email' },
28
+ password: { type: 'string', minLength: 6 },
29
+ },
30
+ },
31
+ response: {
32
+ 200: {
33
+ type: 'object',
34
+ properties: {
35
+ token: { type: 'string' },
36
+ user: {
37
+ type: 'object',
38
+ properties: {
39
+ id: { type: 'string' },
40
+ email: { type: 'string' },
41
+ name: { type: 'string' },
42
+ role: { type: 'string' },
43
+ },
44
+ },
45
+ },
46
+ },
47
+ },
48
+ },
49
+ }, async (request, reply) => {
50
+ const body = loginSchema.parse(request.body);
51
+
52
+ const result = await db.query(
53
+ `SELECT id, email, password_hash, name, role, is_active
54
+ FROM users WHERE email = $1`,
55
+ [body.email]
56
+ );
57
+
58
+ const user = result.rows[0];
59
+
60
+ if (!user || !user.is_active) {
61
+ return reply.status(401).send({ error: true, message: 'Invalid credentials' });
62
+ }
63
+
64
+ const validPassword = await bcrypt.compare(body.password, user.password_hash);
65
+ if (!validPassword) {
66
+ return reply.status(401).send({ error: true, message: 'Invalid credentials' });
67
+ }
68
+
69
+ // Update last login
70
+ await db.query(
71
+ 'UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE id = $1',
72
+ [user.id]
73
+ );
74
+
75
+ const token = fastify.jwt.sign({
76
+ id: user.id,
77
+ email: user.email,
78
+ role: user.role,
79
+ });
80
+
81
+ return {
82
+ token,
83
+ user: {
84
+ id: user.id,
85
+ email: user.email,
86
+ name: user.name,
87
+ role: user.role,
88
+ },
89
+ };
90
+ });
91
+
92
+ // Register (admin only in production)
93
+ fastify.post('/register', {
94
+ schema: {
95
+ tags: ['Auth'],
96
+ summary: 'Register a new user',
97
+ body: {
98
+ type: 'object',
99
+ required: ['email', 'password', 'name'],
100
+ properties: {
101
+ email: { type: 'string', format: 'email' },
102
+ password: { type: 'string', minLength: 6 },
103
+ name: { type: 'string', minLength: 2 },
104
+ },
105
+ },
106
+ },
107
+ }, async (request, reply) => {
108
+ const body = registerSchema.parse(request.body);
109
+
110
+ // Check if user exists
111
+ const existing = await db.query(
112
+ 'SELECT id FROM users WHERE email = $1',
113
+ [body.email]
114
+ );
115
+
116
+ if (existing.rows.length > 0) {
117
+ return reply.status(400).send({ error: true, message: 'Email already registered' });
118
+ }
119
+
120
+ const passwordHash = await bcrypt.hash(body.password, 12);
121
+
122
+ const result = await db.query(
123
+ `INSERT INTO users (email, password_hash, name, role)
124
+ VALUES ($1, $2, $3, 'editor')
125
+ RETURNING id, email, name, role`,
126
+ [body.email, passwordHash, body.name]
127
+ );
128
+
129
+ const user = result.rows[0];
130
+
131
+ const token = fastify.jwt.sign({
132
+ id: user.id,
133
+ email: user.email,
134
+ role: user.role,
135
+ });
136
+
137
+ return {
138
+ token,
139
+ user: {
140
+ id: user.id,
141
+ email: user.email,
142
+ name: user.name,
143
+ role: user.role,
144
+ },
145
+ };
146
+ });
147
+
148
+ // Get current user
149
+ fastify.get('/me', {
150
+ schema: {
151
+ tags: ['Auth'],
152
+ summary: 'Get current user',
153
+ security: [{ bearerAuth: [] }],
154
+ },
155
+ preHandler: [fastify.authenticate],
156
+ }, async (request) => {
157
+ const user = request.user as { id: string };
158
+
159
+ const result = await db.query(
160
+ `SELECT id, email, name, role, avatar_url, created_at
161
+ FROM users WHERE id = $1`,
162
+ [user.id]
163
+ );
164
+
165
+ return { user: result.rows[0] };
166
+ });
167
+
168
+ // Logout (invalidate token - for future implementation with token blacklist)
169
+ fastify.post('/logout', {
170
+ schema: {
171
+ tags: ['Auth'],
172
+ summary: 'Logout current user',
173
+ security: [{ bearerAuth: [] }],
174
+ },
175
+ preHandler: [fastify.authenticate],
176
+ }, async () => {
177
+ return { success: true, message: 'Logged out successfully' };
178
+ });
179
+
180
+ // Update profile
181
+ fastify.put('/profile', {
182
+ schema: {
183
+ tags: ['Auth'],
184
+ summary: 'Update user profile',
185
+ security: [{ bearerAuth: [] }],
186
+ },
187
+ preHandler: [fastify.authenticate],
188
+ }, async (request, reply) => {
189
+ const user = request.user as { id: string };
190
+ const body = z.object({
191
+ name: z.string().min(2),
192
+ }).parse(request.body);
193
+
194
+ const result = await db.query(
195
+ `UPDATE users SET name = $1, updated_at = CURRENT_TIMESTAMP
196
+ WHERE id = $2
197
+ RETURNING id, email, name, role, avatar_url`,
198
+ [body.name, user.id]
199
+ );
200
+
201
+ if (result.rows.length === 0) {
202
+ return reply.status(404).send({ error: true, message: 'User not found' });
203
+ }
204
+
205
+ return { user: result.rows[0] };
206
+ });
207
+
208
+ // Change password
209
+ fastify.put('/password', {
210
+ schema: {
211
+ tags: ['Auth'],
212
+ summary: 'Change user password',
213
+ security: [{ bearerAuth: [] }],
214
+ },
215
+ preHandler: [fastify.authenticate],
216
+ }, async (request, reply) => {
217
+ const user = request.user as { id: string };
218
+ const body = z.object({
219
+ currentPassword: z.string().min(6),
220
+ newPassword: z.string().min(6),
221
+ }).parse(request.body);
222
+
223
+ // Get current password hash
224
+ const result = await db.query(
225
+ 'SELECT password_hash FROM users WHERE id = $1',
226
+ [user.id]
227
+ );
228
+
229
+ if (result.rows.length === 0) {
230
+ return reply.status(404).send({ error: true, message: 'User not found' });
231
+ }
232
+
233
+ // Verify current password
234
+ const validPassword = await bcrypt.compare(body.currentPassword, result.rows[0].password_hash);
235
+ if (!validPassword) {
236
+ return reply.status(400).send({ error: true, message: 'Current password is incorrect' });
237
+ }
238
+
239
+ // Hash new password and update
240
+ const newPasswordHash = await bcrypt.hash(body.newPassword, 12);
241
+ await db.query(
242
+ 'UPDATE users SET password_hash = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
243
+ [newPasswordHash, user.id]
244
+ );
245
+
246
+ return { success: true, message: 'Password changed successfully' };
247
+ });
248
+
249
+ };
250
+
251
+ // Type augmentation for Fastify
252
+ declare module 'fastify' {
253
+ interface FastifyInstance {
254
+ authenticate: any;
255
+ }
256
+ }