[nextjs]SNS Server-40(myapp50)ย 

๐Ÿ‘‰๐Ÿป myapp50์—์„œ๋Š” ํ…Œ๋งˆ์— ๊ณต์œ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ ํ•ฉ๋‹ˆ๋‹ค.
myapp50 adds a sharing feature to the theme.

๐Ÿ‘‰๐Ÿป myapp50๋ถ€ํ„ฐ๋Š” ๋กœ์ปฌ์„œ๋ฒ„์—์„œ ํ„ฐ๋ฏธ๋„๋กœ ๊ธ€ ์ž‘์„ฑํ•˜๊ณ  ๊ธ€ ์‚ญ์ œํ•  ๋•Œ ํ† ํฐ์„ ์ž…๋ ฅํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
Starting with myapp50, you must enter a token when creating or deleting posts via the terminal on the local server.

๐Ÿ‘‰๐Ÿป ์ „์ฒด ์ฝ”๋“œ๋Š” ๊นƒํ—ˆ๋ธŒ์—์„œ ํ™•์ธ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
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

โœ”๏ธ ๊ณต์œ  ๊ธฐ๋Šฅ ์ถ”๊ฐ€ / Added sharing feature

— app/usersui/[username]/ClientPage.tsx

... ...
  // โœ… myapp50 - ๊ณต์œ  ๊ธฐ๋Šฅ - (ActivityPub์ด๋ž‘ ์ƒ๊ด€์—†์Œ)
  // Sharing feature - (Unrelated to ActivityPub)
  const handleShare = async (post: any) => {
    const latest = timelineRef.current.find((p: any) => p.id === post.id) || post;
    const DOMAIN = process.env.NEXT_PUBLIC_DOMAIN || 'aloy-horizon.duckdns.org';

    let url = latest.original_id || latest.id;
    if (!url.startsWith('http')) {
      url = `https://${DOMAIN}/users/${latest.username}/statuses/${url}`;
    }
    
    const shareUrl = url.includes('/statuses/')? url : `https://${DOMAIN}/users/${latest.username}/statuses/${latest.id}`;

    const text = latest.content? latest.content.replace(/<[^>]*>/g, '').slice(0, 100) : '';

    // 1. ๋ชจ๋ฐ”์ผ - ๋„ค์ดํ‹ฐ๋ธŒ ๊ณต์œ  ์‹œํŠธ (์นดํ†ก, ํ…”๋ ˆ๊ทธ๋žจ, ์—์–ด๋“œ๋ž ์ž๋™)
    // Mobile โ€“ Native share sheet (KakaoTalk, Telegram, AirDrop supported automatically)
    if (navigator.share) {
      try {
        await navigator.share({
          title: `${latest.username}์˜ ๊ธ€`,
          text: text,
          url: shareUrl
        });
        return { ok: true, method: 'native' };
      } catch (e) {
        // ์ทจ์†Œํ•œ ๊ฒฝ์šฐ ๋ฌด์‹œ
        console.log('share cancelled', e);
      }
    }

    // 2. PC ํด๋ฐฑ - ํด๋ฆฝ๋ณด๋“œ ๋ณต์‚ฌ
    // PC Fallback - Copy to Clipboard
    try {
      await navigator.clipboard.writeText(shareUrl);
      alert(`๐Ÿ”— ๋งํฌ ๋ณต์‚ฌ๋จ\n${shareUrl}`);
      return { ok: true, method: 'clipboard' };
    } catch {
      // ํด๋ฆฝ๋ณด๋“œ๋„ ๋ง‰ํžŒ ๊ฒฝ์šฐ
      prompt('๋งํฌ ๋ณต์‚ฌ:', shareUrl);
      return { ok: true, method: 'prompt' };
    }
  };
... ...
  // โœ… myapp50 - handleShare๋ฅผ onShare์— ์ถ”๊ฐ€ํ•ด์„œ ํ…Œ๋งˆ๋กœ ๋„˜๊น€
  // Added handleShare to onShare and passed it to the theme.
  const props = { timeline, username, initialView, onBoost: handleBoost, onLike: handleLike, onShare: handleShare };
... ...

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

