• Что бы вступить в ряды "Принятый кодер" Вам нужно:
    Написать 10 полезных сообщений или тем и Получить 10 симпатий.
    Для того кто не хочет терять время,может пожертвовать средства для поддержки сервеса, и вступить в ряды VIP на месяц, дополнительная информация в лс.

  • Пользаватели которые будут спамить, уходят в бан без предупреждения. Спам сообщения определяется администрацией и модератором.

  • Гость, Что бы Вы хотели увидеть на нашем Форуме? Изложить свои идеи и пожелания по улучшению форума Вы можете поделиться с нами здесь. ----> Перейдите сюда
  • Все пользователи не прошедшие проверку электронной почты будут заблокированы. Все вопросы с разблокировкой обращайтесь по адресу электронной почте : info@guardianelinks.com . Не пришло сообщение о проверке или о сбросе также сообщите нам.

ABC

Lomanu4 Оффлайн

Lomanu4

Команда форума
Администратор
Регистрация
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

  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:


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

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

    Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

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

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}`);
});
  1. Update package.json: Add a start script:

"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
  1. 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.

  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;
  1. Integrate Routes: Update index.js to include the posts routes:

const postsRouter = require('./routes/posts');
app.use('/api/posts', postsRouter);
  1. Test Endpoints: Use Postman to test:
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
  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
  1. 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;
  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')
);
  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=

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.


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

      Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

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

  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
  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['

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

'] || [];
if (!roles.includes('admin')) {
return res.status(403).json({ error: 'Access denied' });
}
next();
};

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


  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:

  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:
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:
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}`);
});

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!


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

 
Вверх Снизу