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,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
|
+
}
|