[nextjs]SNS Server-39(myapp49) 

👉🏻 myapp49에서는 myapp48까지 기능 추가 되면서 발생한 오류나 버그를 수정합니다.
myapp49 fixes errors and bugs that arose during the addition of features up to myapp48.

👉🏻 부스트 ,좋아요 배달 부분 오류를 수정합니다.
Fixed errors related to Boosts, Likes, and delivery.

👉🏻 외부앱에서 글쓰거나 삭제하는 API를 터미널에서 사용할 경우 토큰을 입력해야합니다.
You must enter a token when using the API to create or delete posts from an external app via the terminal.

👉🏻 전체 코드는 깃허브에서 확인 할 수 있습니다.
You can find the full code on GitHub.

https://github.com/gideonslife01/flm-nextjs

📁 전체 프로젝트 구조 / Overall Project Structure

myapp project/  
├── app/  (Next.js App Router)
│   ├── .well-known/webfinger/route.ts  -> webfinger
│   ├── api/follow/route.ts    -> Follow API(temporary)
│   ├── api/announce/route.ts  -> Boost(Announcement)
│   ├── api/like/route.ts.     -> Like API
│   ├── api/posts/route.ts     -> Writing API
│   ├── api/timeline/route.ts  -> Timeline API
│   ├── api/followinglist/route.ts  -> followinglist API
│   ├── api/followerslist/route.ts  -> followerslist API
│   ├── api/v1/instance/route.ts -> auth
│   ├── api/v1/apps/route.ts.    -> auth
│   ├── api/v1/accounts/verify_credentials/route.ts -> auth
│   ├── api/v1/statuses/route.ts -> Write Post
│   ├── api/v1/timelines/home/route.ts -> timeline
│   ├── oauth/authorize/route.ts -> auth
│   ├── oauth/token/route.ts -> auth
│   ├── api/v1/search/home/route.ts -> search
│   ├── api/v2/search/home/route.ts -> search
│   ├── api/v1/accounts/[id]/followers/route.ts -> followers
│   ├── api/v1/accounts/[id]/following/route.ts -> following
│   ├── api/v1/statuses/[id]/route.ts -> -> Post delete(pinafore)
│   ├── api/auth/signup/route.ts        -> Signup
│   ├── api/auth/login/route.ts         -> Login
│   ├── api/auth/logout/route.ts        -> Logout
│   ├── api/auth/refresh/route.ts       -> Refresh Token
│   ├── api/cleanup-orphan/route.ts     -> post,outbox clean up
│   ├── api/auth/me/route.tsx           -> Login Check
│   ├── auth/signup/page.tsx            -> Signup UI
│   ├── auth/signup/page.tsx            -> Login UI
│   ├── users/[username]/
│   │   ├── statuses/[id]/route.ts -> Indivisual Post
│   │   ├── route.ts               -> Acotr Information
│   │   ├── followers/route.ts     -> Followers List
│   │   ├── following/route.ts     -> Following List
│   │   ├── inbox/route.ts         -> Inbox
│   │   └── outbox/route.ts        -> outbox
│   ├── usersui/[username]/
│   │   ├── page.tsx               -> Timeline UI(Server, 404)
│   │   ├── ClientPage.tsx.        -> Timeline UI(Client,Call Active Themes)
│   │   └── _components/themes/
│   │       ├── themeex/ThemeexTheme.tsx   -> Example Theme
│   │       ├── pinafore/PinaforeTheme.tsx -> Theme 1
│   │       ├── pinafore/FollowersList.tsx -> Theme 1, FollowersList
│   │       ├── pinafore/FollowingList.tsx -> Theme 1, FollowingList
│   │       ├── pinafore/NotificationsList.tsx -> Theme 1, NotificationsList
│   │       ├── mastodon/MastodonTheme.tsx -> Theme 2
│   │       └── minimal/MinimaltTheme.tsx  -> Theme 3
│   ├── layout.tsx, page.tsx, globals.css
│   └── favicon.ico
├── lib/
│   ├── theme.tsx              -> Theme Provider
│   ├── watchThemes.ts         -> Check real-time theme changes
│   ├── auth.ts                -> Authentication, User Management
│   ├── ap.ts                  -> Follow,Undo,Create,Likes,Announce
│   ├── visibility.ts          -> Visibility(Public,Unlisted,Private,Direct)
│   └── db.ts                  -> DB connection
├── data/
│   ├── keys/userIDs/          -> private.pem, public.pem(New)
│   └── keys/                  -> private.pem, public.pem(legacy)
├── data.sqlite                -> Database(1/3)
├── data.sqlite-wal            -> Database(2/3)
├── data.sqlite-shm            -> Database(3/3)
├── Caddyfile                  -> https 
├── instrumentation.ts         -> Background Server
└── package.json

