[nextjs]SNS Server-22(myapp32)Β 

πŸ‘‰πŸ» νšŒμ›κ°€μž… 및 둜그인 κΈ°λŠ₯ κ΅¬ν˜„ 쀑 μž…λ‹ˆλ‹€.
We are currently implementing the sign-up and login functions.

πŸ‘‰πŸ» public key 검증 λ‘œμ§μ„ 계속 μ§„ν–‰ν•©λ‹ˆλ‹€.
We will proceed with the public key verification logic.

πŸ‘‰πŸ» Inboxλ₯Ό public key둜 κ²€μ¦ν•©λ‹ˆλ‹€.
Verify the Inbox using the public key.

πŸ‘‰πŸ» inbox에 μ„œλ²„μ˜ 속도 ν–₯상을 μœ„ν•΄μ„œ μΊμ‹œ κΈ°λŠ₯을 μΆ”κ°€ν•©λ‹ˆλ‹€.
A caching feature is being added to the inbox to improve server speed.

πŸ‘‰πŸ» outboxλŠ” μΊμ‹œκΈ°λŠ₯을 μ μš©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
The outbox does not utilize caching.

πŸ‘‰πŸ» pinaforeμ—μ„œ κΈ€ μž‘μ„±μ΄ κ°€λŠ₯ν•˜λ„λ‘ outbox/route.ts에 POSTν•¨μˆ˜λ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.
Add a POST function to outbox/route.ts to enable writing posts in Pinafore.

πŸ‘‰πŸ» 전체 μ½”λ“œλŠ” κΉƒν—ˆλΈŒμ—μ„œ 확인 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
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
β”‚   β”œβ”€β”€ 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


πŸ“ Inbox Public Key Cache

βœ”οΈ μ„œλ²„ 속도 ν–₯상을 μœ„ν•΄μ„œ inbox에 μΊμ‹œκΈ°λŠ₯을 μΆ”κ°€ν•©λ‹ˆλ‹€.
A caching function is being added to the inbox to improve server speed.

βœ”οΈ λ‹€λ₯Έ μ„œλ²„ 글을 빨리 받을 수 μžˆμŠ΅λ‹ˆλ‹€.
You can quickly receive posts from other servers.

— myapp32/lib/ap.ts

... ...

// βœ… myapp32 - κ³΅κ°œν‚€ 캐싱 / Public key caching
const publicKeyCache = new Map<string, { key: string, cachedAt: number }>();
const CACHE_TTL = 1000 * 60 * 60 * 1; // 1μ‹œκ°„! (λΉ λ₯΄λ©΄μ„œ μ΅œμ‹ !) / 1 hour! (fast and up-to-date!)

... ...
export async function fetchActorPublicKeyCached(actorUrl: string, username: string): Promise<string> { ... }

export async function fetchActorPublicKeyWithRetry(
  actorUrl: string, 
  username: string, 
  verifyReq: Request
): Promise<{ publicKey: string, isValid: boolean }> { ... }

βœ”οΈ inbox에 μΊμ‹œ 적용
Apply caching to the inbox.

— myapp32/app/users/[username]/inbox/route.ts

import { sendAccept, verifyHttpSignature, fetchActorPublicKey, fetchActorPublicKeyWithRetry } from '@/lib/ap';
export async function POST(req: Request, { params }: { params: Promise<{ username: string }> }) { 
 ... ...
        if (body.type === 'Delete') {
        console.log(`πŸ—‘ DeleteλŠ” 검증 μŠ€ν‚΅! / Skip validation for Delete! ${actorIdForVerify}`);
      } 
      else if (!actorIdForVerify) {
        console.error('❌ actor μ—†μŒ! 검증 μŠ€ν‚΅!/ No actor! Skip verification!', body);
      } else {
... ...
          const verifyReq = new Request(req.url, {
            method: req.method,
            headers: req.headers,
            body: rawBody
          });
... ...          
        
      // ❗️ fetchActorPublicKeyWithRetry에 포함됨 / Included in fetchActorPublicKeyWithRetry
        // const isValid = await verifyHttpSignature(verifyReq, publicKeyPem); 

        // βœ… myapp32 - 캐싱 + μž¬μ‹œλ„! / Caching + Retry!
        const { isValid } = await fetchActorPublicKeyWithRetry(
            actorIdForVerify, 
            username, 
            verifyReq
        );
... ...        
      }
}

