Server code
This commit is contained in:
27
server/src/routes/auth.js
Normal file
27
server/src/routes/auth.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import { db } from '../db.js';
|
||||
|
||||
export default async function auth(fastify, options) {
|
||||
fastify.post('/api/auth/login', async (request, reply) => {
|
||||
const { username, password } = request.body;
|
||||
|
||||
const user = await db.oneOrNone(
|
||||
'SELECT * FROM users WHERE username = $1',
|
||||
[username]
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
reply.code(401).send({ error: 'Invalid credentials' });
|
||||
return;
|
||||
}
|
||||
|
||||
const valid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!valid) {
|
||||
reply.code(401).send({ error: 'Invalid credentials' });
|
||||
return;
|
||||
}
|
||||
|
||||
const token = fastify.jwt.sign({ id: user.id });
|
||||
reply.send({ token });
|
||||
});
|
||||
}
|
||||
33
server/src/routes/images.js
Normal file
33
server/src/routes/images.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createWriteStream } from 'fs';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { join } from 'path';
|
||||
import { db } from '../db.js';
|
||||
|
||||
export default async function images(fastify, options) {
|
||||
fastify.addHook('preHandler', async (request, reply) => {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
} catch (err) {
|
||||
reply.send(err);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post('/api/images/:postId', async (request, reply) => {
|
||||
const { postId } = request.params;
|
||||
const data = await request.file();
|
||||
|
||||
const filename = `${Date.now()}-${data.filename}`;
|
||||
const path = join(__dirname, '../../uploads', filename);
|
||||
|
||||
await pipeline(data.file, createWriteStream(path));
|
||||
|
||||
const image = await db.one(
|
||||
`INSERT INTO images (filename, url, post_id)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING *`,
|
||||
[filename, `/uploads/${filename}`, postId]
|
||||
);
|
||||
|
||||
reply.send(image);
|
||||
});
|
||||
}
|
||||
124
server/src/routes/posts.js
Normal file
124
server/src/routes/posts.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import { marked } from 'marked';
|
||||
import slugify from 'slugify';
|
||||
import { db } from '../db.js';
|
||||
|
||||
export default async function posts(fastify, options) {
|
||||
fastify.addHook('preHandler', async (request, reply) => {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
} catch (err) {
|
||||
reply.send(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Create post
|
||||
fastify.post('/api/posts', async (request, reply) => {
|
||||
const { title, content, status, publishedAt, tags } = request.body;
|
||||
const slug = slugify(title, { lower: true });
|
||||
const compiledContent = marked(content);
|
||||
|
||||
const post = await db.tx(async t => {
|
||||
const post = await t.one(
|
||||
`INSERT INTO posts
|
||||
(title, slug, markdown_content, compiled_content, status, published_at, author_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *`,
|
||||
[title, slug, content, compiledContent, status, publishedAt, request.user.id]
|
||||
);
|
||||
|
||||
if (tags && tags.length) {
|
||||
for (const tagName of tags) {
|
||||
const tag = await t.oneOrNone(
|
||||
'SELECT * FROM tags WHERE name = $1',
|
||||
[tagName]
|
||||
);
|
||||
|
||||
const tagId = tag ? tag.id : (
|
||||
await t.one(
|
||||
'INSERT INTO tags (name, slug) VALUES ($1, $2) RETURNING id',
|
||||
[tagName, slugify(tagName, { lower: true })]
|
||||
)
|
||||
).id;
|
||||
|
||||
await t.none(
|
||||
'INSERT INTO posts_tags (post_id, tag_id) VALUES ($1, $2)',
|
||||
[post.id, tagId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return post;
|
||||
});
|
||||
|
||||
reply.send(post);
|
||||
});
|
||||
|
||||
// Get all posts
|
||||
fastify.get('/api/posts', async (request, reply) => {
|
||||
const posts = await db.any(`
|
||||
SELECT p.*,
|
||||
array_agg(DISTINCT t.name) as tags,
|
||||
array_agg(DISTINCT i.url) as images
|
||||
FROM posts p
|
||||
LEFT JOIN posts_tags pt ON p.id = pt.post_id
|
||||
LEFT JOIN tags t ON pt.tag_id = t.id
|
||||
LEFT JOIN images i ON p.id = i.post_id
|
||||
WHERE p.author_id = $1
|
||||
GROUP BY p.id
|
||||
ORDER BY p.created_at DESC
|
||||
`, [request.user.id]);
|
||||
|
||||
reply.send(posts);
|
||||
});
|
||||
|
||||
// Update post
|
||||
fastify.put('/api/posts/:id', async (request, reply) => {
|
||||
const { id } = request.params;
|
||||
const { title, content, status, publishedAt, tags } = request.body;
|
||||
const compiledContent = marked(content);
|
||||
|
||||
const post = await db.tx(async t => {
|
||||
const post = await t.one(
|
||||
`UPDATE posts
|
||||
SET title = $1,
|
||||
markdown_content = $2,
|
||||
compiled_content = $3,
|
||||
status = $4,
|
||||
published_at = $5,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $6 AND author_id = $7
|
||||
RETURNING *`,
|
||||
[title, content, compiledContent, status, publishedAt, id, request.user.id]
|
||||
);
|
||||
|
||||
// Remove existing tags
|
||||
await t.none('DELETE FROM posts_tags WHERE post_id = $1', [id]);
|
||||
|
||||
// Add new tags
|
||||
if (tags && tags.length) {
|
||||
for (const tagName of tags) {
|
||||
const tag = await t.oneOrNone(
|
||||
'SELECT * FROM tags WHERE name = $1',
|
||||
[tagName]
|
||||
);
|
||||
|
||||
const tagId = tag ? tag.id : (
|
||||
await t.one(
|
||||
'INSERT INTO tags (name, slug) VALUES ($1, $2) RETURNING id',
|
||||
[tagName, slugify(tagName, { lower: true })]
|
||||
)
|
||||
).id;
|
||||
|
||||
await t.none(
|
||||
'INSERT INTO posts_tags (post_id, tag_id) VALUES ($1, $2)',
|
||||
[id, tagId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return post;
|
||||
});
|
||||
|
||||
reply.send(post);
|
||||
});
|
||||
}
|
||||
40
server/src/server.js
Normal file
40
server/src/server.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import Fastify from 'fastify';
|
||||
import cors from '@fastify/cors';
|
||||
import jwt from '@fastify/jwt';
|
||||
import multipart from '@fastify/multipart';
|
||||
import static from '@fastify/static';
|
||||
import { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const app = Fastify({ logger: true });
|
||||
|
||||
app.register(cors, {
|
||||
origin: process.env.FRONTEND_URL || 'http://localhost:3000'
|
||||
});
|
||||
|
||||
app.register(jwt, {
|
||||
secret: process.env.JWT_SECRET
|
||||
});
|
||||
|
||||
app.register(multipart);
|
||||
|
||||
app.register(static, {
|
||||
root: join(fileURLToPath(import.meta.url), '../../uploads'),
|
||||
prefix: '/uploads/'
|
||||
});
|
||||
|
||||
app.register(import('./routes/auth.js'));
|
||||
app.register(import('./routes/posts.js'));
|
||||
app.register(import('./routes/tags.js'));
|
||||
app.register(import('./routes/images.js'));
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
await app.listen({ port: 3001 });
|
||||
} catch (err) {
|
||||
app.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
start();
|
||||
Reference in New Issue
Block a user