Created server and client

This commit is contained in:
2024-12-01 13:17:12 +00:00
parent c4e4affa7c
commit 116b4be72f
11 changed files with 918 additions and 30 deletions

47
client/src/App.js Normal file
View File

@@ -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 : <Navigate to="/login" />;
}
export default function App() {
return (
<Router>
<div className="container">
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route
path="/new"
element={
<PrivateRoute>
<Editor />
</PrivateRoute>
}
/>
<Route
path="/edit/:id"
element={
<PrivateRoute>
<Editor />
</PrivateRoute>
}
/>
<Route path="/" element={<Navigate to="/dashboard" />} />
</Routes>
</div>
</Router>
);
}

View File

@@ -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 (
<div className="dashboard">
<div className="dashboard-header">
<h1>My Posts</h1>
<Link to="/new" className="button">New Post</Link>
</div>
<div className="filters">
<button
className={`filter ${filter === 'all' ? 'active' : ''}`}
onClick={() => setFilter('all')}
>
All
</button>
<button
className={`filter ${filter === 'draft' ? 'active' : ''}`}
onClick={() => setFilter('draft')}
>
Drafts
</button>
<button
className={`filter ${filter === 'published' ? 'active' : ''}`}
onClick={() => setFilter('published')}
>
Published
</button>
</div>
<div className="posts-list">
{filteredPosts.map(post => (
<div key={post.id} className="post-item">
<div className="post-title">
<Link to={`/edit/${post.id}`}>{post.title}</Link>
<span className={`status ${post.status}`}>{post.status}</span>
</div>
<div className="post-meta">
<span className="date">
{post.published_at
? format(new Date(post.published_at), 'MMM d, yyyy')
: 'Not published'}
</span>
<div className="tags">
{post.tags.map((tag, index) => (
<span key={index} className="tag">{tag}</span>
))}
</div>
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -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 (
<div className="post-editor">
<input
type="text"
placeholder="Post title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<input
type="datetime-local"
value={publishDate}
onChange={(e) => setPublishDate(e.target.value)}
/>
<div className="tags-input">
{tags.map((tag, index) => (
<span key={index} className="tag">
{tag}
<span
className="remove"
onClick={() => setTags(tags.filter((_, i) => i !== index))}
>
×
</span>
</span>
))}
<input
type="text"
placeholder="Add a tag"
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter' && tagInput.trim()) {
setTags([...tags, tagInput.trim()]);
setTagInput('');
}
}}
/>
</div>
<MDEditor
value={content}
onChange={setContent}
preview="edit-preview"
height={500}
commands={[
{
name: 'image',
keyCommand: 'image',
buttonProps: { 'aria-label': 'Insert image' },
execute: async (state, api) => {
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();
},
},
]}
/>
<div className="buttons">
<button className="button draft" onClick={() => handleSave('draft')}>
Save as Draft
</button>
<button className="button" onClick={() => handleSave('published')}>
Publish
</button>
</div>
</div>
);
}

View File

@@ -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 (
<div className="login-container">
<form onSubmit={handleSubmit} className="login-form">
<h2>Login</h2>
{error && <div className="error">{error}</div>}
<div className="form-group">
<label>Username</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div className="form-group">
<label>Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" className="button">Login</button>
</form>
</div>
);
}

View File

@@ -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;
}
}
}

407
server/package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}

16
server/src/db.js Normal file
View File

@@ -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);

View File

@@ -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 });
});
}

View File

@@ -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);