[nextjs]SNS Server-31(myapp41)Β 

πŸ‘‰πŸ» myapp41μ—μ„œλŠ” SNSλ©”μΈνŽ˜μ΄μ§€μ— 헀더뢀뢄과 νŒ”λ‘œμš°λ²„νŠΌμ„ μΆ”κ°€ν•©λ‹ˆλ‹€.
In myapp41, a header section and a “Follow” button are added to the social media main page.

πŸ‘‰πŸ» 전체 μ½”λ“œλŠ” κΉƒν—ˆλΈŒμ—μ„œ 확인 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
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/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/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/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
β”‚   β”‚   └── _components/themes/
β”‚   β”‚       β”œβ”€β”€ themeex/ThemeexTheme.tsx   -> Example Theme
β”‚   β”‚       β”œβ”€β”€ pinafore/PinaforeTheme.tsx -> Theme 1
β”‚   β”‚       β”œβ”€β”€ 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
β”‚   └── 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

βœ”οΈ ν”„λ‘œν•„ 헀더 μΆ”κ°€,μ‹€μ‹œκ°„ 카운트 반영
Added profile header; implemented real-time count updates.

— app/usersui/[username]/_components/themes/pinafore/PinaforeTheme.tsx

... ...
function FollowButton({ username, initialFollowing, currentUser, onToggle }: {
... ...
  return (
    <button
      onClick={toggle}
      disabled={loading}
      className={following? 'profile-edit-btn' : 'profile-follow-btn'}
    >
      {following? 'Following' : 'Follow'}
    </button>
  );
 }

export function PinaforeTheme({ timeline, username, onBoost, onLike }: any) {

  return (
    <>
     <style>{``}</style>
    </>
... ...
         {/* βœ… myapp41 - Timeline Header */}
         <div className="profile-header"> ... </div>
... ...
                <div>
                  {/* - λ‘œκ·ΈμΈν•˜λ©΄ ν”„λ‘œν•„ νŽΈμ§‘ 둜그인 ν›„ νŒ”λ‘œμš°λ²„νŠΌ / Log in to edit profile; follow button appears after logging in. */ }
                  {isMe? (
                    <>
                    {/* νŒ”λ‘œμš°,μ–ΈνŒ”λ‘œμš° λ²„νŠΌ / follow, unfollow button */ }
                    {/* <FollowButton username={username} initialFollowing={profile?.following || false} currentUser={currentUser} /> */}
                    <button className="profile-edit-btn">ν”„λ‘œν•„ νŽΈμ§‘/Edit Profile</button>
                    </>
                  ) : ( 
                      <FollowButton username={username} 
                        initialFollowing={profile?.following || false} 
                        currentUser={currentUser} 
                        onToggle={handleFollowToggle} 
                      />)}
                </div>
 )
}

βœ”οΈ user1으둜 아이디 κ³ μ • μˆ˜μ •, νŒ”λ‘œμš° 확인 jsonν•„λ“œ μΆ”κ°€
Fixed the user ID to ‘user1’ and added a JSON field to check follow status.

— app/api/v1/accounts/[id]/route.ts

... ...
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {    

    // parameter User - βœ… myapp41
    const me = getUsernameFromRequest(req);
    // const tempUserid = req.headers.get('x-user') || 'user1';  // βœ… 둜그인 이전 μž„μ‹œ 아이피 λ³€μˆ˜ / Temporary ID variable before login


// νŒ”λ‘œμš° μ—¬λΆ€ / Follow status
    let isFollowing = false;
    if (me && me!== targetUsername) {
      const f = db.prepare(`SELECT 1 FROM following WHERE username =? AND actor LIKE ?`).get(me, `%${targetUsername}%`) as any;
      console.log(`βœ… accounts-username:${me},actor:${targetUsername}`);
      if (f) isFollowing = true;
    }
    console.log(`me:${me},targetUsername:${targetUsername}`);
    return NextResponse.json({
      id: rawId,
      username: targetUsername,
      acct: `${targetUsername}@${DOMAIN}`,
      display_name: userRow?.display_name || targetUsername,
      avatar: `https://${DOMAIN}/icon.png`,
      avatar_static: `https://${DOMAIN}/icon.png`,
      header: `https://picsum.photos/seed/${DOMAIN}_${targetUsername}/1200/400`,
      header_static: `https://picsum.photos/seed/${DOMAIN}_${targetUsername}/1200/400`,
      followers_count: cntFollowers?.cnt || 0,
      following_count: cntFollowing?.cnt || 0,
      statuses_count: cntPosts?.cnt || 0,
      note: userRow?.summary? `<p>${userRow.summary}</p>` : `<p>${targetUsername}</p>`,
      url: `https://${DOMAIN}/users/${targetUsername}`,
      emojis: [], fields: [], bot: false,
      created_at: userRow?.created_at || new Date().toISOString(),
      following: isFollowing, // βœ… FollowButton용!
      is_me: me === targetUsername
    }, { headers: { 'Access-Control-Allow-Origin': '*' } });

  } catch (e) {
    console.error(e);
    return NextResponse.json({ error: 'failed' }, { status: 500, headers: { 'Access-Control-Allow-Origin': '*' , 'Cache-Control': 'no-store, no-cache, must-revalidate',} });
  }
... ...
}

βœ”οΈ μΏ ν‚€ λ””μ½”λ”© μ‹€νŒ¨ μˆ˜μ • / Fixed cookie decoding failure.

— lib/auth.ts

