ππ» 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”