Initial commit
This commit is contained in:
commit
92e96b9107
85 changed files with 24969 additions and 0 deletions
31
app/server/api/articles/[slug].delete.ts
Normal file
31
app/server/api/articles/[slug].delete.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { Article } from "../../models/Article";
|
||||
import { requireRole } from "../../utils/auth";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const slug = getRouterParam(event, "slug");
|
||||
|
||||
// Only admins can delete articles
|
||||
const user = await requireRole(event, ["admin"]);
|
||||
|
||||
if (!slug) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Slug is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Find and delete article
|
||||
const article = await Article.findOneAndDelete({ slug });
|
||||
|
||||
if (!article) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Article not found",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Article deleted successfully",
|
||||
};
|
||||
});
|
||||
71
app/server/api/articles/[slug].get.ts
Normal file
71
app/server/api/articles/[slug].get.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { Article } from '../../models/Article'
|
||||
import { checkAccessLevel, verifyAuth } from '../../utils/auth'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
|
||||
if (!slug) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Slug is required'
|
||||
})
|
||||
}
|
||||
|
||||
// Find article
|
||||
const article = await Article.findOne({ slug })
|
||||
.populate('author', 'username displayName avatar')
|
||||
.populate('contributors', 'username displayName avatar')
|
||||
|
||||
if (!article) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Article not found'
|
||||
})
|
||||
}
|
||||
|
||||
// Check access
|
||||
const hasAccess = await checkAccessLevel(
|
||||
event,
|
||||
article.accessLevel,
|
||||
article.cohorts
|
||||
)
|
||||
|
||||
if (!hasAccess) {
|
||||
// For protected content, return limited preview
|
||||
if (article.accessLevel !== 'admin') {
|
||||
return {
|
||||
slug: article.slug,
|
||||
title: article.title,
|
||||
description: article.description.substring(0, 200) + '...',
|
||||
category: article.category,
|
||||
tags: article.tags,
|
||||
accessLevel: article.accessLevel,
|
||||
restricted: true,
|
||||
message: 'This content requires authentication'
|
||||
}
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Access denied'
|
||||
})
|
||||
}
|
||||
|
||||
// Increment view count
|
||||
article.views += 1
|
||||
await article.save()
|
||||
|
||||
// Get user to check if they've liked
|
||||
const user = await verifyAuth(event)
|
||||
const userLiked = false // TODO: Implement likes tracking
|
||||
|
||||
return {
|
||||
...article.toObject(),
|
||||
userLiked,
|
||||
revisionCount: article.revisions.length,
|
||||
commentCount: article.comments.length,
|
||||
// Don't send full revisions and comments in main response
|
||||
revisions: undefined,
|
||||
comments: undefined
|
||||
}
|
||||
})
|
||||
109
app/server/api/articles/[slug].put.ts
Normal file
109
app/server/api/articles/[slug].put.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { Article } from "../../models/Article";
|
||||
import { requireAuth } from "../../utils/auth";
|
||||
import { Types } from "mongoose";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const slug = getRouterParam(event, "slug");
|
||||
const user = await requireAuth(event);
|
||||
const body = await readBody(event);
|
||||
|
||||
if (!slug) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Slug is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Find article
|
||||
const article = await Article.findOne({ slug });
|
||||
|
||||
if (!article) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Article not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
const isAuthor = article.author.toString() === user.userId;
|
||||
const isAdmin = user.permissions.canAdmin;
|
||||
const isModerator = user.permissions.canModerate;
|
||||
const canEdit = user.permissions.canEdit;
|
||||
|
||||
if (!isAuthor && !isAdmin && !isModerator) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: "You do not have permission to edit this article",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if article is locked by another user
|
||||
if (article.lockedBy && article.lockedBy.toString() !== user.userId) {
|
||||
const lockExpired =
|
||||
article.lockedAt &&
|
||||
new Date().getTime() - article.lockedAt.getTime() > 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
if (!lockExpired) {
|
||||
throw createError({
|
||||
statusCode: 423,
|
||||
statusMessage: "Article is currently being edited by another user",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update fields
|
||||
if (body.title) article.title = body.title;
|
||||
if (body.description) article.description = body.description;
|
||||
if (body.content) {
|
||||
// Add to revision history
|
||||
article.revisions.push({
|
||||
content: body.content,
|
||||
author: new Types.ObjectId(user.userId),
|
||||
message: body.revisionMessage || "Content updated",
|
||||
createdAt: new Date(),
|
||||
});
|
||||
article.content = body.content;
|
||||
}
|
||||
if (body.category) article.category = body.category;
|
||||
if (body.tags) article.tags = body.tags;
|
||||
if (body.accessLevel && (isAdmin || isModerator)) {
|
||||
article.accessLevel = body.accessLevel;
|
||||
}
|
||||
if (body.cohorts && (isAdmin || isModerator)) {
|
||||
article.cohorts = body.cohorts;
|
||||
}
|
||||
if (body.status) {
|
||||
article.status = body.status;
|
||||
if (body.status === "published" && !article.publishedAt) {
|
||||
article.publishedAt = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
// Add contributor if not already listed
|
||||
const userObjectId = new Types.ObjectId(user.userId);
|
||||
if (!article.contributors.some((c: any) => c.toString() === user.userId)) {
|
||||
article.contributors.push(userObjectId);
|
||||
}
|
||||
|
||||
// Clear lock
|
||||
article.lockedBy = undefined;
|
||||
article.lockedAt = undefined;
|
||||
|
||||
// Save changes
|
||||
try {
|
||||
await article.save();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
slug: article.slug,
|
||||
message: "Article updated successfully",
|
||||
revision: article.revisions.length,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating article:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to update article",
|
||||
});
|
||||
}
|
||||
});
|
||||
76
app/server/api/articles/index.get.ts
Normal file
76
app/server/api/articles/index.get.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { Article } from '../../models/Article'
|
||||
import { checkAccessLevel } from '../../utils/auth'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event)
|
||||
|
||||
// Pagination
|
||||
const page = parseInt(query.page as string) || 1
|
||||
const limit = parseInt(query.limit as string) || 20
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
// Build filter
|
||||
const filter: any = { status: 'published' }
|
||||
|
||||
// Category filter
|
||||
if (query.category) {
|
||||
filter.category = query.category
|
||||
}
|
||||
|
||||
// Tags filter
|
||||
if (query.tags) {
|
||||
const tags = (query.tags as string).split(',')
|
||||
filter.tags = { $in: tags }
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if (query.search) {
|
||||
filter.$or = [
|
||||
{ title: { $regex: query.search, $options: 'i' } },
|
||||
{ description: { $regex: query.search, $options: 'i' } },
|
||||
{ content: { $regex: query.search, $options: 'i' } }
|
||||
]
|
||||
}
|
||||
|
||||
// Get articles
|
||||
const articles = await Article.find(filter)
|
||||
.populate('author', 'username displayName avatar')
|
||||
.select('-content -revisions -comments')
|
||||
.sort({ publishedAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
|
||||
// Filter by access level
|
||||
const filteredArticles = []
|
||||
for (const article of articles) {
|
||||
const hasAccess = await checkAccessLevel(
|
||||
event,
|
||||
article.accessLevel,
|
||||
article.cohorts
|
||||
)
|
||||
|
||||
if (hasAccess) {
|
||||
filteredArticles.push(article)
|
||||
} else if (article.accessLevel === 'member' || article.accessLevel === 'cohort') {
|
||||
// Show preview for protected content
|
||||
filteredArticles.push({
|
||||
...article.toObject(),
|
||||
description: article.description.substring(0, 200) + '...',
|
||||
restricted: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Get total count
|
||||
const total = await Article.countDocuments(filter)
|
||||
|
||||
return {
|
||||
articles: filteredArticles,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
}
|
||||
})
|
||||
71
app/server/api/articles/index.post.ts
Normal file
71
app/server/api/articles/index.post.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { Article } from '../../models/Article'
|
||||
import { requireAuth } from '../../utils/auth'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Require authentication
|
||||
const user = await requireAuth(event)
|
||||
|
||||
// Check if user can create articles
|
||||
if (!user.permissions.canEdit) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'You do not have permission to create articles'
|
||||
})
|
||||
}
|
||||
|
||||
// Get request body
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validate required fields
|
||||
if (!body.title || !body.slug || !body.content) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Title, slug, and content are required'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if slug already exists
|
||||
const existing = await Article.findOne({ slug: body.slug })
|
||||
if (existing) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'An article with this slug already exists'
|
||||
})
|
||||
}
|
||||
|
||||
// Create article
|
||||
try {
|
||||
const article = await Article.create({
|
||||
slug: body.slug,
|
||||
title: body.title,
|
||||
description: body.description || '',
|
||||
content: body.content,
|
||||
category: body.category || 'general',
|
||||
tags: body.tags || [],
|
||||
accessLevel: body.accessLevel || 'member',
|
||||
cohorts: body.cohorts || [],
|
||||
author: user.userId,
|
||||
status: body.status || 'draft',
|
||||
publishedAt: body.status === 'published' ? new Date() : null,
|
||||
revisions: [{
|
||||
content: body.content,
|
||||
author: user.userId,
|
||||
message: 'Initial creation',
|
||||
createdAt: new Date()
|
||||
}]
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
slug: article.slug,
|
||||
message: 'Article created successfully'
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating article:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to create article'
|
||||
})
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue