๐๐ป myapp45์์๋ Tursky๋ก๊ทธ์ธ ๋ถ๋ถ์ ๊ตฌํํฉ๋๋ค.
In myapp45, the Tursky login functionality is implemented.
๐๐ป ์น์ด๋ pinafore์์ ์ ์์ ์ผ๋ก ๋ชจ๋ ๋ฐ์ดํฐ๊ฐ ํ์๋ฉ๋๋ค.
All data is displayed correctly on the web or in Pinafore.
๐๐ป ํ์ง๋ง Tusky์ฑ ํ์๋ผ์ธ์ ๋ฐ์ดํฐ๊ฐ ์ ์์ ์ผ๋ก ํ์๋์ง ์๋ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
However, there is an issue where data is not displaying correctly on the Tusky app’s timeline.
๐๐ป ์ถํ ์ดํด ๋ณด๋๋ก ํ๊ณ ์ฌ๊ธฐ์๋ ์ฑ์ ๋ก๊ทธ์ธ ์๋ฃํ๊ณ 404์ค๋ฅ ์์ ๋ ๋ถ๋ถ๊น์ง๋ง ์๋ฃํ๊ฒ ์ต๋๋ค.
We will look into that later; for now, we will just complete the steps of logging into the app and resolving the 404 error.
๐๐ป ์ ์ฒด ์ฝ๋๋ ๊นํ๋ธ์์ ํ์ธ ํ ์ ์์ต๋๋ค.
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
โ โโโ api/v1/instance/rules/route.ts
โ โโโ api/v1/instance/domain_blocks/route.ts
โ โโโ api/v1/filters/route.ts
โ โโโ api/v1/lists/route.ts
โ โโโ api/v1/announcements/route.ts
โ โโโ api/v1/timelines/public/route.ts
โ โโโ oauth/authorize/route.ts -> auth
โ โโโ oauth/token/route.ts -> auth
โ โโโ oauth/login-and-authorize/route.ts -> Tursky,Pinafore login processing
โ โโโ oauth/revoke/route.ts
โ โโโ api/v1/search/home/route.ts -> search
โ โโโ api/v2/search/home/route.ts -> search
โ โโโ api/v2/notifications/policy/route.ts
โ โโโ 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 -> Website Signup
โ โโโ api/auth/login/route.ts -> Website Login
โ โโโ api/auth/logout/route.ts -> Website 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
โ โ โโโ pinafore/TimelineList.tsx -> Theme 1, TimelineList
โ โ โโโ 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
โ๏ธ Tusky์ฑ์ ๋ก๊ทธ์ธ ๊ตฌํ / Implementing login for the Tusky app
1) app/oauth/authorize/route.ts
A. ํ์ฌ ๋ชจ๋ฐ์ผ์ฑ๊ณผ ์น์ฌ์ดํธ์ธ๊ฒฝ์ฐ ๋์ผํ ์ฝ๋๋ก ์๋ํ์ง ์๊ธฐ ๋๋ฌธ์ ๋ถ๋ฆฌ์ ์ฉ
Currently, the mobile app and the website do not operate on the same code, so they are implemented separately.
B. fetch๋ฐฉ์์ด ์๋ methd=POST ๋ฐฉ์ ์ ์ฉ
Applied the POST method instead of the fetch method.
— ๋ก๊ทธ์ธ ํ์ด์ง๋ก(/auth/login/page.tsx)์ด๋ํ์ง ์๊ณ ์ด ๋ผ์ฐํธ์์ ๋ก๊ทธ์ธ ํ์ด์ง ๋ณด์ฌ์ฃผ๊ธฐ
Display the login page on this route without navigating to the login page (/auth/login/page.tsx).
// โ
for CORS error
// if (!username) {
// const loginUrl = new URL('/auth/login', ORIGIN); // โ
๋๋ฉ์ธ, not loacalhost / Domain, not localhost
// const realNextUrl = `${ORIGIN}${url.pathname}${url.search}`; // โ
Use the domain for Next.js, not localhost.
// loginUrl.searchParams.set('next', realNextUrl);
// return NextResponse.redirect(loginUrl.toString());
// }
// โ
myapp55 ์์ - ๋ก๊ทธ์ธ ์ ๋์ผ๋ฉด /auth/login์ผ๋ก ์ด๋ํ์ง ๋ง๊ณ ์ฌ๊ธฐ์ ๋ก๊ทธ์ธ ํผ ๋ณด์ฌ์ฃผ๊ธฐ
// If not logged in, do not redirect to /auth/login, but show the login form here.
if (!username) {
const html = `
<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login</title>
<style>body{font-family:sans-serif;max-width:400px;margin:50px auto;padding:20px}input{width:90%;padding:10px;margin:8px 0;font-size:16px}button{width:100%;padding:12px;background:#6364ff;color:white;border:0;border-radius:8px;font-size:16px}</style>
</head><body>
<h2>๐ ๋ก๊ทธ์ธ ํ์(OAuth)</h2>
<p>${clientId} ์ฑ ์น์ธ ์ํด ๋ก๊ทธ์ธ</p>
<form method="POST" action="/oauth/login-and-authorize">
<input type="hidden" name="client_id" value="${clientId}">
<input type="hidden" name="redirect_uri" value="${redirectUri}">
<input type="hidden" name="scope" value="${scope}">
<input name="username" placeholder="username" required>
<input name="password" type="password" placeholder="password" required>
<button type="submit">๋ก๊ทธ์ธ & ์น์ธ</button>
</form>
</body></html>`;
return new NextResponse(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
}
โ๏ธapp/oauth/login-and-authorize/route.ts – ๋ผ์ฐํธ ์ถ๊ฐ / Add route
— Tursky์ฑ์ ๋ก๊ทธ์ธ์ ์ํ ๋ผ์ฐํธ,๋ก๊ทธ์ธ ์ฒ๋ฆฌ
Route and login handling for the Tursky app
— api/auth/login/route.ts๋ ์น์ฌ์ดํธ์ฉ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
api/auth/login/route.ts handles login for the website.
... ...
export async function POST(req: Request) {
... ...
// - Tusky๋ HTML+JS๋ก, ๊ทธ ์ธ๋ 302 / Tusky uses HTML+JS, others use 302
let html: string;
if (finalUrl.startsWith('oauth2redirect://') || finalUrl.includes('tusky')) {
html = `<!DOCTYPE html><html><body style="text-align:center;padding:50px"><h2>โ
${user.username} ์น์ธ๋จ</h2><p><a href="${finalUrl}">${finalUrl}</a></p><script>setTimeout(()=>{window.location.href="${finalUrl}"},300)</script></body></html>`;
} else {
html = `<!DOCTYPE html><html><body><script>window.location.href="${finalUrl}"</script><a href="${finalUrl}">${finalUrl}</a></body></html>`;
}
const res = new NextResponse(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
res.cookies.set('token', accessToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 60*15, path: '/' });
res.cookies.set('refresh_token', refreshToken, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 60*60*24*7, path: '/' });
return res;
}
โ๏ธ 404 ์ค๋ฅ ๋ฉ์ธ์ง ์ฒ๋ฆฌ
1)app/api/v1/instance/rules/route.ts – ๋ผ์ฐํธ ์ถ๊ฐ / Add route
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json([], { headers: { 'Access-Control-Allow-Origin': '*' } });
}
2)app/api/v1/instance/domain_blocks/route.ts – ๋ผ์ฐํธ ์ถ๊ฐ / Add route
app/api/v1/instance/rules/route.ts ์ฝ๋์ ๋์ผ
Identical to the code in app/api/v1/instance/rules/route.ts
3)app/oauth/revoke/route.ts – ๋ผ์ฐํธ ์ถ๊ฐ / Add route
A.Tusky ๋ก๊ทธ์์ ๋ ํธ์ถ / Add route (called upon Tusky logout)
import { NextResponse } from 'next/server';
import db from '@/lib/db';
export async function POST(req: Request) {
try {
const body = await req.json().catch(() => ({})) as any;
const token = body.token;
if (token) {
try {
db.prepare(`DELETE FROM oauth_tokens WHERE access_token=?`).run(token);
db.prepare(`DELETE FROM sessions WHERE access_token=?`).run(token);
} catch {}
}
} catch {}
return NextResponse.json({});
}
export async function GET() { return NextResponse.json({}); }
4)app/api/v2/notifications/policy/route.ts – ๋ผ์ฐํธ ์ถ๊ฐ / Add route
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ for_not_following: 'accept', for_not_followers: 'accept', for_new_accounts: 'accept', for_private_mentions: 'accept', for_limited_accounts: 'accept' });
}
5)public/icon.png – ํ์ผ์ถ๊ฐ / add file
6) app/api/v1/filters/route.ts – ๋ผ์ฐํธ ์ถ๊ฐ / Add route
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json([], { headers: { 'Access-Control-Allow-Origin': '*' } });
}
7)app/api/v1/lists/route.ts – ๋ผ์ฐํธ ์ถ๊ฐ / Add route
app/api/v1/instance/rules/route.ts ์ฝ๋์ ๋์ผ
Identical to the code in app/api/v1/instance/rules/route.ts
8)app/api/v1/announcements/route.ts – ๋ผ์ฐํธ ์ถ๊ฐ / Add route
app/api/v1/instance/rules/route.ts ์ฝ๋์ ๋์ผ
Identical to the code in app/api/v1/instance/rules/route.ts
9)app/api/v1/timelines/public/route.ts – ๋ผ์ฐํธ ์ถ๊ฐ / Add route
app/api/v1/instance/rules/route.ts ์ฝ๋์ ๋์ผ
Identical to the code in app/api/v1/instance/rules/route.ts
10)app/api/v1/notifications/route.ts – ์ฝ๋์์ / Code Modification
A.๋ผ์ฐํธ ๋ฌดํ ํธ์ถ ๋ฉ์ถ๊ธฐ / Stop infinite route calls
B. /api/v1/notifications ๋ฌดํ ํธ์ถ ์๋ฌ ๋ฉ์ธ์ง
likes table rows=0 for user1
๐ notifications return=2 follow=2 fav=0 reblog=0
GET /api/v1/notifications?max_id=follow_1_https%3A%2F%2Ffreelifemakers.com%2Fusers%2Fuser1%2Ffollow%2F01HW187GFP316GG213ASB2DG3T&limit=30 200 in 16ms (next.js: 13ms, proxy.ts: 0.8ms, application-code: 3ms)
GET /icon.png 404 in 25ms (next.js: 16ms, application-code: 10ms)
โค likes table rows=0 for user1
๐ notifications return=2 follow=2 fav=0 reblog=0
GET /api/v1/notifications?limit=30 200 in 4ms (next.js: 0.9ms, proxy.ts: 1.0ms, application-code: 1.7ms)
โค likes table rows=0 for user1
C. ์ฝ๋ / Code
... ...
export async function GET(req: Request) {
... ...
// โ
myapp55 - Tusky๊ฐ max_id๋ก ๋ค์ ํ์ด์ง ๋ฌ๋ผ๊ณ ํ๋ฉด ๋น ๋ฐฐ์ด ๋ฆฌํด = ๋
// Tusky๊ฐ max_id๋ก ๋ค์ ํ์ด์ง ๋ฌ๋ผ๊ณ ํ๋ฉด ๋น ๋ฐฐ์ด ๋ฆฌํด = ๋
// Tursky requests the next page with max_id, return an empty array = end
const max_id = searchParams.get('max_id');
if (max_id) {
return NextResponse.json([], {
headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' }
});
}
... ...
}
... ...
11) app/api/v1/markers/route.ts – ๋ผ์ฐํธ ์ถ๊ฐ / Add route
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({
notifications: { last_read_id: "0", version: 1, updated_at: new Date().toISOString() },
home: { last_read_id: "0", version: 1, updated_at: new Date().toISOString() }
}, { headers: { 'Access-Control-Allow-Origin': '*' } });
}
export async function POST(req: Request) {
const body = await req.json().catch(()=>({}));
return NextResponse.json(body, { headers: { 'Access-Control-Allow-Origin': '*' } });
}
12)app/api/v1/timelines/home/route.ts – ์ฝ๋์์ / code modification
// โ
myapp55 - ๊ณ์ ์ ๋ณด ๊ตฌ์ฑ / Account information configuration
const baseAccount = isMy
? {
id: '1', username, acct: `${username}@${DOMAIN}`, display_name: username,
avatar: `https://${DOMAIN}/icon.png`,
avatar_static: `https://${DOMAIN}/icon.png`,
header: `https://${DOMAIN}/header.png`,
header_static: `https://${DOMAIN}/header.png`,
}
: (() => {
let uname = 'user1', dom = 'freelifemakers.com', display = 'user1';
if (row.actor?.includes('mastodon.social')) { uname = 'freelifemakers'; dom = 'mastodon.social'; display = 'freelifemakers'; }
return {
id: `remote_${dom.replace(/\./g,'_')}_${uname}`,
username: uname, acct: `${uname}@${dom}`, display_name: display,
avatar: `https://${DOMAIN}/icon.png`,
avatar_static: `https://${DOMAIN}/icon.png`,
header: `https://${DOMAIN}/header.png`,
header_static: `https://${DOMAIN}/header.png`,
};
})();
return {
id: String(row.id),
uri: longId,
url: longId,
account: baseAccount,
content: content.startsWith('<')? content : `<p>${content}</p>`,
created_at: new Date(parseDate(row.created_at)).toISOString(),
visibility: 'public',
reblogs_count, // ์ ์ฒด ๋ถ์คํธ ์นด์ดํธ / Full boost count
favourites_count, // ์ ์ฒด ์ข์์ ์นด์ดํธ / Full like count
replies_count: 0,
favourited: !!favourited, // ์ ์ฒด ์ข์์ / Full like
reblogged: !!reblogged, // ์ ์ฒด ๋ถ์คํธ / Full Boost
muted: false, bookmarked: false, pinned: false,
// โ
myapp55 - Tusky ํ์ / Required for Tusky
sensitive: false, spoiler_text: '',
media_attachments: [],
mentions: [],
tags: [],
emojis: [],
card: null,
poll: null,
reblog: null,
};
๐ ํ ์คํธ / Test
โ๏ธ ์๋ฒ๋ก๊ทธ์์ 404 ์๋ฌ ํ์ธ / Check for 404 errors in the server logs.

โ๏ธ ์ธ๋ถ ์ฑ ๋ก๊ทธ์ธํผ / External App Login Form

โ๏ธ ์ธ๋ถ ์ฑ ๋ก๊ทธ์ธ ํผ – ๋น๋ฐ๋ฒํธ ํ๋ฆฐ๊ฒฝ์ฐ / External App Login Form โ Wrong Password

โ๏ธ Tusky์ฑ ๋ก๊ทธ์ธ ๋ฐ ํ๋กํ ํ์ธ / Logging into the Tusky app and checking your profile

์ํ๋ณต์ 8์ฅ 32์ / John 8:32
“๊ทธ๋ฆฌ๊ณ ๋ํฌ๋ ์ง๋ฆฌ๋ฅผ ์๊ฒ ๋ ๊ฒ์ด๋ฉฐ, ์ง๋ฆฌ๊ฐ ๋ํฌ๋ฅผ ์์ ๋กญ๊ฒ ํ ๊ฒ์ด๋ค.”
“Then you will know the truth ,and the truth will set you free”