📁 프로젝트 시작 / Project Start


📁 코드 수정 / Code Modification

✔️ 내 서버에서 원본글 삭제시 리모트 서버에서 삭제되지 않는 문제 수정
Fixed an issue where deleting the original post on my server did not delete it from the remote server.

— app/api/posts/route.ts

  //const noteId = `https://${DOMAIN}/users/${username}/posts/${id}`; // ✅ 전체 URL 만들기 / Create full URL

  // ✅ myapp49 - bug fix
  const noteId = `https://${DOMAIN}/users/${username}/statuses/${id}`;

✔️ 부스트 및 좋아요 오류 수정
Fixed Boost and Like errors.

— 고투소셜 원본 글에 좋아요,부스트를 내 서버의 배달된 글에 실행한경우 원본 글 삭제시 내서버의 배달된글과 좋아요,부스트도 같이 삭제
If you “like” or “boost” an original GoToSocial post and that action is applied to the copy delivered to your server, deleting the original post will also delete the delivered copy along with the associated likes and boosts.

if (body.type === 'Delete') {
... ...
if (objectId) {
... ...
              // ✅ myapp49 - 배달된 글의 원본 삭제시 부스트 좋아요 삭제 
              // Deleting the original delivered post removes the 'Boost Like'.

              // - 좋아요 정리 / delete likes
              const likeDel = db.prepare(`DELETE FROM likes WHERE object LIKE? OR object LIKE?`).run(`%${shortId}%`, `%${canonicalId}%`);
              if (likeDel.changes > 0) {
                console.log(` -> likes 정리: ${likeDel.changes}개 삭제 (원본 ${shortId})`);
              }

              // - 부스트 정리 / delete boost(announce)
              const announceDel = db.prepare(`DELETE FROM announces WHERE object LIKE? OR object LIKE?`).run(`%${shortId}%`, `%${canonicalId}%`);
              if (announceDel.changes > 0) {
                console.log(` -> announces 정리: ${announceDel.changes}개 삭제 (원본 ${shortId})`);
              }
... ...
}
... ...
}

✔️ 부스트 라벨 수정 / Edit Boost Label

— inbox_posts로 배달되는 모든글에 boosted 라벨이 붙는 문제 수정
Fixed an issue where all posts delivered to inbox_posts were labeled as “boosted.”

    const boostCount = (db.prepare(
      `SELECT COUNT(*) as c FROM announces WHERE object =? OR object =? OR object =? OR object LIKE '%' ||? || '%'`
    ).get(fullId, p.original_id || '', p.id || '', shortId) as any)?.c || 0;

    // ✅ myapp49- announces.object에 원본 있으면 true (네가 말한 로직)
    const isBoostedPost = boostCount > 0 ? true : false;

1)announces테이블에서 id는 부스트한사람 전체 URL,object는 원본글URL, actor는 부스트한 사용자 계정 주소
In the announces table, id is the full URL of the boost, object is the URL of the original post, and actor is the account address of the user who performed the boost.

2) announces테이블의 object필드, inbox_posts 테이블의 id, original_id는 원본 글을 가리킴
The object field in the announces table, as well as the id and original_id fields in the inbox_posts table, refer to the original post.

✔️ 좋아요 실행 및 좋아요취소
Like and Unlike

