feat: Implement blog and blog post models, routes, and services

- Added Blog and BlogPost models with necessary fields and relationships.
- Created blogRouter for handling blog-related API endpoints including CRUD operations.
- Developed BlogService for business logic related to blogs and posts, including sharing functionality.
- Implemented API client methods for frontend to interact with blog-related endpoints.
- Added internationalization support for blog-related text in English and German.
- Created Vue components for blog editing, listing, and viewing, including a rich text editor for post content.
- Enhanced user experience with form validations and dynamic visibility settings based on user input.
This commit is contained in:
Torsten Schulz (local)
2025-08-18 13:41:37 +02:00
parent 19ee6ba0a1
commit 53c748a074
27 changed files with 1342 additions and 19 deletions

View File

@@ -0,0 +1,174 @@
import BlogService from '../services/blogService.js';
import SocialNetworkService from '../services/socialnetworkService.js';
const blogService = new BlogService();
const socialService = new SocialNetworkService();
export const createBlog = async (req, res) => {
try {
const { userid } = req.headers; // creator must be logged in
const blog = await blogService.createBlog(userid, req.body || {});
res.json(blog);
} catch (e) {
res.status(e.status || 400).json({ error: e.message });
}
};
export const updateBlog = async (req, res) => {
try {
const { userid } = req.headers;
const { id } = req.params;
const blog = await blogService.updateBlog(userid, id, req.body || {});
res.json(blog);
} catch (e) {
res.status(e.status || 400).json({ error: e.message });
}
};
export const listBlogs = async (req, res) => {
try {
const { userid } = req.headers; // optional for access checks
const blogs = await blogService.listBlogsForViewer(userid || null);
res.json(blogs);
} catch (e) {
res.status(400).json({ error: e.message });
}
};
export const getBlog = async (req, res) => {
try {
const { userid } = req.headers;
const { id } = req.params;
const blog = await blogService.getBlog(id, userid || null);
res.json(blog);
} catch (e) {
res.status(e.status || 404).json({ error: e.message });
}
};
export const createPost = async (req, res) => {
try {
const { userid } = req.headers;
const { id } = req.params;
const post = await blogService.createPost(userid, id, req.body || {});
res.json(post);
} catch (e) {
res.status(e.status || 400).json({ error: e.message });
}
};
export const listPosts = async (req, res) => {
try {
const { userid } = req.headers;
const { id } = req.params;
const page = Math.max(1, parseInt(req.query.page || '1', 10));
const pageSize = Math.min(50, Math.max(1, parseInt(req.query.pageSize || '10', 10)));
const all = await blogService.listPosts(id, userid || null);
const total = all.length;
const start = (page - 1) * pageSize;
const posts = all.slice(start, start + pageSize);
res.json({ items: posts, page, pageSize, total });
} catch (e) {
res.status(e.status || 400).json({ error: e.message });
}
};
export const listBlogImages = async (req, res) => {
try {
const { userid } = req.headers;
const user = await blogService.getUserByHashedId(userid);
const blogFolder = await blogService.ensureBlogFolder(user.id);
const images = await socialService.getFolderImageList(userid, blogFolder.id);
res.json({ folderId: blogFolder.id, images });
} catch (e) {
res.status(400).json({ error: e.message });
}
};
export const uploadBlogImage = async (req, res) => {
try {
const { userid } = req.headers;
const user = await blogService.getUserByHashedId(userid);
const blogFolder = await blogService.ensureBlogFolder(user.id);
// Default visibility mirrors blog folder visibilities (already applied)
const formData = {
title: req.body?.title || 'Blog Image',
description: req.body?.description || '',
folderId: blogFolder.id,
visibility: JSON.stringify([])
};
const file = req.file;
const image = await socialService.uploadImage(userid, file, formData);
res.status(201).json(image);
} catch (e) {
res.status(400).json({ error: e.message });
}
};
export const getPublicBlogImageByHash = async (req, res) => {
try {
const { hash } = req.params;
const filePath = await socialService.getImageFilePathPublicByHash(hash);
res.sendFile(filePath, err => {
if (err) {
console.error('Error sending file:', err);
res.status(500).json({ error: 'Error sending file' });
}
});
} catch (e) {
res.status(e.status || 403).json({ error: e.message || 'Access denied or image not found' });
}
};
export const shareBlog = async (req, res) => {
try {
const { userid } = req.headers;
const { id } = req.params;
const { toFriends = false, emails = [] } = req.body || {};
// Prefer configured frontend base for public links; fallback to request origin
const feBase = process.env.FRONTEND_BASE_URL;
const proto = (req.headers['x-forwarded-proto'] || req.protocol || 'http').split(',')[0].trim();
const host = (req.headers['x-forwarded-host'] || req.get('host'));
const origin = (feBase && feBase.trim()) ? feBase.replace(/\/$/, '') : `${proto}://${host}`;
// Compute slug from username + title (no spaces, remove unsafe chars)
const { user, blog } = await blogService.assertOwner(userid, id);
const titleNoSpaces = (blog.title || '').toString().replace(/\s+/g, '');
const base = `${user.username || 'user'}${titleNoSpaces}`;
const slug = base.replace(/[^a-zA-Z0-9_-]/g, '');
// Pretty public URL under frontend: /blogs/:slug
const url = `${origin}/blogs/${encodeURIComponent(slug)}`;
const result = await blogService.shareBlog(userid, id, { toFriends, emails }, url);
res.json(result);
} catch (e) {
res.status(e.status || 400).json({ error: e.message });
}
};
export const resolveBlogSlug = async (req, res) => {
try {
const { slug } = req.params;
const blogId = await blogService.resolveSlug(slug);
if (!blogId) return res.status(404).json({ error: 'Not found' });
// Redirect to SPA history route so the client router loads the blog
const feBase = process.env.FRONTEND_BASE_URL;
const proto = (req.headers['x-forwarded-proto'] || req.protocol || 'http').split(',')[0].trim();
const host = (req.headers['x-forwarded-host'] || req.get('host'));
const origin = (feBase && feBase.trim()) ? feBase.replace(/\/$/, '') : `${proto}://${host}`;
return res.redirect(302, `${origin}/blogs/${blogId}/${encodeURIComponent(slug)}`);
} catch (e) {
res.status(400).json({ error: e.message });
}
};
export const resolveBlogSlugId = async (req, res) => {
try {
const { slug } = req.params;
const blogId = await blogService.resolveSlug(slug);
if (!blogId) return res.status(404).json({ error: 'Not found' });
res.json({ id: blogId });
} catch (e) {
res.status(400).json({ error: e.message });
}
};