๐๐ป 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

โ๏ธ ์ด ๋ก์ปฌ ์๋ฒ ๋ถ๋ถ์ ๋์ค์ ์ญ์ ๋ ์๋ ์์(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}%

โ๏ธ ํฐ๋ฏธ๋์์ ํ ํฐ ์์ด ์ญ์ ์๋ํ ๊ฒฝ์ฐ
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”