- Регистрация
- 1 Мар 2015
- Сообщения
- 1,481
- Баллы
- 155
Creating a post publishing system for your website with a blogs page and a secure admin panel using Neon Database's free trial is an exciting project. This guide will walk you through the entire process in a detailed, step-by-step manner, covering the architecture, technologies, database setup, backend and frontend development, security measures, and deployment. The goal is to create a robust system where posts are displayed on a public blogs page, and only authorized admins can access a secret admin panel to create, edit, publish, or delete posts.
Table of Contents
The post publishing system will consist of two main components:
The system will use:
The architecture will follow a client-server model:
Here’s a breakdown of the tools and technologies we’ll use:
Neon Database provides a free-tier serverless PostgreSQL database, perfect for this project. Let’s set it up.
Step 1: Create a Neon Project
We need two tables:
-- Create posts table
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
is_published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create admins table
CREATE TABLE admins (
id SERIAL PRIMARY KEY,
auth0_id VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'admin',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Grant privileges to public schema
GRANT CREATE ON SCHEMA public TO PUBLIC;
INSERT INTO posts (title, content, slug, is_published)
VALUES (
'Welcome to AquaScript',
'This is the first blog post on AquaScript.xyz!',
'welcome-to-aquascript',
TRUE
);
The backend will be a Node.js application using Express to create a RESTful API. It will handle CRUD operations for posts and admin authentication.
Step 1: Set Up the Backend Project
mkdir aquascript-blog-backend
cd aquascript-blog-backend
npm init -y
npm install express pg cors dotenv jsonwebtoken express-jwt @auth0/auth0-spa-js
npm install --save-dev nodemon
DATABASE_URL=postgresql://username:password@ep-project-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require
PORT=5000
AUTH0_DOMAIN=your-auth0-domain.auth0.com
AUTH0_AUDIENCE=your-auth0-api-identifier
AUTH0_CLIENT_ID=your-auth0-client-id
Replace placeholders with your Neon connection string and Auth0 credentials (obtained later).
const express = require('express');
const cors = require('cors');
const { Pool } = require('pg');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json());
// Database connection
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false }
});
// Test database connection
pool.connect((err) => {
if (err) {
console.error('Database connection error:', err.stack);
} else {
console.log('Connected to Neon Database');
}
});
// Basic route
app.get('/', (req, res) => {
res.json({ message: 'AquaScript Blog API' });
});
// Start server
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
npm run dev
Visit to see the API response.
Step 2: Create API Endpoints
We’ll create endpoints for posts and admin management.
const express = require('express');
const router = express.Router();
const { Pool } = require('pg');
require('dotenv').config();
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false }
});
// Get all published posts (public)
router.get('/', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM posts WHERE is_published = TRUE ORDER BY created_at DESC');
res.json(result.rows);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Get single post by slug (public)
router.get('/:slug', async (req, res) => {
const { slug } = req.params;
try {
const result = await pool.query('SELECT * FROM posts WHERE slug = $1 AND is_published = TRUE', [slug]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Create a post (admin only)
router.post('/', async (req, res) => {
const { title, content, slug, is_published } = req.body;
try {
const result = await pool.query(
'INSERT INTO posts (title, content, slug, is_published) VALUES ($1, $2, $3, $4) RETURNING *',
[title, content, slug, is_published]
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Update a post (admin only)
router.put('/:id', async (req, res) => {
const { id } = req.params;
const { title, content, slug, is_published } = req.body;
try {
const result = await pool.query(
'UPDATE posts SET title = $1, content = $2, slug = $3, is_published = $4, updated_at = CURRENT_TIMESTAMP WHERE id = $5 RETURNING *',
[title, content, slug, is_published, id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Delete a post (admin only)
router.delete('/:id', async (req, res) => {
const { id } = req.params;
try {
const result = await pool.query('DELETE FROM posts WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json({ message: 'Post deleted' });
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;
const postsRouter = require('./routes/posts');
app.use('/api/posts', postsRouter);
The frontend will be a React application with two main sections: the blogs page and the admin panel.
Step 1: Set Up the React Project
npx create-react-app aquascript-blog-frontend
cd aquascript-blog-frontend
npm install tailwindcss postcss autoprefixer react-router-dom axios @auth0/auth0-react
npm install --save-dev @tailwindcss/typography
npx tailwindcss init -p
Update tailwind.config.js:
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/typography'),
],
}
Create src/index.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import { Auth0Provider } from '@auth0/auth0-react';
ReactDOM.render(
<Auth0Provider
domain={process.env.REACT_APP_AUTH0_DOMAIN}
clientId={process.env.REACT_APP_AUTH0_CLIENT_ID}
redirectUri={window.location.origin}
audience={process.env.REACT_APP_AUTH0_AUDIENCE}
>
<BrowserRouter>
<App />
</BrowserRouter>
</Auth0Provider>,
document.getElementById('root')
);
REACT_APP_AUTH0_DOMAIN=your-auth0-domain.auth0.com
REACT_APP_AUTH0_CLIENT_ID=your-auth0-client-id
REACT_APP_AUTH0_AUDIENCE=your-auth0-api-identifier
REACT_APP_API_URL=
Step 2: Create the Blogs Page
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Link } from 'react-router-dom';
const Blogs = () => {
const [posts, setPosts] = useState([]);
useEffect(() => {
axios.get(`${process.env.REACT_APP_API_URL}/api/posts`)
.then(response => setPosts(response.data))
.catch(error => console.error('Error fetching posts:', error));
}, []);
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">AquaScript Blogs</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{posts.map(post => (
<div key={post.id} className="border rounded-lg p-4 shadow-md">
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-600 mt-2">{post.content.substring(0, 100)}...</p>
<Link to={`/blogs/${post.slug}`} className="text-blue-500 hover:underline">
Read More
</Link>
</div>
))}
</div>
</div>
);
};
export default Blogs;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useParams } from 'react-router-dom';
const Post = () => {
const { slug } = useParams();
const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
axios.get(`${process.env.REACT_APP_API_URL}/api/posts/${slug}`)
.then(response => {
setPost(response.data);
setLoading(false);
})
.catch(error => {
console.error('Error fetching post:', error);
setLoading(false);
});
}, [slug]);
if (loading) return <div>Loading...</div>;
if (!post) return <div>Post not found</div>;
return (
<div className="container mx-auto p-4">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="prose" dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
);
};
export default Post;
6. Implementing Authentication and Security
To secure the admin panel, we’ll use Auth0 for authentication and role-based access control.
Step 1: Set Up Auth0
npm install jwks-rsa
const jwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');
const checkJwt = jwt({
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`
}),
audience: process.env.AUTH0_AUDIENCE,
issuer: `https://${process.env.AUTH0_DOMAIN}/`,
algorithms: ['RS256']
});
const checkAdmin = (req, res, next) => {
const roles = req.user[''] || [];
if (!roles.includes('admin')) {
return res.status(403).json({ error: 'Access denied' });
}
next();
};
module.exports = { checkJwt, checkAdmin };
const { checkJwt, checkAdmin } = require('../middleware/auth');
router.post('/', checkJwt, checkAdmin, async (req, res) => { /* ... */ });
router.put('/:id', checkJwt, checkAdmin, async (req, res) => { /* ... */ });
router.delete('/:id', checkJwt, checkAdmin, async (req, res) => { /* ... */ });
7. Creating the Admin Panel
The admin panel will be a React component accessible only to authenticated admins.
Step 1: Create Admin Component
Create src/components/Admin.js:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useAuth0 } from '@auth0/auth0-react';
import { Link } from 'react-router-dom';
const Admin = () => {
const { user, isAuthenticated, loginWithRedirect, getAccessTokenSilently } = useAuth0();
const [posts, setPosts] = useState([]);
const [form, setForm] = useState({ title: '', content: '', slug: '', is_published: false });
useEffect(() => {
if (isAuthenticated) {
fetchPosts();
}
}, [isAuthenticated]);
const fetchPosts = async () => {
try {
const token = await getAccessTokenSilently();
const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/posts`, {
headers: { Authorization: `Bearer ${token}` }
});
setPosts(response.data);
} catch (error) {
console.error('Error fetching posts:', error);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const token = await getAccessTokenSilently();
await axios.post(`${process.env.REACT_APP_API_URL}/api/posts`, form, {
headers: { Authorization: `Bearer ${token}` }
});
fetchPosts();
setForm({ title: '', content: '', slug: '', is_published: false });
} catch (error) {
console.error('Error creating post:', error);
}
};
const handleDelete = async (id) => {
try {
const token = await getAccessTokenSilently();
await axios.delete(`${process.env.REACT_APP_API_URL}/api/posts/${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
fetchPosts();
} catch (error) {
console.error('Error deleting post:', error);
}
};
if (!isAuthenticated) {
loginWithRedirect();
return null;
}
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">Admin Panel</h1>
<form onSubmit={handleSubmit} className="mb-8">
<div className="mb-4">
<label className="block text-sm font-medium">Title</label>
<input
type="text"
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium">Content</label>
<textarea
value={form.content}
onChange={(e) => setForm({ ...form, content: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium">Slug</label>
<input
type="text"
value={form.slug}
onChange={(e) => setForm({ ...form, slug: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="inline-flex items-center">
<input
type="checkbox"
checked={form.is_published}
onChange={(e) => setForm({ ...form, is_published: e.target.checked })}
/>
<span className="ml-2">Published</span>
</label>
</div>
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
Create Post
</button>
</form>
<h2 className="text-2xl font-semibold mb-4">Existing Posts</h2>
<div className="grid grid-cols-1 gap-4">
{posts.map(post => (
<div key={post.id} className="border rounded-lg p-4 shadow-md">
<h3 className="text-lg font-semibold">{post.title}</h3>
<p>{post.is_published ? 'Published' : 'Draft'}</p>
<div className="mt-2">
<Link to={`/admin/edit/${post.id}`} className="text-blue-500 hover:underline mr-4">
Edit
</Link>
<button
onClick={() => handleDelete(post.id)}
className="text-red-500 hover:underline"
>
Delete
</button>
</div>
</div>
))}
</div>
</div>
);
};
export default Admin;
Step 2: Create Edit Post Component
Create src/components/EditPost.js:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useParams, useHistory } from 'react-router-dom';
import { useAuth0 } from '@auth0/auth0-react';
const EditPost = () => {
const { id } = useParams();
const history = useHistory();
const { getAccessTokenSilently } = useAuth0();
const [form, setForm] = useState({ title: '', content: '', slug: '', is_published: false });
useEffect(() => {
const fetchPost = async () => {
try {
const token = await getAccessTokenSilently();
const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/posts/${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
setForm(response.data);
} catch (error) {
console.error('Error fetching post:', error);
}
};
fetchPost();
}, [id, getAccessTokenSilently]);
const handleSubmit = async (e) => {
e.preventDefault();
try {
const token = await getAccessTokenSilently();
await axios.put(`${process.env.REACT_APP_API_URL}/api/posts/${id}`, form, {
headers: { Authorization: `Bearer ${token}` }
});
history.push('/admin');
} catch (error) {
console.error('Error updating post:', error);
}
};
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">Edit Post</h1>
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-sm font-medium">Title</label>
<input
type="text"
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium">Content</label>
<textarea
value={form.content}
onChange={(e) => setForm({ ...form, content: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium">Slug</label>
<input
type="text"
value={form.slug}
onChange={(e) => setForm({ ...form, slug: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="inline-flex items-center">
<input
type="checkbox"
checked={form.is_published}
onChange={(e) => setForm({ ...form, is_published: e.target.checked })}
/>
<span className="ml-2">Published</span>
</label>
</div>
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
Update Post
</button>
</form>
</div>
);
};
export default EditPost;
Step 3: Set Up Routing
Update src/App.js:
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import Blogs from './components/Blogs';
import Post from './components/Post';
import Admin from './components/Admin';
import EditPost from './components/EditPost';
const App = () => {
return (
<Switch>
<Route exact path="/blogs" component={Blogs} />
<Route path="/blogs/:slug" component={Post} />
<Route exact path="/admin" component={Admin} />
<Route path="/admin/edit/:id" component={EditPost} />
</Switch>
);
};
export default App;
8. Building the Blogs Page
The blogs page is already implemented in Blogs.js and Post.js. It fetches published posts and displays them in a grid. Each post links to a detailed view using the slug.
9. Testing the System
Deploy the application to Vercel for easy hosting.
Step 1: Deploy Backend
const express = require('express');
const cors = require('cors');
const { Pool } = require('pg');
require('dotenv').config();
const postsRouter = require('./routes/posts');
const app = express();
const port = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json());
// Database connection
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false }
});
// Test database connection
pool.connect((err) => {
if (err) {
console.error('Database connection error:', err.stack);
} else {
console.log('Connected to Neon Database');
}
});
// Routes
app.get('/', (req, res) => {
res.json({ message: 'AquaScript Blog API' });
});
app.use('/api/posts', postsRouter);
// Start server
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
const express = require('express');
const router = express.Router();
const { Pool } = require('pg');
require('dotenv').config();
const { checkJwt, checkAdmin } = require('../middleware/auth');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false }
});
// Get all published posts (public)
router.get('/', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM posts WHERE is_published = TRUE ORDER BY created_at DESC');
res.json(result.rows);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Get single post by slug (public)
router.get('/:slug', async (req, res) => {
const { slug } = req.params;
try {
const result = await pool.query('SELECT * FROM posts WHERE slug = $1 AND is_published = TRUE', [slug]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Create a post (admin only)
router.post('/', checkJwt, checkAdmin, async (req, res) => {
const { title, content, slug, is_published } = req.body;
try {
const result = await pool.query(
'INSERT INTO posts (title, content, slug, is_published) VALUES ($1, $2, $3, $4) RETURNING *',
[title, content, slug, is_published]
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Update a post (admin only)
router.put('/:id', checkJwt, checkAdmin, async (req, res) => {
const { id } = req.params;
const { title, content, slug, is_published } = req.body;
try {
const result = await pool.query(
'UPDATE posts SET title = $1, content = $2, slug = $3, is_published = $4, updated_at = CURRENT_TIMESTAMP WHERE id = $5 RETURNING *',
[title, content, slug, is_published, id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Delete a post (admin only)
router.delete('/:id', checkJwt, checkAdmin, async (req, res) => {
const { id } = req.params;
try {
const result = await pool.query('DELETE FROM posts WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json({ message: 'Post deleted' });
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useAuth0 } from '@auth0/auth0-react';
import { Link } from 'react-router-dom';
const Admin = () => {
const { user, isAuthenticated, loginWithRedirect, getAccessTokenSilently } = useAuth0();
const [posts, setPosts] = useState([]);
const [form, setForm] = useState({ title: '', content: '', slug: '', is_published: false });
useEffect(() => {
if (isAuthenticated) {
fetchPosts();
}
}, [isAuthenticated]);
const fetchPosts = async () => {
try {
const token = await getAccessTokenSilently();
const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/posts`, {
headers: { Authorization: `Bearer ${token}` }
});
setPosts(response.data);
} catch (error) {
console.error('Error fetching posts:', error);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const token = await getAccessTokenSilently();
await axios.post(`${process.env.REACT_APP_API_URL}/api/posts`, form, {
headers: { Authorization: `Bearer ${token}` }
});
fetchPosts();
setForm({ title: '', content: '', slug: '', is_published: false });
} catch (error) {
console.error('Error creating post:', error);
}
};
const handleDelete = async (id) => {
try {
const token = await getAccessTokenSilently();
await axios.delete(`${process.env.REACT_APP_API_URL}/api/posts/${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
fetchPosts();
} catch (error) {
console.error('Error deleting post:', error);
}
};
if (!isAuthenticated) {
loginWithRedirect();
return null;
}
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">Admin Panel</h1>
<form onSubmit={handleSubmit} className="mb-8">
<div className="mb-4">
<label className="block text-sm font-medium">Title</label>
<input
type="text"
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium">Content</label>
<textarea
value={form.content}
onChange={(e) => setForm({ ...form, content: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium">Slug</label>
<input
type="text"
value={form.slug}
onChange={(e) => setForm({ ...form, slug: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="inline-flex items-center">
<input
type="checkbox"
checked={form.is_published}
onChange={(e) => setForm({ ...form, is_published: e.target.checked })}
/>
<span className="ml-2">Published</span>
</label>
</div>
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
Create Post
</button>
</form>
<h2 className="text-2xl font-semibold mb-4">Existing Posts</h2>
<div className="grid grid-cols-1 gap-4">
{posts.map(post => (
<div key={post.id} className="border rounded-lg p-4 shadow-md">
<h3 className="text-lg font-semibold">{post.title}</h3>
<p>{post.is_published ? 'Published' : 'Draft'}</p>
<div className="mt-2">
<Link to={`/admin/edit/${post.id}`} className="text-blue-500 hover:underline mr-4">
Edit
</Link>
<button
onClick={() => handleDelete(post.id)}
className="text-red-500 hover:underline"
>
Delete
</button>
</div>
</div>
))}
</div>
</div>
);
};
export default Admin;
This guide provides a comprehensive roadmap to build your post publishing system. Follow the steps, use the provided code artifacts, and reach out if you encounter issues. Happy coding!
Table of Contents
- Overview of the System
- Technologies and Tools
- Setting Up Neon Database
- Backend Development (Node.js, Express, PostgreSQL)
- Frontend Development (React, Tailwind CSS)
- Implementing Authentication and Security
- Creating the Admin Panel
- Building the Blogs Page
- Testing the System
- Deployment
- Additional Considerations and Best Practices
- Artifacts (Code Samples)
The post publishing system will consist of two main components:
- Public Blogs Page: A page on that displays all published blog posts. Visitors can view posts without authentication.
- Secret Admin Panel: A secure dashboard accessible only to authenticated admins at a hidden URL (e.g., ). Admins can:
- Create new posts.
- Edit existing posts.
- Publish or unpublish posts.
- Delete posts.
The system will use:
- Neon Database (serverless PostgreSQL) to store posts and admin credentials.
- Node.js and Express for the backend API.
- React with Tailwind CSS for the frontend.
- Auth0 for secure admin authentication.
- Vercel for deployment.
The architecture will follow a client-server model:
- The frontend (React) communicates with the backend (Express) via RESTful API endpoints.
- The backend interacts with Neon Database to perform CRUD (Create, Read, Update, Delete) operations.
- Authentication ensures only admins access the admin panel.
Here’s a breakdown of the tools and technologies we’ll use:
- Neon Database: A serverless PostgreSQL database with a free tier, ideal for storing posts and user data. It offers features like autoscaling and branching.
- Node.js and Express: For building a RESTful API to handle post and user management.
- React: For creating a dynamic and responsive frontend for both the blogs page and admin panel.
- Tailwind CSS: For styling the frontend with a utility-first approach.
- Auth0: For secure authentication to restrict admin panel access.
- Vercel: For hosting the frontend and backend.
- PostgreSQL Client (pg): To connect the backend to Neon Database.
- Postman: For testing API endpoints.
- Git and GitHub: For version control.
- Basic knowledge of JavaScript, Node.js, React, and SQL.
- A Neon account (sign up at ).
- An Auth0 account (sign up at ).
- Node.js installed (v16 or higher).
- A code editor (e.g., VS Code).
- A GitHub account.
Neon Database provides a free-tier serverless PostgreSQL database, perfect for this project. Let’s set it up.
Step 1: Create a Neon Project
- Sign Up: Go to and sign up using your email, GitHub, or Google account.
- Create a Project:
- In the Neon Console, click “Create Project.”
- Enter a project name (e.g., aquascript-blog).
- Choose PostgreSQL version 16 (default).
- Select a region close to your users (e.g., US East).
- Click “Create Project.”
Get Connection String:
- After creating the project, Neon will display a connection string like:
postgresql://username:password@ep-project-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require
- Copy this string and save it securely. It’s used to connect to the database.
We need two tables:
- posts: To store blog posts.
- admins: To store admin credentials (though Auth0 will handle authentication, we’ll store admin roles).
Access Neon SQL Editor:
- In the Neon Console, navigate to the “SQL Editor” tab.
- Select the default database neondb and the production branch.
Create Tables:
Run the following SQL commands in the SQL Editor to create the tables:
-- Create posts table
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
is_published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Create admins table
CREATE TABLE admins (
id SERIAL PRIMARY KEY,
auth0_id VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'admin',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Grant privileges to public schema
GRANT CREATE ON SCHEMA public TO PUBLIC;
- posts table:
- id: Unique identifier for each post.
- title: Post title.
- content: Post body.
- slug: URL-friendly string for post URLs (e.g., my-first-post).
- is_published: Boolean to control visibility on the blogs page.
- created_at and updated_at: Timestamps for tracking creation and updates.
- admins table:
- auth0_id: Unique identifier from Auth0 for each admin.
- email: Admin’s email.
- role: Role (e.g., admin).
- created_at: Timestamp for account creation.
- Insert Sample Data (Optional): To test the database, insert a sample post:
INSERT INTO posts (title, content, slug, is_published)
VALUES (
'Welcome to AquaScript',
'This is the first blog post on AquaScript.xyz!',
'welcome-to-aquascript',
TRUE
);
- Verify Setup: Run SELECT * FROM posts; in the SQL Editor to ensure the table and data are created correctly.
The backend will be a Node.js application using Express to create a RESTful API. It will handle CRUD operations for posts and admin authentication.
Step 1: Set Up the Backend Project
- Create a Project Directory:
mkdir aquascript-blog-backend
cd aquascript-blog-backend
npm init -y
- Install Dependencies: Install the required packages:
npm install express pg cors dotenv jsonwebtoken express-jwt @auth0/auth0-spa-js
npm install --save-dev nodemon
- express: Web framework.
- pg: PostgreSQL client for Node.js.
- cors: Enables cross-origin requests.
- dotenv: Loads environment variables.
- jsonwebtoken and express-jwt: For JWT authentication.
- @auth0/auth0-spa-js: For Auth0 integration.
- nodemon: Automatically restarts the server during development.
- Configure Environment Variables: Create a .env file in the root directory:
DATABASE_URL=postgresql://username:password@ep-project-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require
PORT=5000
AUTH0_DOMAIN=your-auth0-domain.auth0.com
AUTH0_AUDIENCE=your-auth0-api-identifier
AUTH0_CLIENT_ID=your-auth0-client-id
Replace placeholders with your Neon connection string and Auth0 credentials (obtained later).
- Set Up Express Server: Create index.js:
const express = require('express');
const cors = require('cors');
const { Pool } = require('pg');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json());
// Database connection
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false }
});
// Test database connection
pool.connect((err) => {
if (err) {
console.error('Database connection error:', err.stack);
} else {
console.log('Connected to Neon Database');
}
});
// Basic route
app.get('/', (req, res) => {
res.json({ message: 'AquaScript Blog API' });
});
// Start server
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
- Update package.json: Add a start script:
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
- Run the Server:
npm run dev
Visit to see the API response.
Step 2: Create API Endpoints
We’ll create endpoints for posts and admin management.
- Posts Endpoints: Create a routes/posts.js file:
const express = require('express');
const router = express.Router();
const { Pool } = require('pg');
require('dotenv').config();
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false }
});
// Get all published posts (public)
router.get('/', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM posts WHERE is_published = TRUE ORDER BY created_at DESC');
res.json(result.rows);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Get single post by slug (public)
router.get('/:slug', async (req, res) => {
const { slug } = req.params;
try {
const result = await pool.query('SELECT * FROM posts WHERE slug = $1 AND is_published = TRUE', [slug]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Create a post (admin only)
router.post('/', async (req, res) => {
const { title, content, slug, is_published } = req.body;
try {
const result = await pool.query(
'INSERT INTO posts (title, content, slug, is_published) VALUES ($1, $2, $3, $4) RETURNING *',
[title, content, slug, is_published]
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Update a post (admin only)
router.put('/:id', async (req, res) => {
const { id } = req.params;
const { title, content, slug, is_published } = req.body;
try {
const result = await pool.query(
'UPDATE posts SET title = $1, content = $2, slug = $3, is_published = $4, updated_at = CURRENT_TIMESTAMP WHERE id = $5 RETURNING *',
[title, content, slug, is_published, id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Delete a post (admin only)
router.delete('/:id', async (req, res) => {
const { id } = req.params;
try {
const result = await pool.query('DELETE FROM posts WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json({ message: 'Post deleted' });
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;
- Integrate Routes: Update index.js to include the posts routes:
const postsRouter = require('./routes/posts');
app.use('/api/posts', postsRouter);
- Test Endpoints: Use Postman to test:
- GET : Retrieve all published posts.
- GET : Retrieve a single post.
- POST : Create a post (requires admin authentication, implemented later).
The frontend will be a React application with two main sections: the blogs page and the admin panel.
Step 1: Set Up the React Project
- Create a React App:
npx create-react-app aquascript-blog-frontend
cd aquascript-blog-frontend
- Install Dependencies: Install Tailwind CSS, React Router, and Axios:
npm install tailwindcss postcss autoprefixer react-router-dom axios @auth0/auth0-react
npm install --save-dev @tailwindcss/typography
- Initialize Tailwind CSS:
npx tailwindcss init -p
Update tailwind.config.js:
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/typography'),
],
}
Create src/index.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
- Update src/index.js: Wrap the app with Auth0 provider:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import { Auth0Provider } from '@auth0/auth0-react';
ReactDOM.render(
<Auth0Provider
domain={process.env.REACT_APP_AUTH0_DOMAIN}
clientId={process.env.REACT_APP_AUTH0_CLIENT_ID}
redirectUri={window.location.origin}
audience={process.env.REACT_APP_AUTH0_AUDIENCE}
>
<BrowserRouter>
<App />
</BrowserRouter>
</Auth0Provider>,
document.getElementById('root')
);
- Configure Environment Variables: Create .env in the frontend root:
REACT_APP_AUTH0_DOMAIN=your-auth0-domain.auth0.com
REACT_APP_AUTH0_CLIENT_ID=your-auth0-client-id
REACT_APP_AUTH0_AUDIENCE=your-auth0-api-identifier
REACT_APP_API_URL=
Step 2: Create the Blogs Page
- Create Blogs Component: Create src/components/Blogs.js:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Link } from 'react-router-dom';
const Blogs = () => {
const [posts, setPosts] = useState([]);
useEffect(() => {
axios.get(`${process.env.REACT_APP_API_URL}/api/posts`)
.then(response => setPosts(response.data))
.catch(error => console.error('Error fetching posts:', error));
}, []);
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">AquaScript Blogs</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{posts.map(post => (
<div key={post.id} className="border rounded-lg p-4 shadow-md">
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-600 mt-2">{post.content.substring(0, 100)}...</p>
<Link to={`/blogs/${post.slug}`} className="text-blue-500 hover:underline">
Read More
</Link>
</div>
))}
</div>
</div>
);
};
export default Blogs;
- Create Single Post Component: Create src/components/Post.js:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useParams } from 'react-router-dom';
const Post = () => {
const { slug } = useParams();
const [post, setPost] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
axios.get(`${process.env.REACT_APP_API_URL}/api/posts/${slug}`)
.then(response => {
setPost(response.data);
setLoading(false);
})
.catch(error => {
console.error('Error fetching post:', error);
setLoading(false);
});
}, [slug]);
if (loading) return <div>Loading...</div>;
if (!post) return <div>Post not found</div>;
return (
<div className="container mx-auto p-4">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="prose" dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
);
};
export default Post;
6. Implementing Authentication and Security
To secure the admin panel, we’ll use Auth0 for authentication and role-based access control.
Step 1: Set Up Auth0
Create an Auth0 Application:
- Sign up at .
- Create a new application (Single Page Application for the frontend, Regular Web Application for the backend).
- Note the Domain, Client ID, and Audience from the application settings.
Create an API:
- In Auth0, go to “APIs” and create a new API.
- Set the identifier (e.g., ).
- Note the audience.
Configure Rules:
- Create a rule to add admin roles to the JWT token:
function (user, context, callback) {
const namespace = 'https://aquascript.xyz';
context.accessToken[namespace + '/roles'] = user.roles || ['admin'];
callback(null, user, context);
}
Update Environment Variables:
Add Auth0 credentials to .env files in both backend and frontend projects.
- Install Auth0 Middleware: Ensure express-jwt and jwks-rsa are installed:
npm install jwks-rsa
- Create Middleware: Create middleware/auth.js in the backend:
const jwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');
const checkJwt = jwt({
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`
}),
audience: process.env.AUTH0_AUDIENCE,
issuer: `https://${process.env.AUTH0_DOMAIN}/`,
algorithms: ['RS256']
});
const checkAdmin = (req, res, next) => {
const roles = req.user[''] || [];
if (!roles.includes('admin')) {
return res.status(403).json({ error: 'Access denied' });
}
next();
};
module.exports = { checkJwt, checkAdmin };
- Protect Admin Routes: Update routes/posts.js to protect create, update, and delete endpoints:
const { checkJwt, checkAdmin } = require('../middleware/auth');
router.post('/', checkJwt, checkAdmin, async (req, res) => { /* ... */ });
router.put('/:id', checkJwt, checkAdmin, async (req, res) => { /* ... */ });
router.delete('/:id', checkJwt, checkAdmin, async (req, res) => { /* ... */ });
7. Creating the Admin Panel
The admin panel will be a React component accessible only to authenticated admins.
Step 1: Create Admin Component
Create src/components/Admin.js:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useAuth0 } from '@auth0/auth0-react';
import { Link } from 'react-router-dom';
const Admin = () => {
const { user, isAuthenticated, loginWithRedirect, getAccessTokenSilently } = useAuth0();
const [posts, setPosts] = useState([]);
const [form, setForm] = useState({ title: '', content: '', slug: '', is_published: false });
useEffect(() => {
if (isAuthenticated) {
fetchPosts();
}
}, [isAuthenticated]);
const fetchPosts = async () => {
try {
const token = await getAccessTokenSilently();
const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/posts`, {
headers: { Authorization: `Bearer ${token}` }
});
setPosts(response.data);
} catch (error) {
console.error('Error fetching posts:', error);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const token = await getAccessTokenSilently();
await axios.post(`${process.env.REACT_APP_API_URL}/api/posts`, form, {
headers: { Authorization: `Bearer ${token}` }
});
fetchPosts();
setForm({ title: '', content: '', slug: '', is_published: false });
} catch (error) {
console.error('Error creating post:', error);
}
};
const handleDelete = async (id) => {
try {
const token = await getAccessTokenSilently();
await axios.delete(`${process.env.REACT_APP_API_URL}/api/posts/${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
fetchPosts();
} catch (error) {
console.error('Error deleting post:', error);
}
};
if (!isAuthenticated) {
loginWithRedirect();
return null;
}
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">Admin Panel</h1>
<form onSubmit={handleSubmit} className="mb-8">
<div className="mb-4">
<label className="block text-sm font-medium">Title</label>
<input
type="text"
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium">Content</label>
<textarea
value={form.content}
onChange={(e) => setForm({ ...form, content: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium">Slug</label>
<input
type="text"
value={form.slug}
onChange={(e) => setForm({ ...form, slug: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="inline-flex items-center">
<input
type="checkbox"
checked={form.is_published}
onChange={(e) => setForm({ ...form, is_published: e.target.checked })}
/>
<span className="ml-2">Published</span>
</label>
</div>
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
Create Post
</button>
</form>
<h2 className="text-2xl font-semibold mb-4">Existing Posts</h2>
<div className="grid grid-cols-1 gap-4">
{posts.map(post => (
<div key={post.id} className="border rounded-lg p-4 shadow-md">
<h3 className="text-lg font-semibold">{post.title}</h3>
<p>{post.is_published ? 'Published' : 'Draft'}</p>
<div className="mt-2">
<Link to={`/admin/edit/${post.id}`} className="text-blue-500 hover:underline mr-4">
Edit
</Link>
<button
onClick={() => handleDelete(post.id)}
className="text-red-500 hover:underline"
>
Delete
</button>
</div>
</div>
))}
</div>
</div>
);
};
export default Admin;
Step 2: Create Edit Post Component
Create src/components/EditPost.js:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useParams, useHistory } from 'react-router-dom';
import { useAuth0 } from '@auth0/auth0-react';
const EditPost = () => {
const { id } = useParams();
const history = useHistory();
const { getAccessTokenSilently } = useAuth0();
const [form, setForm] = useState({ title: '', content: '', slug: '', is_published: false });
useEffect(() => {
const fetchPost = async () => {
try {
const token = await getAccessTokenSilently();
const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/posts/${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
setForm(response.data);
} catch (error) {
console.error('Error fetching post:', error);
}
};
fetchPost();
}, [id, getAccessTokenSilently]);
const handleSubmit = async (e) => {
e.preventDefault();
try {
const token = await getAccessTokenSilently();
await axios.put(`${process.env.REACT_APP_API_URL}/api/posts/${id}`, form, {
headers: { Authorization: `Bearer ${token}` }
});
history.push('/admin');
} catch (error) {
console.error('Error updating post:', error);
}
};
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">Edit Post</h1>
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-sm font-medium">Title</label>
<input
type="text"
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium">Content</label>
<textarea
value={form.content}
onChange={(e) => setForm({ ...form, content: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium">Slug</label>
<input
type="text"
value={form.slug}
onChange={(e) => setForm({ ...form, slug: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="inline-flex items-center">
<input
type="checkbox"
checked={form.is_published}
onChange={(e) => setForm({ ...form, is_published: e.target.checked })}
/>
<span className="ml-2">Published</span>
</label>
</div>
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
Update Post
</button>
</form>
</div>
);
};
export default EditPost;
Step 3: Set Up Routing
Update src/App.js:
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import Blogs from './components/Blogs';
import Post from './components/Post';
import Admin from './components/Admin';
import EditPost from './components/EditPost';
const App = () => {
return (
<Switch>
<Route exact path="/blogs" component={Blogs} />
<Route path="/blogs/:slug" component={Post} />
<Route exact path="/admin" component={Admin} />
<Route path="/admin/edit/:id" component={EditPost} />
</Switch>
);
};
export default App;
8. Building the Blogs Page
The blogs page is already implemented in Blogs.js and Post.js. It fetches published posts and displays them in a grid. Each post links to a detailed view using the slug.
9. Testing the System
Backend Testing:
- Use Postman to test all API endpoints.
- Verify that admin-only endpoints require a valid JWT token with the admin role.
Frontend Testing:
- Run the frontend: npm start.
- Visit to see the blogs page.
- Visit to test the admin panel (requires login).
- Test creating, editing, and deleting posts.
Database Testing:
- Use Neon’s SQL Editor to verify that posts are created, updated, and deleted correctly.
Deploy the application to Vercel for easy hosting.
Step 1: Deploy Backend
Push to GitHub:
- Create a GitHub repository for the backend.
- Push the code:
git init
git add .
git commit -m "Initial commit"
git remote add origin <repository-url>
git push origin main
Deploy to Vercel:
- Sign up at .
- Import the backend repository.
- Add environment variables (DATABASE_URL, AUTH0_*) in Vercel’s dashboard.
- Deploy the project. Note the URL (e.g., ).
Push to GitHub:
- Create a separate GitHub repository for the frontend.
- Push the code.
Deploy to Vercel:
- Import the frontend repository.
- Add environment variables (REACT_APP_*).
- Deploy the project. Update the REACT_APP_API_URL to the backend Vercel URL.
Update Auth0:
- Add the Vercel frontend URL to Auth0’s “Allowed Callback URLs” and “Allowed Logout URLs”.
Test Deployment:
- Visit the deployed blogs page (e.g., ).
- Test the admin panel and ensure authentication works.
- Security:
- Use HTTPS for all API calls.
- Sanitize user inputs to prevent SQL injection and XSS attacks.
- Regularly rotate Auth0 credentials and database passwords.
- Performance:
- Use Neon’s autoscaling to handle traffic spikes.
- Implement caching for the blogs page using a CDN or server-side caching.
- SEO:
- Add meta tags to blog posts for better search engine visibility.
- Generate sitemaps for the blogs page.
- Scalability:
- Use Neon’s branching for development and testing environments.
- Monitor database performance using Neon’s tools.
- Backup:
- Regularly back up the Neon database using the Neon Console or automated scripts.
const express = require('express');
const cors = require('cors');
const { Pool } = require('pg');
require('dotenv').config();
const postsRouter = require('./routes/posts');
const app = express();
const port = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json());
// Database connection
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false }
});
// Test database connection
pool.connect((err) => {
if (err) {
console.error('Database connection error:', err.stack);
} else {
console.log('Connected to Neon Database');
}
});
// Routes
app.get('/', (req, res) => {
res.json({ message: 'AquaScript Blog API' });
});
app.use('/api/posts', postsRouter);
// Start server
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
const express = require('express');
const router = express.Router();
const { Pool } = require('pg');
require('dotenv').config();
const { checkJwt, checkAdmin } = require('../middleware/auth');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false }
});
// Get all published posts (public)
router.get('/', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM posts WHERE is_published = TRUE ORDER BY created_at DESC');
res.json(result.rows);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Get single post by slug (public)
router.get('/:slug', async (req, res) => {
const { slug } = req.params;
try {
const result = await pool.query('SELECT * FROM posts WHERE slug = $1 AND is_published = TRUE', [slug]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Create a post (admin only)
router.post('/', checkJwt, checkAdmin, async (req, res) => {
const { title, content, slug, is_published } = req.body;
try {
const result = await pool.query(
'INSERT INTO posts (title, content, slug, is_published) VALUES ($1, $2, $3, $4) RETURNING *',
[title, content, slug, is_published]
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Update a post (admin only)
router.put('/:id', checkJwt, checkAdmin, async (req, res) => {
const { id } = req.params;
const { title, content, slug, is_published } = req.body;
try {
const result = await pool.query(
'UPDATE posts SET title = $1, content = $2, slug = $3, is_published = $4, updated_at = CURRENT_TIMESTAMP WHERE id = $5 RETURNING *',
[title, content, slug, is_published, id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(result.rows[0]);
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
// Delete a post (admin only)
router.delete('/:id', checkJwt, checkAdmin, async (req, res) => {
const { id } = req.params;
try {
const result = await pool.query('DELETE FROM posts WHERE id = $1 RETURNING *', [id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Post not found' });
}
res.json({ message: 'Post deleted' });
} catch (err) {
console.error(err.stack);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useAuth0 } from '@auth0/auth0-react';
import { Link } from 'react-router-dom';
const Admin = () => {
const { user, isAuthenticated, loginWithRedirect, getAccessTokenSilently } = useAuth0();
const [posts, setPosts] = useState([]);
const [form, setForm] = useState({ title: '', content: '', slug: '', is_published: false });
useEffect(() => {
if (isAuthenticated) {
fetchPosts();
}
}, [isAuthenticated]);
const fetchPosts = async () => {
try {
const token = await getAccessTokenSilently();
const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/posts`, {
headers: { Authorization: `Bearer ${token}` }
});
setPosts(response.data);
} catch (error) {
console.error('Error fetching posts:', error);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const token = await getAccessTokenSilently();
await axios.post(`${process.env.REACT_APP_API_URL}/api/posts`, form, {
headers: { Authorization: `Bearer ${token}` }
});
fetchPosts();
setForm({ title: '', content: '', slug: '', is_published: false });
} catch (error) {
console.error('Error creating post:', error);
}
};
const handleDelete = async (id) => {
try {
const token = await getAccessTokenSilently();
await axios.delete(`${process.env.REACT_APP_API_URL}/api/posts/${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
fetchPosts();
} catch (error) {
console.error('Error deleting post:', error);
}
};
if (!isAuthenticated) {
loginWithRedirect();
return null;
}
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">Admin Panel</h1>
<form onSubmit={handleSubmit} className="mb-8">
<div className="mb-4">
<label className="block text-sm font-medium">Title</label>
<input
type="text"
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium">Content</label>
<textarea
value={form.content}
onChange={(e) => setForm({ ...form, content: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium">Slug</label>
<input
type="text"
value={form.slug}
onChange={(e) => setForm({ ...form, slug: e.target.value })}
className="w-full border rounded p-2"
/>
</div>
<div className="mb-4">
<label className="inline-flex items-center">
<input
type="checkbox"
checked={form.is_published}
onChange={(e) => setForm({ ...form, is_published: e.target.checked })}
/>
<span className="ml-2">Published</span>
</label>
</div>
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
Create Post
</button>
</form>
<h2 className="text-2xl font-semibold mb-4">Existing Posts</h2>
<div className="grid grid-cols-1 gap-4">
{posts.map(post => (
<div key={post.id} className="border rounded-lg p-4 shadow-md">
<h3 className="text-lg font-semibold">{post.title}</h3>
<p>{post.is_published ? 'Published' : 'Draft'}</p>
<div className="mt-2">
<Link to={`/admin/edit/${post.id}`} className="text-blue-500 hover:underline mr-4">
Edit
</Link>
<button
onClick={() => handleDelete(post.id)}
className="text-red-500 hover:underline"
>
Delete
</button>
</div>
</div>
))}
</div>
</div>
);
};
export default Admin;
This guide provides a comprehensive roadmap to build your post publishing system. Follow the steps, use the provided code artifacts, and reach out if you encounter issues. Happy coding!