最后活跃于 1757456555

MisterFixx 修订了这个 Gist 1757456554. 转到此修订

1 file changed, 645 insertions

gpt-oss:120b.md(文件已创建)

@@ -0,0 +1,645 @@
1 +
2 + Below is a **complete, runnable example** of a small CRUD REST API built with
3 +
4 + | Technology | What it does |
5 + |------------|--------------|
6 + | **Node.js + Express** | HTTP server & routing |
7 + | **MongoDB + Mongoose** | Data persistence & schema definition |
8 + | **JWT (json‑webtoken)** | Stateless authentication |
9 + | **bcrypt** | Password hashing |
10 + | **dotenv** | Environment variables |
11 +
12 + The API manages a simple resource called **`Post`** (title + body) and provides **user registration / login**. All `Post` routes are protected – a valid JWT must be sent in the `Authorization: Bearer <token>` header.
13 +
14 + > **⚠️ This is a learning‑oriented example.**
15 + > In production you’d add more validation, rate‑limiting, HTTPS, proper error handling, refresh‑tokens, etc.
16 +
17 + ---
18 +
19 + ## 1. Project Structure
20 +
21 + ```
22 + my-crud-api/
23 +
24 + ├─ .env # environment variables (not committed)
25 + ├─ package.json
26 + ├─ README.md
27 +
28 + ├─ src/
29 + │ ├─ index.js # entry point (Express app)
30 + │ ├─ config/
31 + │ │ └─ db.js # MongoDB connection
32 + │ ├─ middleware/
33 + │ │ └─ auth.js # JWT verification middleware
34 + │ ├─ models/
35 + │ │ ├─ User.js
36 + │ │ └─ Post.js
37 + │ ├─ routes/
38 + │ │ ├─ auth.js # /api/auth (register, login)
39 + │ │ └─ posts.js # /api/posts (CRUD)
40 + │ └─ utils/
41 + │ └─ generateToken.js
42 +
43 + └─ .gitignore
44 + ```
45 +
46 + ---
47 +
48 + ## 2. Install the dependencies
49 +
50 + ```bash
51 + # create folder & cd into it
52 + mkdir my-crud-api && cd my-crud-api
53 +
54 + # initialise npm
55 + npm init -y
56 +
57 + # install runtime deps
58 + npm i express mongoose dotenv jsonwebtoken bcryptjs
59 +
60 + # install dev deps (optional but helpful)
61 + npm i -D nodemon
62 + ```
63 +
64 + Add the following `scripts` entry to **package.json** so you can run the server with `npm run dev`:
65 +
66 + ```json
67 + "scripts": {
68 + "start": "node src/index.js",
69 + "dev": "nodemon src/index.js"
70 + }
71 + ```
72 +
73 + ---
74 +
75 + ## 3. Environment variables (`.env`)
76 +
77 + Create a file named **`.env`** in the project root:
78 +
79 + ```dotenv
80 + PORT=5000
81 + MONGO_URI=mongodb://localhost:27017/crud-demo
82 + JWT_SECRET=superSecretKeyChangeMe
83 + JWT_EXPIRES_IN=1d # 1 day, you can also use "7d", "12h", etc.
84 + ```
85 +
86 + > **Never commit `.env`** – add it to `.gitignore`.
87 +
88 + ---
89 +
90 + ## 4. Code
91 +
92 + ### 4.1 `src/config/db.js` – MongoDB connection
93 +
94 + ```js
95 + // src/config/db.js
96 + const mongoose = require('mongoose');
97 +
98 + const connectDB = async () => {
99 + try {
100 + const conn = await mongoose.connect(process.env.MONGO_URI, {
101 + // options are optional with Mongoose 6+
102 + });
103 + console.log(`MongoDB connected: ${conn.connection.host}`);
104 + } catch (err) {
105 + console.error('MongoDB connection error:', err);
106 + process.exit(1);
107 + }
108 + };
109 +
110 + module.exports = connectDB;
111 + ```
112 +
113 + ### 4.2 `src/models/User.js` – User schema
114 +
115 + ```js
116 + // src/models/User.js
117 + const mongoose = require('mongoose');
118 + const bcrypt = require('bcryptjs');
119 +
120 + const userSchema = new mongoose.Schema(
121 + {
122 + name: {
123 + type: String,
124 + required: [true, 'Please add a name'],
125 + },
126 + email: {
127 + type: String,
128 + required: [true, 'Please add an email'],
129 + unique: true,
130 + lowercase: true,
131 + match: [
132 + /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
133 + 'Please add a valid email',
134 + ],
135 + },
136 + password: {
137 + type: String,
138 + required: [true, 'Please add a password'],
139 + minlength: 6,
140 + select: false, // do NOT return password by default
141 + },
142 + },
143 + { timestamps: true }
144 + );
145 +
146 + // Hash password before saving
147 + userSchema.pre('save', async function (next) {
148 + // only hash if the password field is modified (or new)
149 + if (!this.isModified('password')) {
150 + return next();
151 + }
152 + const salt = await bcrypt.genSalt(10);
153 + this.password = await bcrypt.hash(this.password, salt);
154 + next();
155 + });
156 +
157 + // Method to compare entered password with hashed password
158 + userSchema.methods.matchPassword = async function (enteredPwd) {
159 + return await bcrypt.compare(enteredPwd, this.password);
160 + };
161 +
162 + module.exports = mongoose.model('User', userSchema);
163 + ```
164 +
165 + ### 4.3 `src/models/Post.js` – Post schema (the CRUD resource)
166 +
167 + ```js
168 + // src/models/Post.js
169 + const mongoose = require('mongoose');
170 +
171 + const postSchema = new mongoose.Schema(
172 + {
173 + title: {
174 + type: String,
175 + required: [true, 'Post title required'],
176 + },
177 + body: {
178 + type: String,
179 + required: [true, 'Post body required'],
180 + },
181 + author: {
182 + type: mongoose.Schema.Types.ObjectId,
183 + ref: 'User', // optional – you can link a post to its creator
184 + required: true,
185 + },
186 + },
187 + { timestamps: true }
188 + );
189 +
190 + module.exports = mongoose.model('Post', postSchema);
191 + ```
192 +
193 + ### 4.4 `src/utils/generateToken.js` – JWT helper
194 +
195 + ```js
196 + // src/utils/generateToken.js
197 + const jwt = require('jsonwebtoken');
198 +
199 + const generateToken = (payload) => {
200 + return jwt.sign(payload, process.env.JWT_SECRET, {
201 + expiresIn: process.env.JWT_EXPIRES_IN || '1d',
202 + });
203 + };
204 +
205 + module.exports = generateToken;
206 + ```
207 +
208 + ### 4.5 `src/middleware/auth.js` – Protect routes
209 +
210 + ```js
211 + // src/middleware/auth.js
212 + const jwt = require('jsonwebtoken');
213 + const User = require('../models/User');
214 +
215 + const protect = async (req, res, next) => {
216 + let token;
217 +
218 + // Expect header: Authorization: Bearer <token>
219 + if (
220 + req.headers.authorization &&
221 + req.headers.authorization.startsWith('Bearer')
222 + ) {
223 + token = req.headers.authorization.split(' ')[1];
224 + }
225 +
226 + if (!token) {
227 + return res.status(401).json({ message: 'Not authorized, token missing' });
228 + }
229 +
230 + try {
231 + const decoded = jwt.verify(token, process.env.JWT_SECRET);
232 + // Attach user (without password) to request
233 + req.user = await User.findById(decoded.id).select('-password');
234 + if (!req.user) {
235 + return res.status(401).json({ message: 'User not found' });
236 + }
237 + next();
238 + } catch (err) {
239 + console.error(err);
240 + return res.status(401).json({ message: 'Not authorized, token invalid' });
241 + }
242 + };
243 +
244 + module.exports = protect;
245 + ```
246 +
247 + ### 4.6 `src/routes/auth.js` – Register & Login
248 +
249 + ```js
250 + // src/routes/auth.js
251 + const express = require('express');
252 + const router = express.Router();
253 + const User = require('../models/User');
254 + const generateToken = require('../utils/generateToken');
255 +
256 + // @route POST /api/auth/register
257 + // @desc Register new user & return JWT
258 + // @access Public
259 + router.post('/register', async (req, res) => {
260 + const { name, email, password } = req.body;
261 +
262 + // Simple validation
263 + if (!name || !email || !password) {
264 + return res.status(400).json({ message: 'Please provide all fields' });
265 + }
266 +
267 + try {
268 + // Check if user exists
269 + const existing = await User.findOne({ email });
270 + if (existing) {
271 + return res.status(400).json({ message: 'User already exists' });
272 + }
273 +
274 + const user = await User.create({ name, email, password });
275 + const token = generateToken({ id: user._id });
276 +
277 + res.status(201).json({
278 + token,
279 + user: { id: user._id, name: user.name, email: user.email },
280 + });
281 + } catch (err) {
282 + console.error(err);
283 + res.status(500).json({ message: 'Server error' });
284 + }
285 + });
286 +
287 + // @route POST /api/auth/login
288 + // @desc Authenticate user & return JWT
289 + // @access Public
290 + router.post('/login', async (req, res) => {
291 + const { email, password } = req.body;
292 +
293 + // Validation
294 + if (!email || !password) {
295 + return res.status(400).json({ message: 'Please provide email & password' });
296 + }
297 +
298 + try {
299 + // Include password field (select: false in schema)
300 + const user = await User.findOne({ email }).select('+password');
301 + if (!user) {
302 + return res.status(400).json({ message: 'Invalid credentials' });
303 + }
304 +
305 + const isMatch = await user.matchPassword(password);
306 + if (!isMatch) {
307 + return res.status(400).json({ message: 'Invalid credentials' });
308 + }
309 +
310 + const token = generateToken({ id: user._id });
311 +
312 + res.json({
313 + token,
314 + user: { id: user._id, name: user.name, email: user.email },
315 + });
316 + } catch (err) {
317 + console.error(err);
318 + res.status(500).json({ message: 'Server error' });
319 + }
320 + });
321 +
322 + module.exports = router;
323 + ```
324 +
325 + ### 4.7 `src/routes/posts.js` – CRUD endpoints (protected)
326 +
327 + ```js
328 + // src/routes/posts.js
329 + const express = require('express');
330 + const router = express.Router();
331 + const Post = require('../models/Post');
332 + const protect = require('../middleware/auth');
333 +
334 + // ---------- CREATE ----------
335 + /**
336 + * @route POST /api/posts
337 + * @desc Create a new post (auth required)
338 + * @access Private
339 + */
340 + router.post('/', protect, async (req, res) => {
341 + const { title, body } = req.body;
342 +
343 + if (!title || !body) {
344 + return res.status(400).json({ message: 'Title and body are required' });
345 + }
346 +
347 + try {
348 + const post = await Post.create({
349 + title,
350 + body,
351 + author: req.user._id,
352 + });
353 + res.status(201).json(post);
354 + } catch (err) {
355 + console.error(err);
356 + res.status(500).json({ message: 'Server error' });
357 + }
358 + });
359 +
360 + // ---------- READ ALL ----------
361 + /**
362 + * @route GET /api/posts
363 + * @desc Get all posts (public or private, up to you)
364 + * @access Public
365 + */
366 + router.get('/', async (req, res) => {
367 + try {
368 + // Populate author name/email (optional)
369 + const posts = await Post.find()
370 + .populate('author', 'name email')
371 + .sort({ createdAt: -1 });
372 + res.json(posts);
373 + } catch (err) {
374 + console.error(err);
375 + res.status(500).json({ message: 'Server error' });
376 + }
377 + });
378 +
379 + // ---------- READ ONE ----------
380 + /**
381 + * @route GET /api/posts/:id
382 + * @desc Get a single post by ID
383 + * @access Public
384 + */
385 + router.get('/:id', async (req, res) => {
386 + try {
387 + const post = await Post.findById(req.params.id).populate(
388 + 'author',
389 + 'name email'
390 + );
391 + if (!post) {
392 + return res.status(404).json({ message: 'Post not found' });
393 + }
394 + res.json(post);
395 + } catch (err) {
396 + console.error(err);
397 + // CastError => invalid ObjectId format
398 + if (err.name === 'CastError')
399 + return res.status(400).json({ message: 'Invalid post ID' });
400 + res.status(500).json({ message: 'Server error' });
401 + }
402 + });
403 +
404 + // ---------- UPDATE ----------
405 + /**
406 + * @route PUT /api/posts/:id
407 + * @desc Update a post (only author can edit)
408 + * @access Private
409 + */
410 + router.put('/:id', protect, async (req, res) => {
411 + const { title, body } = req.body;
412 +
413 + try {
414 + const post = await Post.findById(req.params.id);
415 + if (!post) return res.status(404).json({ message: 'Post not found' });
416 +
417 + // Only the author can modify
418 + if (post.author.toString() !== req.user._id.toString()) {
419 + return res
420 + .status(403)
421 + .json({ message: 'You are not authorized to edit this post' });
422 + }
423 +
424 + // Apply updates
425 + if (title) post.title = title;
426 + if (body) post.body = body;
427 +
428 + const updated = await post.save();
429 + res.json(updated);
430 + } catch (err) {
431 + console.error(err);
432 + if (err.name === 'CastError')
433 + return res.status(400).json({ message: 'Invalid post ID' });
434 + res.status(500).json({ message: 'Server error' });
435 + }
436 + });
437 +
438 + // ---------- DELETE ----------
439 + /**
440 + * @route DELETE /api/posts/:id
441 + * @desc Delete a post (only author can delete)
442 + * @access Private
443 + */
444 + router.delete('/:id', protect, async (req, res) => {
445 + try {
446 + const post = await Post.findById(req.params.id);
447 + if (!post) return res.status(404).json({ message: 'Post not found' });
448 +
449 + // Only the author can delete
450 + if (post.author.toString() !== req.user._id.toString()) {
451 + return res
452 + .status(403)
453 + .json({ message: 'You are not authorized to delete this post' });
454 + }
455 +
456 + await post.remove();
457 + res.json({ message: 'Post removed' });
458 + } catch (err) {
459 + console.error(err);
460 + if (err.name === 'CastError')
461 + return res.status(400).json({ message: 'Invalid post ID' });
462 + res.status(500).json({ message: 'Server error' });
463 + }
464 + });
465 +
466 + module.exports = router;
467 + ```
468 +
469 + ### 4.8 `src/index.js` – Entry point
470 +
471 + ```js
472 + // src/index.js
473 + require('dotenv').config(); // load .env first
474 + const express = require('express');
475 + const morgan = require('morgan');
476 + const connectDB = require('./config/db');
477 +
478 + const app = express();
479 +
480 + // Connect to DB
481 + connectDB();
482 +
483 + // Middleware
484 + app.use(express.json()); // parse JSON bodies
485 + app.use(morgan('dev')); // logging (optional)
486 +
487 + // Routes
488 + app.use('/api/auth', require('./routes/auth'));
489 + app.use('/api/posts', require('./routes/posts'));
490 +
491 + // 404 handler
492 + app.use((req, res) => {
493 + res.status(404).json({ message: 'Endpoint not found' });
494 + });
495 +
496 + // Global error handler (optional but nice)
497 + app.use((err, req, res, next) => {
498 + console.error('Unhandled error:', err);
499 + res.status(500).json({ message: 'Something went wrong' });
500 + });
501 +
502 + // Start server
503 + const PORT = process.env.PORT || 5000;
504 + app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
505 + ```
506 +
507 + ### 4.9 `.gitignore`
508 +
509 + ```gitignore
510 + node_modules/
511 + .env
512 + ```
513 +
514 + ### 4.10 `README.md` – How to run
515 +
516 + ```markdown
517 + # CRUD REST API with Express, MongoDB & JWT
518 +
519 + A minimal example that shows:
520 +
521 + * User registration & login (passwords hashed with bcrypt)
522 + * JWT generation & verification
523 + * Protected CRUD endpoints (`/api/posts`) that only the authenticated user can create / edit / delete
524 +
525 + ## Prerequisites
526 +
527 + * **Node.js** (>= 14)
528 + * **MongoDB** (local or Atlas). The example uses a local MongoDB instance at `mongodb://localhost:27017/crud-demo`.
529 +
530 + ## Setup
531 +
532 + ```bash
533 + # Clone (or copy) the repo
534 + git clone <repo-url>
535 + cd my-crud-api
536 +
537 + # Install deps
538 + npm install
539 +
540 + # Create a .env file (see .env.example)
541 + cp .env.example .env
542 + # Edit .env if needed
543 +
544 + # Run the server (development)
545 + npm run dev
546 + ```
547 +
548 + Server will start on **http://localhost:5000**.
549 +
550 + ## API Overview
551 +
552 + | Method | Endpoint | Description | Auth |
553 + |--------|------------------------|---------------------------------|------|
554 + | POST | `/api/auth/register` | Register a new user | ❌ |
555 + | POST | `/api/auth/login` | Login & receive JWT | ❌ |
556 + | POST | `/api/posts` | Create a post (title, body) | ✅ |
557 + | GET | `/api/posts` | List all posts | ❌ |
558 + | GET | `/api/posts/:id` | Get a single post | ❌ |
559 + | PUT | `/api/posts/:id` | Update post (author only) | ✅ |
560 + | DELETE | `/api/posts/:id` | Delete post (author only) | ✅ |
561 +
562 + **Authenticated requests** must include the header:
563 +
564 + ```
565 + Authorization: Bearer <jwt-token>
566 + ```
567 +
568 + ### Example using `curl`
569 +
570 + ```bash
571 + # 1️⃣ Register
572 + curl -X POST http://localhost:5000/api/auth/register \
573 + -H "Content-Type: application/json" \
574 + -d '{"name":"Alice","email":"[email protected]","password":"secret123"}'
575 +
576 + # 2️⃣ Login (returns token)
577 + TOKEN=$(curl -s -X POST http://localhost:5000/api/auth/login \
578 + -H "Content-Type: application/json" \
579 + -d '{"email":"[email protected]","password":"secret123"}' | jq -r .token)
580 +
581 + # 3️⃣ Create a post (protected)
582 + curl -X POST http://localhost:5000/api/posts \
583 + -H "Content-Type: application/json" \
584 + -H "Authorization: Bearer $TOKEN" \
585 + -d '{"title":"My first post","body":"Hello world!"}'
586 + ```
587 +
588 + ## Extending the Project
589 +
590 + * Add request validation with **express-validator** or **Joi**.
591 + * Use **Refresh tokens** for longer sessions.
592 + * Add pagination, filtering, and search on `/api/posts`.
593 + * Deploy to **Heroku**, **Render**, **Vercel (serverless)**, etc.
594 +
595 + Feel free to fork and build on top of this skeleton.
596 + ```
597 +
598 + ---
599 +
600 + ## Testing the Flow
601 +
602 + 1. **Register a user** → DB now contains a user with a hashed password.
603 + 2. **Login** → Server checks password, creates a JWT (`payload.id = userId`).
604 + 3. **Create a post** → Middleware `protect` reads `Authorization` header, verifies token, attaches `req.user`.
605 + 4. **Read posts** → No auth needed; returns list with author details populated.
606 + 5. **Update/Delete** → The middleware ensures the request is authenticated, then the route checks that `post.author` matches `req.user._id`.
607 +
608 + If you send an invalid or expired token, the request will be rejected with a `401 Unauthorized` response.
609 +
610 + ---
611 +
612 + That's the full, ready‑to‑run code for a CRUD API with JWT authentication using Express and MongoDB. Happy coding! 🎉
613 + ```
614 +
615 + ---
616 +
617 + ## Quick Recap of How It Works
618 +
619 + 1. **User registers** → password stored as a bcrypt hash.
620 + 2. **User logs in** → bcrypt compares the supplied password with stored hash.
621 + 3. **Server creates a JWT** (`payload = { id: userId }`) signed with a secret.
622 + 4. **Client stores the token** (e.g., in localStorage, cookies, or in memory).
623 + 5. **For each protected route**, the `protect` middleware extracts the token, verifies it (`jwt.verify`), and attaches the user document to `req.user`.
624 + 6. **CRUD logic** can now safely rely on `req.user` to enforce ownership rules.
625 +
626 + ---
627 +
628 + Enjoy building your own full‑stack apps on top of this foundation! 🚀
629 + ```
630 +
631 + ---
632 +
633 + ## Optional Enhancements (not required for the base assignment)
634 +
635 + 1. **Refresh Tokens** – store a long‑lived refresh token in DB and rotate.
636 + 2. **Rate Limiting** – protect `/api/auth` from brute‑force.
637 + 3. **Schema Validation** – use `express-validator` or `Joi` for request bodies.
638 + 4. **Testing** – write Jest/Supertest integration tests.
639 +
640 + Feel free to ask if you'd like sample code for any of those.
641 + ```
642 +
643 + ---
644 +
645 + This completes the full, functional CRUD API with authentication via JWT. Copy the files into a new folder, run `npm install`, create `.env`, and start the server. You now have a solid base to expand into a larger project!
上一页 下一页