[nextjs]SNS Server-43(myapp53)Β 

πŸ‘‰πŸ» myapp53μ—μ„œλŠ” λΆ€μŠ€νŠΈμ™€ μ’‹μ•„μš” λΆ€λΆ„ λ³΄μ•ˆμ„ κ°•ν™”ν•©λ‹ˆλ‹€.
myapp53 is strengthening security for the Boost and Like features.

πŸ‘‰πŸ» 둜그인 ν•˜μ§€ μ•ŠμœΌλ©΄ λΆ€μŠ€νŠΈλ‚˜ μ’‹μ•„μš”λ₯Ό ν΄λ¦­ν•˜λ©΄ λ‘œκ·ΈμΈν™”λ©΄μœΌλ‘œ μ΄λ™ν•©λ‹ˆλ‹€.
If you are not logged in, clicking “Boost” or “Like” will redirect you to the login screen.

πŸ‘‰πŸ» μ‚¬μš©ν•˜μ§€μ•ŠλŠ” getTokenν•¨μˆ˜λ₯Ό μ‚­μ œν•©λ‹ˆλ‹€.
Remove the unused getToken function.

πŸ‘‰πŸ» 둜컬 μ„œλ²„μ—μ„œ κΈ€ μž‘μ„±ν•  λ•Œ 첨뢀 파일이 μžˆλŠ” 경우 μ²¨λΆ€νŒŒμΌ λ¦¬μŠ€νŠΈκ°€ μ΄ˆκΈ°ν™” λ˜μ§€ μ•ŠλŠ” 문제λ₯Ό μˆ˜μ •ν•©λ‹ˆλ‹€.
Fixes an issue where the attachment list was not reset when creating a post with attachments on a 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(deprecated)
β”‚   β”œβ”€β”€ 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
β”‚   β”œβ”€β”€ api/v1/media/route.ts -> Image upload
β”‚   β”œβ”€β”€ api/v1/media/[id]/route.ts  -> Image Delete 
β”‚   β”œβ”€β”€ 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

βœ”οΈ 버그 μˆ˜μ • / Bug fixes

— κΈ€ μž‘μ„± ν›„ 이미지 첨뢀 λ¦¬μŠ€νŠΈμ™€ 프리뷰λ₯Ό μ΄ˆκΈ°ν™” ν•©λ‹ˆλ‹€.
After posting, the list of attached images and the preview are reset.

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

const handlePost = async () => {
  if (!composeText.trim() || posting) return;
  setPosting(true);
  try {
    // 토큰 있으면 헀더에 λ„£κ³  μ—†μœΌλ©΄ μΏ ν‚€ 인증으둜
    // Include the token in the header if present; otherwise, use cookie authentication.
    const token = localStorage.getItem('access_token') || localStorage.getItem('token') || '';
    const res = await fetch('/api/v1/statuses', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
         ...(token? { Authorization: `Bearer ${token}` } : {})
      },
      credentials: 'include',
      body: JSON.stringify({ 
        status: composeText, 
        visibility: composeVis, 
        media_ids: mediaIds // βœ… myapp51 - add media_ids
      })
    });
    if (res.ok) {
      const newPost = await res.json();
      setComposeText('');
      setMediaIds([]); // βœ… myapp53 - 첨뢀 리슀트 μ΄ˆκΈ°ν™” / Reset attachment list
      setPreviews([]); // βœ… myapp53 - 프리뷰 μ΄ˆκΈ°ν™” / Reset Preview
      // νƒ€μž„λΌμΈ 맨 μœ„μ— μ¦‰μ‹œ μΆ”κ°€ / Add immediately to the top of the timeline
      setTimeline((prev: any[]) => [{
        id: newPost.id, content: newPost.content, actor: `https://${DOMAIN}/users/${username}`,
        username: username, 
        created_at: new Date().toISOString(), 
        source: 'local', 
        isMine: true, 
        visibility: composeVis,
        media_attachments: newPost.media_attachments || [] // βœ… myap51 - preview
      },...prev]);
      setCounts(c => ({...c, posts: c.posts + 1}));
    } else alert('κ²Œμ‹œ μ‹€νŒ¨');
    } catch (e) { console.error(e); alert('κ²Œμ‹œ μ‹€νŒ¨'); }
  setPosting(false);
  
};

