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.
- package/README.md +160 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +264 -0
- package/package.json +51 -0
- package/templates/blog/frontend/app/blog/[slug]/page.tsx +175 -0
- package/templates/blog/frontend/app/blog/page.tsx +102 -0
- package/templates/blog/frontend/app/components/footer.tsx +21 -0
- package/templates/blog/frontend/app/components/header.tsx +45 -0
- package/templates/blog/frontend/app/globals.css +30 -0
- package/templates/blog/frontend/app/layout.tsx +38 -0
- package/templates/blog/frontend/app/lib/cms.ts +71 -0
- package/templates/blog/frontend/app/page.tsx +155 -0
- package/templates/blog/frontend/next.config.ts +16 -0
- package/templates/blog/frontend/package.json +24 -0
- package/templates/blog/frontend/postcss.config.mjs +7 -0
- package/templates/blog/frontend/tsconfig.json +23 -0
- package/templates/blog/pxlr-cms/README.md +188 -0
- package/templates/blog/pxlr-cms/docker-compose.yml +132 -0
- package/templates/blog/pxlr-cms/nginx/nginx.conf +107 -0
- package/templates/blog/pxlr-cms/packages/admin/.dockerignore +4 -0
- package/templates/blog/pxlr-cms/packages/admin/.env.example +2 -0
- package/templates/blog/pxlr-cms/packages/admin/Dockerfile +19 -0
- package/templates/blog/pxlr-cms/packages/admin/next-env.d.ts +6 -0
- package/templates/blog/pxlr-cms/packages/admin/next.config.ts +22 -0
- package/templates/blog/pxlr-cms/packages/admin/package.json +63 -0
- package/templates/blog/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
- package/templates/blog/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/globals.css +132 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
- package/templates/blog/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
- package/templates/blog/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
- package/templates/blog/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
- package/templates/blog/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
- package/templates/blog/pxlr-cms/packages/admin/tsconfig.json +27 -0
- package/templates/blog/pxlr-cms/packages/api/.env.example +23 -0
- package/templates/blog/pxlr-cms/packages/api/Dockerfile +26 -0
- package/templates/blog/pxlr-cms/packages/api/package.json +42 -0
- package/templates/blog/pxlr-cms/packages/api/src/config.ts +39 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/index.ts +60 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/init.sql +258 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/redis.ts +95 -0
- package/templates/blog/pxlr-cms/packages/api/src/database/seed.sql +78 -0
- package/templates/blog/pxlr-cms/packages/api/src/index.ts +157 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
- package/templates/blog/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
- package/templates/blog/pxlr-cms/packages/api/tsconfig.json +24 -0
- package/templates/blog/pxlr-cms/packages/shared/package.json +14 -0
- package/templates/blog/pxlr-cms/packages/shared/src/types/index.ts +139 -0
- package/templates/blog/pxlr-cms/packages/shared/tsconfig.json +18 -0
- package/templates/clean/pxlr-cms/README.md +188 -0
- package/templates/clean/pxlr-cms/docker-compose.yml +132 -0
- package/templates/clean/pxlr-cms/nginx/nginx.conf +107 -0
- package/templates/clean/pxlr-cms/packages/admin/.dockerignore +4 -0
- package/templates/clean/pxlr-cms/packages/admin/.env.example +2 -0
- package/templates/clean/pxlr-cms/packages/admin/Dockerfile +19 -0
- package/templates/clean/pxlr-cms/packages/admin/next-env.d.ts +6 -0
- package/templates/clean/pxlr-cms/packages/admin/next.config.ts +22 -0
- package/templates/clean/pxlr-cms/packages/admin/package.json +63 -0
- package/templates/clean/pxlr-cms/packages/admin/pnpm-lock.yaml +5748 -0
- package/templates/clean/pxlr-cms/packages/admin/postcss.config.mjs +9 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/[id]/page.tsx +503 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/new/page.tsx +424 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/content/page.tsx +191 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/globals.css +132 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/layout.tsx +25 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/login/page.tsx +119 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/media/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/media/page.tsx +362 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/page.tsx +184 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/profile/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/profile/page.tsx +206 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/[name]/page.tsx +312 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/schemas/page.tsx +210 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/settings/layout.tsx +7 -0
- package/templates/clean/pxlr-cms/packages/admin/src/app/settings/page.tsx +178 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/editor/media-picker.tsx +202 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/editor/rich-text-editor.tsx +387 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/auth-layout.tsx +43 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/header.tsx +79 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/layout/sidebar.tsx +68 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/providers.tsx +29 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/schema-code-generator.tsx +326 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/avatar.tsx +49 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/button.tsx +55 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/dropdown-menu.tsx +194 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/input.tsx +24 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/label.tsx +25 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toast.tsx +127 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/toaster.tsx +35 -0
- package/templates/clean/pxlr-cms/packages/admin/src/components/ui/use-toast.ts +187 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/api.ts +96 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/context.tsx +60 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/i18n/translations.ts +317 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/store/auth.ts +51 -0
- package/templates/clean/pxlr-cms/packages/admin/src/lib/utils.ts +29 -0
- package/templates/clean/pxlr-cms/packages/admin/tailwind.config.ts +57 -0
- package/templates/clean/pxlr-cms/packages/admin/tsconfig.json +27 -0
- package/templates/clean/pxlr-cms/packages/api/.env.example +23 -0
- package/templates/clean/pxlr-cms/packages/api/Dockerfile +26 -0
- package/templates/clean/pxlr-cms/packages/api/package.json +42 -0
- package/templates/clean/pxlr-cms/packages/api/src/config.ts +39 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/index.ts +60 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/init.sql +178 -0
- package/templates/clean/pxlr-cms/packages/api/src/database/redis.ts +95 -0
- package/templates/clean/pxlr-cms/packages/api/src/index.ts +157 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/auth/routes.ts +256 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/content/routes.ts +385 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/media/routes.ts +312 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/realtime/handler.ts +228 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/schema/routes.ts +284 -0
- package/templates/clean/pxlr-cms/packages/api/src/modules/versions/routes.ts +70 -0
- package/templates/clean/pxlr-cms/packages/api/tsconfig.json +24 -0
- package/templates/clean/pxlr-cms/packages/shared/package.json +14 -0
- package/templates/clean/pxlr-cms/packages/shared/src/types/index.ts +139 -0
- 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();
|