πŸ“ Outbox Public Key 검증
Outbox Public Key Verification

βœ”οΈ OutboxλŠ” μΊμ‹œλ₯Ό μ μš©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
The Outbox does not use caching.

πŸ’‘ inboxλŠ” λ‚΄ μ„œλ²„μ— 글이 λ“€μ–΄μ˜¬ λ•Œ μƒλŒ€λ°© μ„œλ²„κ°€ 본인의 private key둜 μ„œλͺ…ν•΄μ„œ 보내면 λ‚΄ μ„œλ²„κ°€ κΈ€ λ³΄λ‚΄λŠ” μ„œλ²„μ— public keyλ₯Ό μš”μ²­ν•©λ‹ˆλ‹€. 두 ν‚€κ°€ 맞으면 κΈ€ λ°›κΈ°λ₯Ό ν—ˆμš©ν•©λ‹ˆλ‹€.

With the inbox mechanism, when a message arrives at my serverβ€”signed by the sender's server using its private keyβ€”my server requests the public key from the sending server. If the keys match, the message is accepted.

πŸ’‘ μΊμ‹œλ₯Ό μ μš©ν•˜λ©΄ λ‚΄ μ„œλ²„μ— μƒλŒ€λ°© μ„œλ²„μ˜ publick keyκ°€ μ €μž₯λ˜μ–΄ μžˆμœΌλ―€λ‘œ μƒλŒ€λ°© μ„œλ²„μ— public key μš”μ²­ νšŸμˆ˜κ°€ μ€„μ–΄λ“­λ‹ˆλ‹€. κ·Έλž˜μ„œ κΈ€ λ°›κΈ° 속도가 ν–₯μƒλ©λ‹ˆλ‹€.

Implementing caching stores the other server's public key on your server, thereby reducing the number of requests made to the other server for that key. This results in faster content retrieval speeds.

πŸ’‘ outboxμ—μ„œ POSTλŠ” λ°˜λŒ€λ‘œ λ‚΄κ°€ μž‘μ„±ν•œ 글을 μƒλŒ€λ°© μ„œλ²„μ— λ³΄λ‚΄λŠ” κΈ°λŠ₯을 μˆ˜ν–‰ν•©λ‹ˆλ‹€.

In the outbox, the POST method performs the function of sending a message I have written to the recipient's server.

πŸ’‘ 글을 λ³΄λ‚΄λŠ”κ±΄ λ‚΄ μ„œλ²„μ˜ 속도와 κ΄€λ ¨ μ—†μœΌλ‹ˆ μΊμ‹œλ₯Ό μ μš©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

Sending the content is unrelated to my server's speed, so caching is not applied.

βœ”οΈ POST ν•¨μˆ˜μΆ”κ°€ / Add POST function

— myapp32/app/users/[username]/outbox/route.ts

// βœ… POST μΆ”κ°€! Pinaforeκ°€ μ—¬κΈ°λ‘œ κΈ€ 씀! / Added POST! Pinafore writes here!
export async function POST(req: Request, { params }: { params: Promise<{ username: string }> }) { ... }

βœ”οΈ GETν•¨μˆ˜μ— username ν•„ν„° μΆ”κ°€
Add a username filter to the GET function.