... ...
export function PinaforeTheme({ timeline : initialTimeline, username, initialView, onBoost, onLike, onShare }: any) {
... ...
     <div className="status-actions">
           <button onClick={() => onBoost(p)} className={p.isMyBoost? 'boosted' : ''}>๐Ÿ” {p.boostCount || ''}</button>
           <button onClick={() => onLike(p)} className={p.isMyLike? 'liked' : ''}>โญ {p.likeCount || ''}</button>
            <button onClick={() => onShare(p)}>โœˆ๏ธ</button> {/* โœ… myapp50 - Share */}
            <button>๐Ÿ’ฌ</button>
      </div>
... ...
}

โœ”๏ธ ๊ธ€์“ฐ๊ธฐ,๊ธ€์‚ญ์ œ ๋ณด์•ˆ๊ฐ•ํ™”(๋กœ์ปฌ์„œ๋ฒ„)
Enhanced security for posting and deleting content (local server)

— app/api/posts/route.ts

// โœ… myapp50 - ๊ณตํ†ต ์ธ์ฆ ํ•จ์ˆ˜ - refresh_token(์ฟ ํ‚ค) + Bearer ๋‘˜ ๋‹ค ์ฒดํฌ
// Common authentication function โ€“ checks for both refresh_token (cookie) and Bearer token.
function getAuthenticatedUser(req: Request): string | null {
  // 1. Bearer ํ† ํฐ (Pinafore, curl)
  const auth = req.headers.get('Authorization') || '';
  const token = auth.replace('Bearer ', '').trim();
  if (token) {
    try {
      const oauth = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(token) as any;
      if (oauth?.username) return oauth.username;
    } catch {}
  }
  // 2. refresh_token ์ฟ ํ‚ค (์›น UI)
  try {
    const viewer = getViewerFromRequest(req as any);
    if (viewer) return viewer;
  } catch {}
  return null;
}
... ...
export async function POST(req: Request) {

  let { content,visibility } = await req.json();
  // โœ… myapp48 - visibility default value
  visibility = visibility || 'public';

  // โœ… myapp45 - apply token - deleted myapp50
  // const auth = req.headers.get('Authorization') || '';
  // const token = auth.replace('Bearer ', '').trim();
  // if (token) {
  //   const oauth = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(token) as any;
  //   if (oauth?.username) username = oauth.username; // ํ† ํฐ ์žˆ์œผ๋ฉด ํ† ํฐ ์œ ์ €๋กœ ๋ฎ์–ด์“ฐ๊ธฐ!
  // }

   // โœ… myapp50 - ๋ฐ”๋”” username ์ ˆ๋Œ€ ์‹ ๋ขฐ ๊ธˆ์ง€, ์ธ์ฆ๋œ ์œ ์ €๋งŒ ์‚ฌ์šฉ
   // Never blindly trust usernames; use only verified users.
  const username = getAuthenticatedUser(req);
... ...
}

โœ”๏ธ ๊ธ€์“ฐ๊ธฐ ์‚ญ์ œ๋Š” v1/statuses/route.ts๋กœ ์„ค์ •
Post deletion is configured in v1/statuses/route.ts.

— ๊ธฐ์กด posts/route.ts๋Š” ์‚ญ์ œํ•จ.
Deleted the existing posts/route.ts.

const handleDelete = async (postId: string) => {
  if (!confirm('์‚ญ์ œํ• ๊นŒ? / Delete?')) return;
  try {
    const token = localStorage.getItem('access_token') || localStorage.getItem('token') || '';
    const res = await fetch(`/api/v1/statuses/${postId}`, { 
      method: 'DELETE',
      headers: {
        ...(token? { Authorization: `Bearer ${token}` } : {})
      },
      credentials: 'include' // refresh_token ์ฟ ํ‚ค ํฌํ•จ
    });
    if (res.ok) {
      setTimeline((prev: any[]) => prev.filter((t: any) => t.id !== postId && t.original_id !== postId));
      console.log(`๐Ÿ—‘ Delete ${postId} ok`);
    } else {
      const err = await res.json().catch(()=>({}));
      console.error('delete fail', err);
      alert(`์‚ญ์ œ ์‹คํŒจ: ${err.error || res.status}`);
    }
  } catch (e) {
    console.error(e);
    alert('์‚ญ์ œ ์‹คํŒจ / Deletion failed');
  }
};