βœ”οΈ λΆ€μŠ€νŠΈ λ³΄μ•ˆκ°•ν™” / Boost Security Enhancement

— λ‘œκ·ΈμΈν•˜μ§€ μ•ŠμœΌλ©΄ λΆ€μŠ€νŠΈ κΈ°λŠ₯을 μ‚¬μš©ν•  수 없도둝 ν•©λ‹ˆλ‹€.
Disable the use of the boost function if the user is not logged in.

A.app/api/announce/route.ts – λ°±μ—”λ“œ / Backend

... ...
function getUser(req: Request) {
  const auth = req.headers.get('Authorization')?.replace('Bearer ','').trim();
  if (auth) {
    const o = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(auth) as any;
    if (o?.username) return o.username;
  }
  const cookie = req.headers.get('cookie') || '';
  const m = cookie.match(/refresh_token=([^;]+)/);
  if (m) {
    const s = db.prepare('SELECT username FROM sessions WHERE refresh_token=?').get(m[1]) as any;
    if (s?.username) return s.username;
  }
  return null;
}

export async function POST(req: NextRequest) {
  try {
    //const { username, target } = await req.json();
    const authUser = getUser(req);
    if (!authUser) {
      return NextResponse.json({ error: '둜그인 ν•„μš” / authentication required' }, { status: 401 });
    }
    const { target } = await req.json(); 
    const username = authUser; 
... ...
}

export async function DELETE(req: NextRequest) {
  try {
    // βœ… myapp53 - λΆ€μŠ€νŠΈ μ‚­μ œ λ³΄μ•ˆκ°•ν™” / Boost Removal & Security Enhancement
    //const { username, target } = await req.json();
    const authUser = getUser(req);
    if (!authUser) {
      return NextResponse.json({ error: '둜그인 ν•„μš”/ authentication required' }, { status: 401 });
    }
    const { target } = await req.json();
    const username = authUser;
... ...
}

B.app/usersui/[username]/_components/themes/pinafore/PinaforeTheme.tsx – ν”„λ‘ νŠΈμ—”λ“œ / Frontend

a.λΆ€μŠ€νŠΈ λ³΄μ•ˆκ°•ν™” / Boost Security Enhancement

  ... ...
  // βœ… myapp53-boost-secure: λΆ€μŠ€νŠΈ λ³΄μ•ˆκ°•ν™” Boost Security Enhancement
  const handleBoostClick = async (p: any) => {
    if (!currentUser) return window.location.href = `/auth/login?next=/@${username}`;
    onBoost(p)
  };

  // βœ… myapp53-boost-secure: μ’‹μ•„μš” λ³΄μ•ˆκ°•ν™” / Like Security Enhancement
  const handleLikeClick = (p: any) => {
    if (!currentUser) return window.location.href = `/auth/login?next=/@${username}`;
    onLike(p);
  };
... ...

b.λ²„κ·Έμˆ˜μ •(μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” getTokenν•¨μˆ˜ λΆ€λΆ„) / Bug fix (unused getToken function)

... ...
// βœ… deprecated(myapp53)-myapp51 - ν† ν°κ°€μ Έμ˜€κ°€ , url κ²½λ‘œλ³΄μ • / Retrieve token, adjust URL path
// βœ… myapp53 - 토큰 제거, μΏ ν‚€ 인증만 μ‚¬μš© / cookie auth only
//const getToken = () => localStorage.getItem('access_token') || localStorage.getItem('token') || '';
... ...
 // βœ… myapp53 - cookie auth only
 //const token = localStorage.getItem('access_token') || localStorage.getItem('token') || '';
const handleDelete = async (postId: string) => {
... ...
    const res = await fetch(`/api/v1/statuses/${postId}`, { 
      method: 'DELETE',
      // headers: {
      //   ...(token? { Authorization: `Bearer ${token}` } : {})
      // },
      credentials: 'include' // refresh_token μΏ ν‚€ 포함 / Include refresh_token cookie
    });
... ...
}
... ...
const handlePost = async () => {
... ...
    // βœ… myapp53 - cookie auth only
    const res = await fetch('/api/v1/statuses', {
      method: 'POST',
      // headers: {
      //   'Content-Type': 'application/json',
      //    ...(token? { Authorization: `Bearer ${token}` } : {})
      // },
      credentials: 'include',
      body: JSON.stringify({ 
        status: composeText, 
        visibility: composeVis, 
        media_ids: mediaIds // βœ… myapp51 - add media_ids
      })
    });
... ...
    if (res.ok) {
      const newPost = await res.json();
      setComposeText('');
      setMediaIds([]); // βœ… myapp53 - 첨뢀 리슀트 μ΄ˆκΈ°ν™” / Reset attachment list
      setPreviews([]); // βœ… myapp53 - 프리뷰 μ΄ˆκΈ°ν™” / Reset Preview
... ...
    }
... ...
}

async function onFiles(e: React.ChangeEvent<HTMLInputElement>) {
    // βœ… myapp53 - Cookie auth only
    //const token = getToken();
... ...
        const res = await fetch('/api/v1/media', {
          method: 'POST',
          body: fd,
          //headers: {...(token? { Authorization: `Bearer ${token}` } : {}) },// βœ… myapp53 - Cookie auth only
          credentials: 'include'
        });
... ...
}

βœ”οΈ μ’‹μ•„μš” λ³΄μ•ˆκ°•ν™” / Like Security Enhancement

— λ‘œκ·ΈμΈν•˜μ§€ μ•ŠμœΌλ©΄ μ’‹μ•„μš” κΈ°λŠ₯을 μ‚¬μš©ν•  수 없도둝 ν•©λ‹ˆλ‹€.
Disable the ‘like’ function for users who are not logged in.

A.app/api/announce/route.ts – λ°±μ—”λ“œ / Backend

... ...
function getUser(req: Request) {
  const auth = req.headers.get('Authorization')?.replace('Bearer ','').trim();
  if (auth) {
    const o = db.prepare('SELECT username FROM oauth_tokens WHERE access_token=?').get(auth) as any;
    if (o?.username) return o.username;
  }
  const cookie = req.headers.get('cookie') || '';
  const m = cookie.match(/refresh_token=([^;]+)/);
  if (m) {
    const s = db.prepare('SELECT username FROM sessions WHERE refresh_token=?').get(m[1]) as any;
    if (s?.username) return s.username;
  }
  return null;
}

export async function POST(req: NextRequest) {
  //const { username, target } = await req.json();
  // βœ… myapp53 - μ’‹μ•„μš” λ³΄μ•ˆκ°•ν™” / like Security Enhancement
  //const { username, target } = await req.json();
  const authUser = getUser(req);
  if (!authUser) {
    return NextResponse.json({ error: '둜그인 ν•„μš” / authentication required' }, { status: 401 });
  }
  const { target } = await req.json();
  const username = authUser;
... ...
}

export async function DELETE(req: NextRequest) {

  // βœ… myapp53 - μ’‹μ•„μš” λ³΄μ•ˆκ°•ν™” / like Security Enhancement
  //const { username, target } = await req.json();
  const authUser = getUser(req);
  if (!authUser) {
    return NextResponse.json({ error: '둜그인 ν•„μš” / authentication required' }, { status: 401 });
  }
  const { target } = await req.json();
  const username = authUser;
... ...
}

B.app/usersui/[username]/_components/themes/pinafore/PinaforeTheme.tsx – ν”„λ‘ νŠΈμ—”λ“œ / Frontend

— μœ„μ˜ λΆ€μŠ€νŠΈ λ³΄μ•ˆκ°•ν™” λΆ€λΆ„μ˜ μ½”λ“œλ₯Ό μ°Έμ‘°ν•˜μ„Έμš”
Please refer to the code in the Boost security enhancement section above.

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

βœ”οΈ μ²¨λΆ€νŒŒμΌ 리슀트 μ΄ˆκΈ°ν™” ν…ŒμŠ€νŠΈ
Attachment list reset test

Attachment list reset

βœ”οΈ λ‘œκ·Έμ•„μ›ƒμ‹œ μ’‹μ•„μš”,λΆ€μŠ€νŠΈ ν΄λ¦­ν•˜λ©΄ 둜그인 νŽ˜μ΄μ§€λ‘œ 이동
If you click “Like” or “Boost” while logged out, you will be redirected to the login page.

βœ”οΈ λ‘œκ·ΈμΈμ‹œ μ’‹μ•„μš”,λΆ€μŠ€νŠΈ μž‘λ™ν™•μΈ
Verify that ‘Like’ and ‘Boost’ functions work upon login.

— Local Server

Local Server

— GTS Server

GTS Server

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

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

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

Leave a Reply