diff --git a/client/src/App.js b/client/src/App.js
new file mode 100644
index 0000000..c28b30c
--- /dev/null
+++ b/client/src/App.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
+import Login from './components/Login';
+import Dashboard from './components/Dashboard';
+import Editor from './components/Editor';
+
+function PrivateRoute({ children }) {
+ const token = localStorage.getItem('token');
+ return token ? children : ;
+}
+
+export default function App() {
+ return (
+
+
+
+ } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ } />
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/Dashboard.js b/client/src/components/Dashboard.js
new file mode 100644
index 0000000..7a51632
--- /dev/null
+++ b/client/src/components/Dashboard.js
@@ -0,0 +1,82 @@
+import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import axios from 'axios';
+import { format } from 'date-fns';
+
+export default function Dashboard() {
+ const [posts, setPosts] = useState([]);
+ const [filter, setFilter] = useState('all'); // all, draft, published
+
+ useEffect(() => {
+ loadPosts();
+ }, []);
+
+ const loadPosts = async () => {
+ try {
+ const response = await axios.get('/api/posts', {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ setPosts(response.data);
+ } catch (error) {
+ console.error('Error loading posts:', error);
+ }
+ };
+
+ const filteredPosts = posts.filter(post => {
+ if (filter === 'all') return true;
+ return post.status === filter;
+ });
+
+ return (
+
+
+
My Posts
+ New Post
+
+
+
+
+
+
+
+
+
+ {filteredPosts.map(post => (
+
+
+ {post.title}
+ {post.status}
+
+
+
+ {post.published_at
+ ? format(new Date(post.published_at), 'MMM d, yyyy')
+ : 'Not published'}
+
+
+ {post.tags.map((tag, index) => (
+ {tag}
+ ))}
+
+
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/Editor.js b/client/src/components/Editor.js
new file mode 100644
index 0000000..7ad5fcb
--- /dev/null
+++ b/client/src/components/Editor.js
@@ -0,0 +1,154 @@
+import React, { useState, useEffect } from 'react';
+import MDEditor from '@uiw/react-md-editor';
+import { useNavigate, useParams } from 'react-router-dom';
+import axios from 'axios';
+
+export default function Editor() {
+ const [title, setTitle] = useState('');
+ const [content, setContent] = useState('');
+ const [tags, setTags] = useState([]);
+ const [tagInput, setTagInput] = useState('');
+ const [publishDate, setPublishDate] = useState('');
+ const { id } = useParams();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (id) {
+ loadPost();
+ }
+ }, [id]);
+
+ const loadPost = async () => {
+ const response = await axios.get(`/api/posts/${id}`, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ const { title, markdown_content, tags, published_at } = response.data;
+ setTitle(title);
+ setContent(markdown_content);
+ setTags(tags);
+ setPublishDate(published_at);
+ };
+
+ const handleSave = async (status) => {
+ const data = {
+ title,
+ content,
+ tags,
+ status,
+ publishedAt: publishDate || new Date().toISOString()
+ };
+
+ try {
+ if (id) {
+ await axios.put(`/api/posts/${id}`, data, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ } else {
+ await axios.post('/api/posts', data, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ }
+ navigate('/dashboard');
+ } catch (error) {
+ console.error('Error saving post:', error);
+ }
+ };
+
+ const handleImageUpload = async (file) => {
+ const formData = new FormData();
+ formData.append('image', file);
+
+ try {
+ const response = await axios.post(`/api/images/${id}`, formData, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('token')}`,
+ 'Content-Type': 'multipart/form-data'
+ }
+ });
+ return response.data.url;
+ } catch (error) {
+ console.error('Error uploading image:', error);
+ return null;
+ }
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/Login.js b/client/src/components/Login.js
new file mode 100644
index 0000000..334a7a1
--- /dev/null
+++ b/client/src/components/Login.js
@@ -0,0 +1,52 @@
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import axios from 'axios';
+
+export default function Login() {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ try {
+ const response = await axios.post('/api/auth/login', {
+ username,
+ password
+ });
+ localStorage.setItem('token', response.data.token);
+ navigate('/dashboard');
+ } catch (error) {
+ setError('Invalid credentials');
+ }
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/styles/main.scss b/client/src/styles/main.scss
new file mode 100644
index 0000000..c94e7ca
--- /dev/null
+++ b/client/src/styles/main.scss
@@ -0,0 +1,66 @@
+$primary-color: #2c3e50;
+$secondary-color: #3498db;
+$background-color: #f5f6fa;
+$text-color: #2c3e50;
+$border-color: #dcdde1;
+
+body {
+ background-color: $background-color;
+ color: $text-color;
+ font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+.post-editor {
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ padding: 20px;
+ margin-bottom: 20px;
+
+ input[type="text"] {
+ width: 100%;
+ padding: 10px;
+ margin-bottom: 20px;
+ border: 1px solid $border-color;
+ border-radius: 4px;
+ }
+
+ .tags-input {
+ margin-bottom: 20px;
+
+ .tag {
+ display: inline-block;
+ background: $secondary-color;
+ color: white;
+ padding: 5px 10px;
+ border-radius: 15px;
+ margin-right: 10px;
+ margin-bottom: 5px;
+
+ .remove {
+ margin-left: 5px;
+ cursor: pointer;
+ }
+ }
+ }
+
+ .button {
+ background: $primary-color;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 4px;
+ cursor: pointer;
+ margin-right: 10px;
+
+ &.draft {
+ background: $secondary-color;
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/package-lock.json b/server/package-lock.json
index 22f6952..4e118a6 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -13,10 +13,14 @@
"@fastify/multipart": "^7.0.0",
"@fastify/static": "^6.0.0",
"bcrypt": "^5.0.1",
+ "dotenv": "^16.0.0",
"fastify": "^4.0.0",
"marked": "^4.0.0",
- "pg": "^8.7.1",
+ "pg-promise": "^11.0.0",
"slugify": "^1.6.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.0.0"
}
},
"node_modules/@fastify/accept-negotiator": {
@@ -275,6 +279,20 @@
"node": ">=8"
}
},
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
@@ -307,6 +325,15 @@
"safer-buffer": "^2.1.0"
}
},
+ "node_modules/assert-options": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.2.tgz",
+ "integrity": "sha512-XaXoMxY0zuwAb0YuZjxIm8FeWvNq0aWNIbrzHhFjme8Smxw4JlPoyrAKQ6808k5UvQdhvnWqHZCphq5mXd4TDA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
@@ -346,6 +373,19 @@
"node": ">= 10.0.0"
}
},
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/bn.js": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz",
@@ -361,6 +401,44 @@
"balanced-match": "^1.0.0"
}
},
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@@ -453,6 +531,18 @@
"node": ">=8"
}
},
+ "node_modules/dotenv": {
+ "version": "16.4.5",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
+ "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -653,6 +743,19 @@
"xtend": "^4.0.0"
}
},
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/find-my-way": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz",
@@ -706,6 +809,21 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
@@ -747,6 +865,29 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -782,6 +923,13 @@
"node": ">= 6"
}
},
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -808,6 +956,29 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -817,6 +988,29 @@
"node": ">=8"
}
},
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
"node_modules/json-schema-ref-resolver": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz",
@@ -1013,6 +1207,59 @@
}
}
},
+ "node_modules/nodemon": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz",
+ "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/nodemon/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
@@ -1028,6 +1275,16 @@
"node": ">=6"
}
},
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
@@ -1144,6 +1401,16 @@
"integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==",
"license": "MIT"
},
+ "node_modules/pg-cursor": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.12.1.tgz",
+ "integrity": "sha512-V13tEaA9Oq1w+V6Q3UBIB/blxJrwbbr35/dY54r/86soBJ7xkP236bXaORUTVXUPt9B6Ql2BQu+uwQiuMfRVgg==",
+ "license": "MIT",
+ "peer": true,
+ "peerDependencies": {
+ "pg": "^8"
+ }
+ },
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
@@ -1153,6 +1420,15 @@
"node": ">=4.0.0"
}
},
+ "node_modules/pg-minify": {
+ "version": "1.6.5",
+ "resolved": "https://registry.npmjs.org/pg-minify/-/pg-minify-1.6.5.tgz",
+ "integrity": "sha512-u0UE8veaCnMfJmoklqneeBBopOAPG3/6DHqGVHYAhz8DkJXh9dnjPlz25fRxn4e+6XVzdOp7kau63Rp52fZ3WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/pg-pool": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz",
@@ -1162,12 +1438,43 @@
"pg": ">=8.0"
}
},
+ "node_modules/pg-promise": {
+ "version": "11.10.2",
+ "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-11.10.2.tgz",
+ "integrity": "sha512-wK4yjxZdfxBmAMcs40q6IsC1SOzdLilc1yNvJqlbOjtm2syayqLDCt1JQ9lhS6yNSgVlGOQZT88yb/SADJmEBw==",
+ "license": "MIT",
+ "dependencies": {
+ "assert-options": "0.8.2",
+ "pg": "8.13.1",
+ "pg-minify": "1.6.5",
+ "spex": "3.4.0"
+ },
+ "engines": {
+ "node": ">=14.0"
+ },
+ "peerDependencies": {
+ "pg-query-stream": "4.7.1"
+ }
+ },
"node_modules/pg-protocol": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz",
"integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==",
"license": "MIT"
},
+ "node_modules/pg-query-stream": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/pg-query-stream/-/pg-query-stream-4.7.1.tgz",
+ "integrity": "sha512-UMgsgn/pOIYsIifRySp59vwlpTpLADMK9HWJtq5ff0Z3MxBnPMGnCQeaQl5VuL+7ov4F96mSzIRIcz+Duo6OiQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "pg-cursor": "^2.12.1"
+ },
+ "peerDependencies": {
+ "pg": "^8"
+ }
+ },
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
@@ -1193,6 +1500,19 @@
"split2": "^4.1.0"
}
},
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/pino": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz",
@@ -1294,6 +1614,13 @@
"node": ">= 0.10"
}
},
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -1323,6 +1650,19 @@
"node": ">= 6"
}
},
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
@@ -1511,6 +1851,19 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/slugify": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
@@ -1529,6 +1882,15 @@
"atomic-sleep": "^1.0.0"
}
},
+ "node_modules/spex": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/spex/-/spex-3.4.0.tgz",
+ "integrity": "sha512-8JeZJ7QlEBnSj1W1fKXgbB2KUPA8k4BxFMf6lZX/c1ZagU/1b9uZWZK0yD6yjfzqAIuTNG4YlRmtMpQiXuohsg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -1604,6 +1966,19 @@
"node": ">=8"
}
},
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
@@ -1636,6 +2011,19 @@
"real-require": "^0.2.0"
}
},
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
"node_modules/toad-cache": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
@@ -1654,12 +2042,29 @@
"node": ">=0.6"
}
},
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
diff --git a/server/package.json b/server/package.json
index 41176e0..f2ae924 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,15 +1,24 @@
{
"name": "blog-server",
"version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "start": "node src/server.js",
+ "dev": "nodemon src/server.js"
+ },
"dependencies": {
"@fastify/cors": "^8.0.0",
"@fastify/jwt": "^7.0.0",
"@fastify/multipart": "^7.0.0",
"@fastify/static": "^6.0.0",
"bcrypt": "^5.0.1",
+ "dotenv": "^16.0.0",
"fastify": "^4.0.0",
"marked": "^4.0.0",
- "pg": "^8.7.1",
+ "pg-promise": "^11.0.0",
"slugify": "^1.6.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.0.0"
}
}
\ No newline at end of file
diff --git a/schema.sql b/server/schema.sql
similarity index 100%
rename from schema.sql
rename to server/schema.sql
diff --git a/server/src/db.js b/server/src/db.js
new file mode 100644
index 0000000..316e7ee
--- /dev/null
+++ b/server/src/db.js
@@ -0,0 +1,16 @@
+import pgPromise from 'pg-promise';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const pgp = pgPromise();
+
+const connectionConfig = {
+ host: process.env.DB_HOST || 'localhost',
+ port: process.env.DB_PORT || 5432,
+ database: process.env.DB_NAME || 'blog',
+ user: process.env.DB_USER || 'postgres',
+ password: process.env.DB_PASSWORD || 'postgres'
+};
+
+export const db = pgp(connectionConfig);
\ No newline at end of file
diff --git a/server/src/routes/auth.js b/server/src/routes/auth.js
index 328b385..d54f0f9 100644
--- a/server/src/routes/auth.js
+++ b/server/src/routes/auth.js
@@ -1,27 +1,56 @@
+// File: src/routes/auth.js
import bcrypt from 'bcrypt';
import { db } from '../db.js';
-export default async function auth(fastify, options) {
+export default async function authRoutes(fastify) {
fastify.post('/api/auth/login', async (request, reply) => {
const { username, password } = request.body;
- const user = await db.oneOrNone(
- 'SELECT * FROM users WHERE username = $1',
- [username]
- );
+ try {
+ const user = await db.oneOrNone(
+ 'SELECT * FROM users WHERE username = $1',
+ [username]
+ );
- if (!user) {
- reply.code(401).send({ error: 'Invalid credentials' });
- return;
+ if (!user) {
+ return reply.code(401).send({ error: 'Invalid credentials' });
+ }
+
+ const valid = await bcrypt.compare(password, user.password_hash);
+ if (!valid) {
+ return reply.code(401).send({ error: 'Invalid credentials' });
+ }
+
+ const token = fastify.jwt.sign({ id: user.id });
+ return { token };
+ } catch (err) {
+ fastify.log.error(err);
+ return reply.code(500).send({ error: 'Internal server error' });
}
+ });
- const valid = await bcrypt.compare(password, user.password_hash);
- if (!valid) {
- reply.code(401).send({ error: 'Invalid credentials' });
- return;
+ // Add a test user if none exists
+ fastify.post('/api/auth/setup', async (request, reply) => {
+ try {
+ const userExists = await db.oneOrNone('SELECT id FROM users LIMIT 1');
+
+ if (!userExists) {
+ const password = 'admin123'; // Default password
+ const salt = await bcrypt.genSalt(10);
+ const hash = await bcrypt.hash(password, salt);
+
+ await db.one(
+ 'INSERT INTO users (username, password_hash) VALUES ($1, $2) RETURNING id',
+ ['admin', hash]
+ );
+
+ return { message: 'Test user created. Username: admin, Password: admin123' };
+ }
+
+ return { message: 'Users already exist' };
+ } catch (err) {
+ fastify.log.error(err);
+ return reply.code(500).send({ error: 'Internal server error' });
}
-
- const token = fastify.jwt.sign({ id: user.id });
- reply.send({ token });
});
}
\ No newline at end of file
diff --git a/server/src/server.js b/server/src/server.js
index 25bfea6..d404521 100644
--- a/server/src/server.js
+++ b/server/src/server.js
@@ -2,35 +2,63 @@ import Fastify from 'fastify';
import cors from '@fastify/cors';
import jwt from '@fastify/jwt';
import multipart from '@fastify/multipart';
-import static from '@fastify/static';
+import fastifyStatic from '@fastify/static';
import { join } from 'path';
import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+import dotenv from 'dotenv';
+import authRoutes from './routes/auth.js';
-const app = Fastify({ logger: true });
+// Load environment variables
+dotenv.config();
-app.register(cors, {
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const app = Fastify({
+ logger: true,
+ ajv: {
+ customOptions: {
+ removeAdditional: false,
+ useDefaults: true,
+ coerceTypes: true,
+ allErrors: true
+ }
+ }
+});
+
+// Register plugins
+await app.register(cors, {
origin: process.env.FRONTEND_URL || 'http://localhost:3000'
});
-app.register(jwt, {
- secret: process.env.JWT_SECRET
+await app.register(jwt, {
+ secret: process.env.JWT_SECRET || 'your-super-secret-key-change-this-in-production'
});
-app.register(multipart);
+await app.register(multipart);
-app.register(static, {
- root: join(fileURLToPath(import.meta.url), '../../uploads'),
+await app.register(fastifyStatic, {
+ root: join(__dirname, '../uploads'),
prefix: '/uploads/'
});
-app.register(import('./routes/auth.js'));
-app.register(import('./routes/posts.js'));
-app.register(import('./routes/tags.js'));
-app.register(import('./routes/images.js'));
+// Register routes
+await app.register(authRoutes);
+// Testing route
+app.get('/', async (request, reply) => {
+ return { hello: 'world' }
+});
+
+// Start the server
const start = async () => {
try {
- await app.listen({ port: 3001 });
+ await app.listen({
+ port: process.env.PORT || 3001,
+ host: process.env.HOST || 'localhost'
+ });
+ console.log(`Server listening on ${app.server.address().port}`);
} catch (err) {
app.log.error(err);
process.exit(1);