// βœ… myapp39 + myapp40 + myapp41 - username return from request
export function getUsernameFromRequest(req: Request | NextRequest): string | null { ... 
    // token μΏ ν‚€/Cookie
    const tokenCookie = cookies['token'];
    if (tokenCookie) {
      const decoded = verifyToken(tokenCookie);
      if (decoded?.username) {
        console.log(`βœ… me from token (verify): ${decoded.username}`);
        return decoded.username;
      }
      const payload = decodeJwtPayloadNoVerify(tokenCookie);
      if (payload?.username) {
        console.log(`βœ… me from token (no-verify): ${payload.username}`);
        return payload.username;
      }
    }
...
}

// βœ… myapp41 - verify 없이 payload만 μ½λŠ” ν•¨μˆ˜
// A function that reads only the payload without verification.
function decodeJwtPayloadNoVerify(token: string): any {
  try {
    const parts = token.split('.');
    if (parts.length!== 3) return null;
    let payload = parts[1];
    payload = payload.replace(/-/g, '+').replace(/_/g, '/');
    while (payload.length % 4) payload += '=';
    const json = Buffer.from(payload, 'base64').toString('utf-8');
    return JSON.parse(json);
  } catch {
    return null;
  }
}

βœ”οΈ νŒ”λ‘œμš°λ‚˜ μ–ΈνŒ”λ‘œμš°μ‹œ ν•œμͺ½ ν…Œμ΄λΈ”λ§Œ 정보 μΆ”κ°€ μ‚­μ œλ˜λŠ”κ²ƒ μˆ˜μ •
Fixed the issue where information was added or deleted in only one table when following or unfollowing.

— /api/follow/route.ts

...
// νŒ”λ‘œμš° API / follow API
export async function POST(req: Request) {
    // ===== 1. 둜컬 νŒ”λ‘œμš° / Local Follow =====
    if (localTarget) {
      // following: λ‚΄κ°€ νŒ”λ‘œμš°ν•˜λŠ” λͺ©λ‘
      // Following: The list of people I follow
      try {
        db.prepare(`INSERT OR IGNORE INTO following (id, actor, inbox, username) VALUES (?,?,?,?)`)
         .run(id, targetActor, `${targetActor}/inbox`, username);
      } catch {
        // ꡬ μŠ€ν‚€λ§ˆ ν˜Έν™˜ (inbox 컬럼 μ—†λŠ” 버전)
        // Legacy schema compatibility (version without the 'inbox' column)
        db.prepare(`INSERT OR IGNORE INTO following (id, actor, username) VALUES (?,?,?)`)
         .run(id, targetActor, username);
      }

      // followers: μƒλŒ€λ°©μ˜ νŒ”λ‘œμ›Œ λͺ©λ‘
      // followers: The other party's list of followers
      try {
        db.prepare(`INSERT OR IGNORE INTO followers (id, actor, inbox, username) VALUES (?,?,?,?)`)
         .run(`${localTarget}-${username}-${Date.now()}`, myActor, `${myActor}/inbox`, localTarget);
      } catch {
        db.prepare(`INSERT OR IGNORE INTO followers (id, actor, username) VALUES (?,?,?)`)
         .run(`${localTarget}-${username}-${Date.now()}`, myActor, localTarget);
      }
    }
    // ===== 2. 리λͺ¨νŠΈ νŒ”λ‘œμš° / Remote Follow =====
    const actorRes = await signedFetch(target, username);
}

// μ–ΈνŒ”λ‘œμš° / Unfollow
export async function DELETE(req: Request) {
... ...
    if (localTarget) {
      // λ‘œμ»¬μ€ DBμ—μ„œλ§Œ μ‚­μ œ / Delete from the database only (local).
      db.prepare(`DELETE FROM following WHERE username=? AND (actor LIKE? OR actor=?)`).run(username, `%/users/${localTarget}%`, localTarget);
      db.prepare(`DELETE FROM followers WHERE username=? AND (actor LIKE? OR actor=?)`).run(localTarget, `%/users/${username}%`, username);
      console.log(`πŸ—‘ 둜컬 μ–ΈνŒ”λ‘œμš°: ${username} -X-> ${localTarget}`);
      return Response.json({ ok: true, local: true });
    }
... ...
    // 리λͺ¨νŠΈ μ–ΈνŒ”λ‘œμš° / remote unfollow
... ...
    const { sendUndoFollow } = await import('@/lib/ap');
    const result = await sendUndoFollow(target, username);

    db.prepare('DELETE FROM following WHERE username=? AND (actor=? OR actor LIKE?)').run(username, target, `%${target}%`);

}

πŸ“ ν…ŒμŠ€νŠΈ / Test

βœ”οΈ user1λ‘œκ·ΈμΈν•˜κ³  user2 νŒ”λ‘œμš°
Log in as user1 and follow user2.

/@user2
/@user1

βœ”οΈ user1λ‘œκ·ΈμΈν•˜κ³  user2 μ–ΈνŒ”λ‘œμš°
Log in as user1 and unfollow user2.

/@user1
@user2

βœ”οΈ μ–ΈνŒ”λ‘œμš° μ„œλ²„λ‘œκ·Έ / Unfollow server log

Server Log

βœ”οΈ Database

sqlite> SELECT username, actor FROM following;
user1|https://aloy-horizon.duckdns.org/users/user2
user1|https://freelifemakers.com/users/user1
user1|https://mastodon.social/users/freelifemakers
sqlite> 
sqlite> 
sqlite> SELECT username, actor FROM followers;
user2|https://aloy-horizon.duckdns.org/users/user1
user1|https://freelifemakers.com/users/user1
user1|https://mastodon.social/ap/users/117193449750714993
sqlite> 

μš”ν•œλ³΅μŒ 8μž₯ 32절 / John 8:32

“그리고 λ„ˆν¬λŠ” 진리λ₯Ό μ•Œκ²Œ 될 것이며, 진리가 λ„ˆν¬λ₯Ό 자유둭게 ν•  것이닀.”

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

Leave a Reply