— 좋아요 부분에서 내 글인지 체크하는 부분 도메인추가
Added the domain check to verify if the post belongs to the user in the “Like” section.

— 배송된 좋아요 삭제 로직 수정
Fixed logic for deleting delivered ‘likes’

— app/api/like/route.ts

export async function POST(req: NextRequest) {
... ...
const isOwnPost = target.includes(`${DOMAIN}/users/${username}/`); // ✅ myapp49 도메인추가 / add DOMAIN
... ...
}

export async function DELETE(req: NextRequest) {
  const { username, target } = await req.json();
  const myActor = `https://${DOMAIN}/users/${username}`;
  const shortId = target.replace('/posts/', '/statuses/').split('/').pop()!.split('?')[0];

  const likeRow = db.prepare(`SELECT id, object FROM likes WHERE actor =? AND object LIKE '%' ||? || '%'`).get(myActor, shortId) as any;

  if (!likeRow) {
    const likeCount = (db.prepare(`SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||? || '%'`).get(shortId) as any).c;
    return NextResponse.json({ ok: true, alreadyUnliked: true, isMyLike: false, likeCount });
  }

  const objectForCheck = likeRow.object || target;

  const isOwnPost = objectForCheck.includes(`${DOMAIN}/users/${username}/`);

 // ✅ myapp49 - 좋아요 배달 / Send a Like
  if (!isOwnPost) {
    try {
      const u = new URL(objectForCheck);
      const parts = u.pathname.split('/').filter(Boolean);
      const idx = parts.indexOf('users');
      const inbox = idx!== -1? `${u.origin}/users/${parts[idx+1]}/inbox` : null;
      if (inbox) {
        await sendUndoLike(inbox, likeRow.id, objectForCheck, username);
      }
    } catch (e) {
      console.error('Undo Like 배달 실패', e);
    }
  }

  db.prepare('DELETE FROM likes WHERE id =?').run(likeRow.id);
  const likeCount = (db.prepare(`SELECT COUNT(*) as c FROM likes WHERE object LIKE '%' ||? || '%'`).get(shortId) as any).c;
  return NextResponse.json({ ok: true, isMyLike: false, likeCount });
}

✔️ 부스트 실행 및 부스트 취소
Start Boost and Cancel Boost

— 좋아요 부분에 내 글 인지 체크하는 부분 도메인추가
Added a domain check to the “Like” section to verify if the post belongs to the user.

— 배송된 부스트 삭제 로직 수정
Modified logic for deleting delivered boosts

— app/api/announces/route.ts

export async function POST(req: NextRequest) {
... ...
const isOwnPost = target.includes(`${DOMAIN}//users/${username}/`); // ✅ myapp48 도메인추가  / Add DOMAIN
... ...
}

export async function DELETE(req: NextRequest) {
  try {
    const { username, target } = await req.json();
    const myActor = `https://${DOMAIN}/users/${username}`;
    const shortId = target.replace('/posts/', '/statuses/').split('?')[0].split('#')[0].split('/').filter(Boolean).pop() || target;

    const row = db.prepare(
      `SELECT id, object, actor FROM announces WHERE username =? AND object LIKE '%' ||? || '%'`
    ).get(username, shortId) as any;

    if (!row) {
      const boostCount = (db.prepare(
        `SELECT COUNT(*) as c FROM announces WHERE object LIKE '%' ||? || '%'`
      ).get(shortId) as any).c;
      return NextResponse.json({ ok: true, alreadyUnboosted: true, isMyBoost: false, boostCount });
    }

    // 부스트 취소 배달 / Undo Boost Delivery
    const objectForCheck = row.object || target;
    const isOwnPost = objectForCheck.includes(`${DOMAIN}/users/${username}/`);

    if (!isOwnPost) {
      try {
        const u = new URL(objectForCheck);
        const parts = u.pathname.split('/').filter(Boolean);
        const idx = parts.indexOf('users');
        const inbox = idx!== -1? `${u.origin}/users/${parts[idx+1]}/inbox` : null;
        if (inbox) {
          await sendUndoAnnounce(inbox, row.id, objectForCheck, username);
        }
      } catch (e) {
        console.error('Undo Announce 배달 실패', e);
      }
    }

    db.prepare('DELETE FROM announces WHERE id =?').run(row.id);

    const boostCount = (db.prepare(
      `SELECT COUNT(*) as c FROM announces WHERE object LIKE '%' ||? || '%'`
    ).get(shortId) as any).c;

    return NextResponse.json({ ok: true, isMyBoost: false, boostCount });

  } catch (e: any) {
    console.error('[Boost DELETE]', e);
    return NextResponse.json({ ok: false, error: e.message }, { status: 500 });
  }
}

✔️ 라우트 보안강화 / Route Security Enhancement

— 터미널에서 토큰이나 쿠키 없이 글쓰거나 삭제를 할 수 없습니다.
You cannot write or delete posts from the terminal without a token or cookie.

— app/api/v1/statuses/route.ts — Pinafore

export async function POST(req: Request) {
... ...
    // ✅ myapp49 - pinafore,local server login 
    let username: string | null = null;
    // ✅ myapp49 - 1. Bearer 토큰 체크 / Bearer token check (Pinafore)
    if (token) {
      try {
        const oauthToken = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(token) as any;
        if (oauthToken?.username) username = oauthToken.username;
      } catch {}
    }

    // 2. 쿠키 체크 (로컬 서버) / Cookie Check (Local Server)
    if (!username) {
      try {
        const { getViewerFromRequest } = await import('@/lib/visibility');
        username = getViewerFromRequest(req);
      } catch {}
    }

    // ✅ 3. 둘 다 없으면 401 - 'user1' 폴백 절대 금지
    // If neither exists, return 401—falling back to 'user1' is strictly prohibited.
    if (!username) {
      return NextResponse.json({ error: 'unauthorized - Bearer token or login required' }, { status: 401 });
    }
... ...
}

— app/api/v1/statuses/[id]/route.ts – Pinafore


export async function DELETE(
  req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
... ...  
// ✅ myapp49 - 1. 인증 - Bearer + 쿠키 둘 다 지원 / Authentication - Supports both Bearer and cookies.

  const auth = req.headers.get('Authorization') || '';
  const token = auth.replace('Bearer ', '');
  let username: string | null = null;


  if (token) {
    try {
      const oauthToken = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(token) as any;
      if (oauthToken?.username) username = oauthToken.username;
    } catch {}
  }

  if (!username) {
    try {
      const { getViewerFromRequest } = await import('@/lib/visibility');
      username = getViewerFromRequest(req as any);
    } catch {}
  }

  // let oauthToken: any;
  // try { oauthToken = db.prepare('SELECT * FROM oauth_tokens WHERE access_token=?').get(token) as any; } catch {}
  // const username = oauthToken?.username || 'user1';

  // ✅ myapp49 - 폴백 금지 - 인증 없으면 401 / Disable fallback – 401 if unauthenticated
  if (!username) {
    return NextResponse.json({ error: 'unauthorized' }, { status: 401, headers: { 'Access-Control-Allow-Origin': '*' } });
  }

  console.log(`🗑 Pinafore 삭제 요청 / Request to delete Pinafore : ${username} -> ${id}`);

  // ✅ myapp49 - 소유권 체크 - 내 글 맞는지 확인 / Ownership Check – Verify if it is my post
  let post: any;
  try {
    post = db.prepare('SELECT id, username, visibility FROM posts WHERE id=?').get(id) as any;
  } catch {}

  if (!post) {
    // 이미 지워졌으면 Pinafore는 404보다 200을 원함 - 멱등성
    return NextResponse.json({ id, deleted: true, alreadyDeleted: true }, {
      headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*' }
    });
  }

  if (post.username !== username) {
    console.log(`🚫 삭제 거부: ${username}가 ${post.username} 글 삭제 시도`);
    return NextResponse.json({ error: 'forbidden - not your post' }, { 
      status: 403, 
      headers: { 'Access-Control-Allow-Origin': '*' } 
    });
  }
... ...
}

