Created server and client
This commit is contained in:
47
client/src/App.js
Normal file
47
client/src/App.js
Normal 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>
|
||||
);
|
||||
}
|
||||
82
client/src/components/Dashboard.js
Normal file
82
client/src/components/Dashboard.js
Normal 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>
|
||||
);
|
||||
}
|
||||
154
client/src/components/Editor.js
Normal file
154
client/src/components/Editor.js
Normal 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 = ``;
|
||||
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>
|
||||
);
|
||||
}
|
||||
52
client/src/components/Login.js
Normal file
52
client/src/components/Login.js
Normal 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>
|
||||
);
|
||||
}
|
||||
66
client/src/styles/main.scss
Normal file
66
client/src/styles/main.scss
Normal 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
407
server/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
16
server/src/db.js
Normal 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);
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user