aethex-forge/api/community/posts.ts
2025-11-16 07:12:36 +00:00

341 lines
9.8 KiB
TypeScript

export const config = {
runtime: "nodejs",
};
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.VITE_SUPABASE_URL;
const supabaseServiceRole = process.env.SUPABASE_SERVICE_ROLE;
if (!supabaseUrl || !supabaseServiceRole) {
throw new Error("Missing Supabase configuration");
}
const supabase = createClient(supabaseUrl, supabaseServiceRole);
const VALID_ARMS = [
"labs",
"gameforge",
"corp",
"foundation",
"devlink",
"nexus",
"staff",
];
export default async function handler(req: any, res: any) {
if (req.method === "POST") {
try {
const { title, content, arm_affiliation, author_id, tags, category } =
req.body;
// Validate required fields
if (!title || !content || !arm_affiliation || !author_id) {
return res.status(400).json({
error:
"Missing required fields: title, content, arm_affiliation, author_id",
});
}
// Validate arm_affiliation
if (!VALID_ARMS.includes(arm_affiliation)) {
return res.status(400).json({
error: `Invalid arm_affiliation. Must be one of: ${VALID_ARMS.join(", ")}`,
});
}
// Validate content length
if (title.trim().length === 0 || title.length > 500) {
return res
.status(400)
.json({ error: "Title must be between 1 and 500 characters" });
}
if (content.trim().length === 0 || content.length > 5000) {
return res
.status(400)
.json({ error: "Content must be between 1 and 5000 characters" });
}
// Validate author exists
const { data: author, error: authorError } = await supabase
.from("user_profiles")
.select("id")
.eq("id", author_id)
.single();
if (authorError || !author) {
return res.status(401).json({ error: "User not found" });
}
// Insert the post (published by default for users)
const { data, error } = await supabase
.from("community_posts")
.insert({
title: title.trim(),
content: content.trim(),
arm_affiliation,
author_id,
tags: tags || [],
category: category || null,
is_published: true,
likes_count: 0,
comments_count: 0,
})
.select(
`
id,
title,
content,
arm_affiliation,
author_id,
created_at,
updated_at,
is_published,
likes_count,
comments_count,
tags,
category,
user_profiles!community_posts_author_id_fkey (
id,
username,
full_name,
avatar_url
)
`,
);
if (error) {
console.error("[Community Posts API] Insert error:", error);
return res.status(500).json({ error: error.message });
}
const createdPost = data?.[0] as any;
// Sync post to Discord feed webhook
try {
const apiBase = process.env.API_BASE || "/api";
await fetch(`${apiBase}/discord/feed-sync`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: createdPost?.id,
title: createdPost?.title,
content: createdPost?.content,
author_name:
createdPost?.user_profiles?.full_name ||
createdPost?.user_profiles?.username ||
"Community member",
author_avatar: createdPost?.user_profiles?.avatar_url,
arm_affiliation: createdPost?.arm_affiliation,
likes_count: createdPost?.likes_count,
comments_count: createdPost?.comments_count,
created_at: createdPost?.created_at,
}),
}).catch((err) =>
console.error("[Posts API] Discord sync error:", err),
);
} catch (error) {
console.error("[Posts API] Failed to sync to Discord:", error);
}
// Publish activity event for post creation
try {
const apiBase = process.env.API_BASE || "/api";
await fetch(`${apiBase}/activity/publish`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
actor_id: author_id,
verb: "created",
object_type: "post",
object_id: createdPost?.id,
metadata: {
summary: `Posted to ${arm_affiliation}`,
title: title.substring(0, 100),
},
}),
}).catch((err) =>
console.error("[Posts API] Activity publish error:", err),
);
} catch (error) {
console.error("[Posts API] Failed to publish activity:", error);
}
// Apply rewards for post creation
try {
const apiBase = process.env.API_BASE || "/api";
await fetch(`${apiBase}/rewards/apply`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_id: author_id,
action: "post_created",
amount: 25,
}),
}).catch((err) =>
console.error("[Posts API] Rewards apply error:", err),
);
} catch (error) {
console.error("[Posts API] Failed to apply rewards:", error);
}
return res.status(201).json({
post: createdPost,
});
} catch (error: any) {
console.error("[Community Posts API POST] Unexpected error:", error);
return res
.status(500)
.json({ error: error.message || "Internal server error" });
}
}
if (req.method === "PUT") {
try {
const { id, title, content, arm_affiliation, category, tags, user_id } =
req.body;
if (!id || !user_id) {
return res.status(400).json({ error: "Missing id or user_id" });
}
// Get the post to verify ownership
const { data: post, error: fetchError } = await supabase
.from("community_posts")
.select("author_id")
.eq("id", id)
.single();
if (fetchError) {
console.error("[Community Posts API] Fetch error:", fetchError);
return res.status(404).json({ error: "Post not found" });
}
if (post.author_id !== user_id) {
return res
.status(403)
.json({ error: "You can only edit your own posts" });
}
// Validate updates
if (title && (title.trim().length === 0 || title.length > 500)) {
return res
.status(400)
.json({ error: "Title must be between 1 and 500 characters" });
}
if (content && (content.trim().length === 0 || content.length > 5000)) {
return res
.status(400)
.json({ error: "Content must be between 1 and 5000 characters" });
}
if (arm_affiliation && !VALID_ARMS.includes(arm_affiliation)) {
return res.status(400).json({
error: `Invalid arm_affiliation. Must be one of: ${VALID_ARMS.join(", ")}`,
});
}
// Build update object
const updateData: any = {};
if (title) updateData.title = title.trim();
if (content) updateData.content = content.trim();
if (arm_affiliation) updateData.arm_affiliation = arm_affiliation;
if (category !== undefined) updateData.category = category;
if (tags) updateData.tags = tags;
const { data, error } = await supabase
.from("community_posts")
.update(updateData)
.eq("id", id)
.select(
`
id,
title,
content,
arm_affiliation,
author_id,
created_at,
updated_at,
is_published,
likes_count,
comments_count,
tags,
category,
user_profiles!community_posts_author_id_fkey (
id,
username,
full_name,
avatar_url
)
`,
);
if (error) {
console.error("[Community Posts API] Update error:", error);
return res.status(500).json({ error: error.message });
}
return res.status(200).json({
post: data?.[0],
});
} catch (error: any) {
console.error("[Community Posts API PUT] Unexpected error:", error);
return res
.status(500)
.json({ error: error.message || "Internal server error" });
}
}
if (req.method === "DELETE") {
try {
const { id, user_id } = req.body;
if (!id || !user_id) {
return res.status(400).json({ error: "Missing id or user_id" });
}
// Get the post to verify ownership
const { data: post, error: fetchError } = await supabase
.from("community_posts")
.select("author_id")
.eq("id", id)
.single();
if (fetchError) {
console.error("[Community Posts API] Fetch error:", fetchError);
return res.status(404).json({ error: "Post not found" });
}
if (post.author_id !== user_id) {
return res
.status(403)
.json({ error: "You can only delete your own posts" });
}
// Delete the post (cascade will delete likes and comments)
const { error } = await supabase
.from("community_posts")
.delete()
.eq("id", id);
if (error) {
console.error("[Community Posts API] Delete error:", error);
return res.status(500).json({ error: error.message });
}
return res.status(200).json({
success: true,
message: "Post deleted successfully",
});
} catch (error: any) {
console.error("[Community Posts API DELETE] Unexpected error:", error);
return res
.status(500)
.json({ error: error.message || "Internal server error" });
}
}
return res.status(405).json({ error: "Method not allowed" });
}