export async function GET(_req: Request, { params }: { params: Promise<{ username: string }> }) {
  const { username } = await params;
    // βœ… myap32 - username ν•„ν„° μΆ”κ°€ / Added username filter
    const posts = db.prepare('SELECT * FROM posts WHERE username = ? ORDER BY created_at DESC LIMIT 20').all(username) as any[];
    
  try {
} catch (e) { }

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

βœ”οΈ /api/posts λΌμš°ν„°λŠ” λ‚΄ UIμ „μš© κΈ€μ“°κΈ° 라우트
The /api/posts router is the route dedicated to post creation for my UI.

— 글을 μž‘μ„±ν• λ•Œ μ•„λž˜μ˜ 라우트둜 μ‚¬μš©ν•©λ‹ˆλ‹€.
Use the route below when writing a post.

curl -X POST https://aloy-horizon.duckdns.org/api/posts \
  -H "Content-Type: application/json" \
  -d '{"username":"user1","content":"Hello local UI #apiposts"}'

βœ”οΈ /users/[username]/outbox λΌμš°ν„°λŠ” activityPub ν‘œμ€€λΌμš°νŠΈλ‘œ κΈ€ μž‘μ„± ν•  λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€.
The /users/[username]/outbox route handles post creation in accordance with the ActivityPub standard.

— pinaforeλ“±μ—μ„œ κΈ€ μž‘μ„± ν• λ•Œ μ‚¬μš©ν•¨.
Used when writing posts on platforms like Pinafore.

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 "}}'

βœ”οΈ λ‚΄κ°€ μž‘μ„±ν•œ κΈ€ λͺ©λ‘
List of posts I’ve written

https://aloy-horizon.duckdns.org/users/user1/outbox

βœ”οΈ 터미널 ν…ŒμŠ€νŠΈ / Terminal test

— 둜컬 UI,pinaforeμ—μ„œ κΈ€ μž‘μ„± ν•  λ•Œ μ‚¬μš©ν•  라우트
Route to be used when composing a post in the local UI, Pinafore.

# api/posts POST ν„°λ―Έλ„μ—μ„œ μ‹€ν–‰ (둜컬 UI μ „μš©)
# Execute api/posts POST request in the terminal (local UI only)

% curl -X POST https://aloy-horizon.duckdns.org/api/posts \
  -H "Content-Type: application/json" \
  -d '{"username":"user1","content":"Hello local UI #apiposts"}'

# 응닡 / response

{"id":"aefc7ff8-5b38-4fb7-829d-9b14343cec6a","content":"Hello local UI #apiposts","created_at":"2026-09-05 03:39:37","username":"user1"}%

% 
% 

# users/[username]/outbox POST ν„°λ―Έλ„μ—μ„œ μ‹€ν–‰(ActivityPub μ „μš©)
# Execute POST to users/[username]/outbox in the terminal (ActivityPub only)

% 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"}}'

# 응닡 / response

{"@context":"https://www.w3.org/ns/activitystreams","id":"https://aloy-horizon.duckdns.org/users/user1/posts/4ace7a75-3a40-494f-8dea-bf3f688b6cfb#activity","type":"Create","actor":"https://aloy-horizon.duckdns.org/users/user1","object":{"id":"https://aloy-horizon.duckdns.org/users/user1/posts/4ace7a75-3a40-494f-8dea-bf3f688b6cfb","type":"Note","attributedTo":"https://aloy-horizon.duckdns.org/users/user1","content":"Hello Activity UI #activityposts","published":"2026-09-05T03:39:48.980Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://aloy-horizon.duckdns.org/users/user1/followers"]}}

%    

βœ”οΈ λ§ˆμŠ€νŠΈλˆμ— κΈ€ 도착 확인
Confirmed receipt of post on Mastodon.

mastodon.social

βœ”οΈ gotosocial에 κΈ€ 도착 확인
Confirming receipt of post on GoToSocial

gotosocial

βœ”οΈ μ„œλ²„ 둜그 / Server Log

Server Log

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

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

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

Leave a Reply