From 116b4be72f6665906fdbb3a801a78aaf9a4ec699 Mon Sep 17 00:00:00 2001 From: rishi Date: Sun, 1 Dec 2024 13:17:12 +0000 Subject: [PATCH] Created server and client --- client/src/App.js | 47 ++++ client/src/components/Dashboard.js | 82 ++++++ client/src/components/Editor.js | 154 +++++++++++ client/src/components/Login.js | 52 ++++ client/src/styles/main.scss | 66 +++++ server/package-lock.json | 407 ++++++++++++++++++++++++++++- server/package.json | 11 +- schema.sql => server/schema.sql | 0 server/src/db.js | 16 ++ server/src/routes/auth.js | 59 +++-- server/src/server.js | 54 +++- 11 files changed, 918 insertions(+), 30 deletions(-) create mode 100644 client/src/App.js create mode 100644 client/src/components/Dashboard.js create mode 100644 client/src/components/Editor.js create mode 100644 client/src/components/Login.js create mode 100644 client/src/styles/main.scss rename schema.sql => server/schema.sql (100%) create mode 100644 server/src/db.js 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 ( +
+ setTitle(e.target.value)} + /> + + setPublishDate(e.target.value)} + /> + +
+ {tags.map((tag, index) => ( + + {tag} + setTags(tags.filter((_, i) => i !== index))} + > + × + + + ))} + setTagInput(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter' && tagInput.trim()) { + setTags([...tags, tagInput.trim()]); + setTagInput(''); + } + }} + /> +
+ + { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.onchange = async () => { + const file = input.files[0]; + const imageUrl = await handleImageUpload(file); + if (imageUrl) { + const imageMarkdown = `![${file.name}](${imageUrl})`; + api.replaceSelection(imageMarkdown); + } + }; + input.click(); + }, + }, + ]} + /> + +
+ + +
+
+ ); +} \ 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 ( +
+
+

Login

+ {error &&
{error}
} +
+ + setUsername(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ +
+
+ ); +} \ 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);