๐Ÿ“ ํ…Œ์ŠคํŠธ / Test

โœ”๏ธ ๊ณต์œ ๊ธฐ๋Šฅ / Sharing feature

Sharing feature

โœ”๏ธ ์ด ๋กœ์ปฌ ์„œ๋ฒ„ ๋ถ€๋ถ„์€ ๋‚˜์ค‘์— ์‚ญ์ œ ๋  ์ˆ˜๋„ ์žˆ์Œ(v1/statues ์™€ ๊ธฐ๋Šฅ ์ค‘๋ณต)
This local server section might be removed later.(Functionality overlaps with v1/statues)

โœ”๏ธ ์•„๋ž˜๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์ง€๋งŒ ๋ผ์šฐํŠธ๊ฐ€ ์กด์žฌํ•˜์ง€ ๋•Œ๋ฌธ์— ํ† ํฐ ์ž…๋ ฅ์œผ๋กœ ๋ณด์•ˆ์„ค์ •์„ ๊ฐ•ํ™”ํ•ฉ๋‹ˆ๋‹ค.
Although the route below is not currently in use, it exists, so security settings are being strengthened by requiring a token for access.

โœ”๏ธ ํ† ํฐ ์—†์ด ํ„ฐ๋ฏธ๋„์—์„œ ๊ธ€์“ฐ๊ธฐ ํ•œ๊ฒฝ์šฐ(๋กœ์ปฌ ์„œ๋ฒ„)
Writing in the terminal without a token (local server)

# Terminal
curl -s -X POST https://aloy-horizon.duckdns.org/api/posts \
  -H "Content-Type: application/json" \
  -d '{"content":"No token test"}'

# response
{"error":"๋กœ๊ทธ์ธ ํ•„์š” / username required"}
                                                                                                    

โœ”๏ธ ํ† ํฐ์œผ๋กœ ๊ธ€์“ฐ๊ธฐ ํ•œ๊ฒฝ์šฐ(๋กœ์ปฌ ์„œ๋ฒ„)
Writing posts using a token (local server)

# Terminal
curl -X POST https://aloy-horizon.duckdns.org/api/posts \
  -H "Cookie: refresh_token=099b61b8616edcf48b4... ..." \
  -H "Content-Type: application/json" \
  -d '{"content":"token present"}'

# response
{"id":"f0cba853-5fa3-4f28-bc44-6a367a78d0b3","content":"token present","created_at":1790553173171,"username":"user1","visibility":"public","original_id":null,"sensitive":0}%       
UI

โœ”๏ธ ํ„ฐ๋ฏธ๋„์—์„œ ํ† ํฐ ์—†์ด ์‚ญ์ œ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ
When attempting deletion without a token in the terminal

# Terminal
curl -s -X DELETE https://aloy-horizon.duckdns.org/api/posts \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","id":"f0cba853-5fa3-4f28-bc44-6a367a78d0b3"}'

# response
{"error":"unauthorized - username required"}%      

โœ”๏ธ ํ„ฐ๋ฏธ๋„์—์„œ ํ† ํฐ์œผ๋กœ ์‚ญ์ œํ•˜๋Š” ๊ฒฝ์šฐ
When deleting using a token in the terminal

# Terminal
curl -s -X DELETE https://aloy-horizon.duckdns.org/api/posts \
 -H "Cookie: refresh_token=099b61b8616edcf48b4 ... ...." \
 -H "Content-Type: application/json" \
 -d '{"username":"user1","id":"f0cba853-5fa3-4f28-bc44-6a367a78d0b3"}'

# response
{"ok":true,"deletedId":"f0cba853-5fa3-4f28-bc44-6a367a78d0b3"}%    

์š”ํ•œ๋ณต์Œ 8์žฅ 32์ ˆ / John 8:32

“๊ทธ๋ฆฌ๊ณ  ๋„ˆํฌ๋Š” ์ง„๋ฆฌ๋ฅผ ์•Œ๊ฒŒ ๋  ๊ฒƒ์ด๋ฉฐ, ์ง„๋ฆฌ๊ฐ€ ๋„ˆํฌ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ํ•  ๊ฒƒ์ด๋‹ค.”

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

Leave a Reply