ABC
Hanzla Baig

Hanzla Baig @hanzla-baig

About: In my projects, I aim to push the boundaries of conventional web design, incorporating advanced animations, responsive layouts, and dynmic features that captivate and retain users. So i like this

Location:
Chichawatni, District Sahiwal
Joined:
Aug 18, 2024

ABC

Publish Date: May 18
1 0

Creating a post publishing system for your website https://aquascript.xyz 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

  1. Overview of the System
  2. Technologies and Tools
  3. Setting Up Neon Database
  4. Backend Development (Node.js, Express, PostgreSQL)
  5. Frontend Development (React, Tailwind CSS)
  6. Implementing Authentication and Security
  7. Creating the Admin Panel
  8. Building the Blogs Page
  9. Testing the System
  10. Deployment
  11. Additional Considerations and Best Practices
  12. Artifacts (Code Samples)

1. Overview of the System

The post publishing system will consist of two main components:

  • Public Blogs Page: A page on https://aquascript.xyz/blogs 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., https://aquascript.xyz/admin). 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.

2. Technologies and Tools

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.

Prerequisites

  • Basic knowledge of JavaScript, Node.js, React, and SQL.
  • A Neon account (sign up at https://neon.tech).
  • An Auth0 account (sign up at https://auth0.com).
  • Node.js installed (v16 or higher).
  • A code editor (e.g., VS Code).
  • A GitHub account.

3. Setting Up Neon Database

Neon Database provides a free-tier serverless PostgreSQL database, perfect for this project. Let’s set it up.

Step 1: Create a Neon Project

  1. Sign Up: Go to https://neon.tech and sign up using your email, GitHub, or Google account.
  2. 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.”
  3. 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.

Step 2: Create Database Schema

We need two tables:

  • posts: To store blog posts.
  • admins: To store admin credentials (though Auth0 will handle authentication, we’ll store admin roles).
  1. Access Neon SQL Editor:

    • In the Neon Console, navigate to the “SQL Editor” tab.
    • Select the default database neondb and the production branch.
  2. 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;
Enter fullscreen mode Exit fullscreen mode
  • 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.
  1. 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
   );
Enter fullscreen mode Exit fullscreen mode
  1. Verify Setup: Run SELECT * FROM posts; in the SQL Editor to ensure the table and data are created correctly.

4. Backend Development (Node.js, Express, PostgreSQL)

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

  1. Create a Project Directory:
   mkdir aquascript-blog-backend
   cd aquascript-blog-backend
   npm init -y
Enter fullscreen mode Exit fullscreen mode
  1. Install Dependencies: Install the required packages:
   npm install express pg cors dotenv jsonwebtoken express-jwt @auth0/auth0-spa-js
   npm install --save-dev nodemon
Enter fullscreen mode Exit fullscreen mode
  • 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.
  1. 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
Enter fullscreen mode Exit fullscreen mode

Replace placeholders with your Neon connection string and Auth0 credentials (obtained later).

  1. 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}`);
   });
Enter fullscreen mode Exit fullscreen mode
  1. Update package.json: Add a start script:
   "scripts": {
       "start": "node index.js",
       "dev": "nodemon index.js"
   }
Enter fullscreen mode Exit fullscreen mode
  1. Run the Server:
   npm run dev
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:5000 to see the API response.

Step 2: Create API Endpoints

We’ll create endpoints for posts and admin management.

  1. 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;
Enter fullscreen mode Exit fullscreen mode
  1. Integrate Routes: Update index.js to include the posts routes:
   const postsRouter = require('./routes/posts');
   app.use('/api/posts', postsRouter);
Enter fullscreen mode Exit fullscreen mode
  1. Test Endpoints: Use Postman to test:
    • GET http://localhost:5000/api/posts: Retrieve all published posts.
    • GET http://localhost:5000/api/posts/welcome-to-aquascript: Retrieve a single post.
    • POST http://localhost:5000/api/posts: Create a post (requires admin authentication, implemented later).

5. Frontend Development (React, Tailwind CSS)

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

  1. Create a React App:
   npx create-react-app aquascript-blog-frontend
   cd aquascript-blog-frontend
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. Initialize Tailwind CSS:
   npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Update tailwind.config.js:

   module.exports = {
       content: [
           "./src/**/*.{js,jsx,ts,tsx}",
       ],
       theme: {
           extend: {},
       },
       plugins: [
           require('@tailwindcss/typography'),
       ],
   }
Enter fullscreen mode Exit fullscreen mode

Create src/index.css:

   @tailwind base;
   @tailwind components;
   @tailwind utilities;
Enter fullscreen mode Exit fullscreen mode
  1. 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')
   );
Enter fullscreen mode Exit fullscreen mode
  1. 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=http://localhost:5000
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Blogs Page

  1. 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;
Enter fullscreen mode Exit fullscreen mode
  1. 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;
Enter fullscreen mode Exit fullscreen mode

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

  1. Create an Auth0 Application:

    • Sign up at https://auth0.com.
    • 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.
  2. Create an API:

    • In Auth0, go to “APIs” and create a new API.
    • Set the identifier (e.g., https://aquascript.xyz/api).
    • Note the audience.
  3. 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);
     }
    
  4. Update Environment Variables:
    Add Auth0 credentials to .env files in both backend and frontend projects.

Step 2: Secure Admin Endpoints

  1. Install Auth0 Middleware: Ensure express-jwt and jwks-rsa are installed:
   npm install jwks-rsa
Enter fullscreen mode Exit fullscreen mode
  1. 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['https://aquascript.xyz/roles'] || [];
       if (!roles.includes('admin')) {
           return res.status(403).json({ error: 'Access denied' });
       }
       next();
   };

   module.exports = { checkJwt, checkAdmin };
Enter fullscreen mode Exit fullscreen mode
  1. 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) => { /* ... */ });
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

  1. Backend Testing:

    • Use Postman to test all API endpoints.
    • Verify that admin-only endpoints require a valid JWT token with the admin role.
  2. Frontend Testing:

    • Run the frontend: npm start.
    • Visit http://localhost:3000/blogs to see the blogs page.
    • Visit http://localhost:3000/admin to test the admin panel (requires login).
    • Test creating, editing, and deleting posts.
  3. Database Testing:

    • Use Neon’s SQL Editor to verify that posts are created, updated, and deleted correctly.

10. Deployment

Deploy the application to Vercel for easy hosting.

Step 1: Deploy Backend

  1. 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
    
  2. Deploy to Vercel:

    • Sign up at https://vercel.com.
    • Import the backend repository.
    • Add environment variables (DATABASE_URL, AUTH0_*) in Vercel’s dashboard.
    • Deploy the project. Note the URL (e.g., https://aquascript-blog-backend.vercel.app).

Step 2: Deploy Frontend

  1. Push to GitHub:

    • Create a separate GitHub repository for the frontend.
    • Push the code.
  2. 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.
  3. Update Auth0:

    • Add the Vercel frontend URL to Auth0’s “Allowed Callback URLs” and “Allowed Logout URLs”.
  4. Test Deployment:

    • Visit the deployed blogs page (e.g., https://aquascript-blog-frontend.vercel.app/blogs).
    • Test the admin panel and ensure authentication works.

11. Additional Considerations and Best Practices

  • 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.

12. Artifacts (Code Samples)

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}`);
});
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

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!

Comments 0 total

    Add comment