📁 테스트 / Test

✔️ 좋아요 / Likes

local
GTS

✔️ 부스트 / Boost

Local
GTS

✔️ api/v1/statuses 라우트 터미널 테스트
Terminal test for the api/v1/statuses route

# 1.글쓰기 테스트 - 토큰이나 쿠키 없는 경우
Writing Test - No tokens or cookies

 curl -s -X POST https://aloy-horizon.duckdns.org/api/v1/statuses \
 -H "Content-Type: application/json" \
 -d '{"status":"write post test-1","visibility":"public"}'

# 응답 / Response
{"error":"unauthorized - Bearer token or login required"}%    

# 2.글쓰기 테스트 / Test - refresh token
curl -s -X POST https://aloy-horizon.duckdns.org/api/v1/statuses \
 -H "Cookie: refresh_token=099b61b8616edcf48b ... ..." \
 -H "Content-Type: application/json" \
 -d '{"status":"refresh token","visibility":"public"}'

# 응답 / Response

{"id":"c3b1cefe-853c-476e-b978-3b2e28506a92","uri":"https://aloy-horizon.duckdns.org/users/user1/statuses/c3b1cefe-853c-476e-b978-3b2e28506a92","url":"https://aloy-horizon.duckdns.org/users/user1/statuses/c3b1cefe-853c-476e-b978-3b2e28506a92","account":{"id":"1","username":"user1","acct":"user1@aloy-horizon.duckdns.org","display_name":"user1","avatar":"https://aloy-horizon.duckdns.org/icon.png"},"content":"<p>refresh token</p>","created_at":"2026-09-26T04:26:48.469Z","visibility":"public","reblogs_count":0,"favourites_count":0,"replies_count":0}%   

   
# 3. 글 삭제 테스트 - 토큰이나 쿠키가 없는경우
Post deletion test – when tokens or cookies are missing

myapp49 % curl -s -X DELETE https://aloy-horizon.duckdns.org/api/v1/statuses/9a9f4c12-265c-46ed-b956-f51a5e61fdb2
# 응답 / Response
{"error":"unauthorized"}%                         

# 4. 글 삭제 테스트 / Post deletion test - refresh token
curl -s -X DELETE https://aloy-horizon.duckdns.org/api/v1/statuses/9a9f4c12-265c-46ed-b956-f51a5e61fdb2 \
 -H "Authorization: Bearer 099b61b8616edcf48b4cf7260ef818b7a451... ..."

# 응답 / Response   
{"id":"9a9f4c12-265c-46ed-b956-f51a5e61fdb2","deleted":true}%   
                                                 
                                                          

💡 터미널에서 직접 글쓰기 / Writing directly in the terminal

  curl -X POST https://aloy-horizon.duckdns.org/users/user1/outbox \
  -H "Content-Type: application/activity+json" \
  -d '{"type":"Create","object":{"content":"Hello Activity UI #activityposts "}}'

💡 터미널에서 직접 글 삭제 / Delete posts directly from the terminal.

curl -s -X DELETE https://aloy-horizon.duckdns.org/api/v1/statuses/9a9f4c12-265c-46ed-b956-f51a5e61fdb2 \
 -H "Authorization: Bearer 099b61b8616edcf48b4cf7260ef818b7a451... ..."

💡 좋아요 및 좋아요 취소실행 / Perform Like and Unlike actions

# 좋아요 실행 / Likes
curl -X POST https://aloy-horizon.duckdns.org/api/like \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://freelifemakers.com/users/user1/statuses/01M0XT1VBF23F8Y5EXAPK1VY56"}'

# 좋아요 취소하기 / Undo Likes
curl -X DELETE https://aloy-horizon.duckdns.org/api/like \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","target":"https://freelifemakers.com/users/user1/statuses/01M0XT1VBF23F8Y5EXAPK1VY56"}'

요한복음 8장 32절 / John 8:32

“그리고 너희는 진리를 알게 될 것이며, 진리가 너희를 자유롭게 할 것이다.”

“Then you will know the truth ,and the truth will set you free”

Leave a Reply