at://Press
at main 189 lines 5.5 kB view raw
1import type { APIRoute } from "astro"; 2import { checkOrigin, checkAuth, parseJsonBody, createPdsSession } from "../../lib/api"; 3import { isValidRkey, invalidateEntry, invalidateCache } from "../../lib/pds"; 4import { getDraft, saveDraft, deleteDraft } from "../../lib/drafts"; 5import { PDS_URL, DID, BLOG_COLLECTION, MAX_TITLE_LENGTH, MAX_CONTENT_LENGTH } from "../../lib/constants"; 6const VALID_VISIBILITY = ["public", "author"]; 7 8export const POST: APIRoute = async ({ request, cookies }) => { 9 const originErr = checkOrigin(request); 10 if (originErr) return originErr; 11 12 const authErr = checkAuth(cookies); 13 if (authErr) return authErr; 14 15 const [body, parseErr] = await parseJsonBody(request); 16 if (parseErr) return parseErr; 17 18 const rkey = typeof body.rkey === "string" ? body.rkey : ""; 19 const title = typeof body.title === "string" ? body.title.trim() : ""; 20 const content = typeof body.content === "string" ? body.content.trim() : ""; 21 const visibility = typeof body.visibility === "string" ? body.visibility : ""; 22 const rawCreatedAt = typeof body.createdAt === "string" ? body.createdAt : ""; 23 const createdAt = rawCreatedAt && !isNaN(Date.parse(rawCreatedAt)) 24 ? new Date(rawCreatedAt).toISOString() 25 : new Date().toISOString(); 26 27 if (!rkey || !isValidRkey(rkey)) { 28 return new Response(JSON.stringify({ error: "Invalid rkey" }), { status: 400 }); 29 } 30 31 if (!title || title.length > MAX_TITLE_LENGTH) { 32 return new Response( 33 JSON.stringify({ error: `Title is required and must be under ${MAX_TITLE_LENGTH} characters` }), 34 { status: 400 } 35 ); 36 } 37 38 if (!content || content.length > MAX_CONTENT_LENGTH) { 39 return new Response( 40 JSON.stringify({ error: `Content is required and must be under ${MAX_CONTENT_LENGTH} characters` }), 41 { status: 400 } 42 ); 43 } 44 45 if (!VALID_VISIBILITY.includes(visibility)) { 46 return new Response( 47 JSON.stringify({ error: "Visibility must be 'public' or 'author'" }), 48 { status: 400 } 49 ); 50 } 51 52 const blobs = Array.isArray(body.blobs) ? body.blobs : []; 53 const existingDraft = getDraft(rkey); 54 55 if (existingDraft) { 56 // This rkey is a local SQLite draft 57 if (visibility === "author") { 58 // Draft → Draft: update in SQLite only 59 saveDraft({ 60 rkey, 61 title, 62 content, 63 createdAt, 64 blobs: blobs.length > 0 ? blobs : undefined, 65 }); 66 return new Response(JSON.stringify({ success: true, rkey })); 67 } 68 69 // Draft → Publish: move from SQLite to PDS 70 const [accessJwt, sessionErr] = await createPdsSession(); 71 if (sessionErr) return sessionErr; 72 73 const putRes = await fetch( 74 `${PDS_URL}/xrpc/com.atproto.repo.putRecord`, 75 { 76 method: "POST", 77 headers: { 78 "Content-Type": "application/json", 79 Authorization: `Bearer ${accessJwt}`, 80 }, 81 body: JSON.stringify({ 82 repo: DID, 83 collection: BLOG_COLLECTION, 84 rkey, 85 record: { 86 $type: BLOG_COLLECTION, 87 title, 88 content, 89 createdAt, 90 visibility: "public", 91 ...(blobs.length > 0 && { blobs }), 92 }, 93 }), 94 } 95 ); 96 97 if (!putRes.ok) { 98 const err = await putRes.text(); 99 console.error("PDS putRecord (publish draft) failed:", err); 100 return new Response( 101 JSON.stringify({ error: "Failed to publish" }), 102 { status: 500 } 103 ); 104 } 105 106 deleteDraft(rkey); 107 invalidateCache(); 108 return new Response(JSON.stringify({ success: true, rkey })); 109 } 110 111 // This rkey is a PDS record 112 if (visibility === "author") { 113 // Published → Unpublish: move from PDS to SQLite 114 saveDraft({ 115 rkey, 116 title, 117 content, 118 createdAt, 119 blobs: blobs.length > 0 ? blobs : undefined, 120 }); 121 122 const [accessJwt, sessionErr] = await createPdsSession(); 123 if (sessionErr) return sessionErr; 124 125 const deleteRes = await fetch( 126 `${PDS_URL}/xrpc/com.atproto.repo.deleteRecord`, 127 { 128 method: "POST", 129 headers: { 130 "Content-Type": "application/json", 131 Authorization: `Bearer ${accessJwt}`, 132 }, 133 body: JSON.stringify({ 134 repo: DID, 135 collection: BLOG_COLLECTION, 136 rkey, 137 }), 138 } 139 ); 140 141 if (!deleteRes.ok) { 142 console.warn("PDS deleteRecord (unpublish) failed:", deleteRes.status); 143 } 144 145 invalidateEntry(rkey); 146 return new Response(JSON.stringify({ success: true, rkey })); 147 } 148 149 // Published → Published: update on PDS (existing behavior) 150 const [accessJwt, sessionErr] = await createPdsSession(); 151 if (sessionErr) return sessionErr; 152 153 const putRes = await fetch( 154 `${PDS_URL}/xrpc/com.atproto.repo.putRecord`, 155 { 156 method: "POST", 157 headers: { 158 "Content-Type": "application/json", 159 Authorization: `Bearer ${accessJwt}`, 160 }, 161 body: JSON.stringify({ 162 repo: DID, 163 collection: BLOG_COLLECTION, 164 rkey, 165 record: { 166 $type: BLOG_COLLECTION, 167 title, 168 content, 169 createdAt, 170 visibility, 171 ...(blobs.length > 0 && { blobs }), 172 }, 173 }), 174 } 175 ); 176 177 if (!putRes.ok) { 178 const err = await putRes.text(); 179 console.error("PDS putRecord failed:", err); 180 return new Response( 181 JSON.stringify({ error: "Failed to update entry" }), 182 { status: 500 } 183 ); 184 } 185 186 invalidateEntry(rkey); 187 188 return new Response(JSON.stringify({ success: true, rkey })); 189};