๐๐ป gotosocial์ ๊ธ์ฐ๊ธฐ UI๋ฅผ ์ง์๋์ง ์์ต๋๋ค.
GoToSocial does not support a post-writing UI.
๐๐ป ๊ทธ๋์ ๊ธ์ ์์ฑํ๊ณ ์์ ํ๊ณ ์ญ์ ํ๊ธฐ์ํด์๋ ๋ณ๋์ ์ฑ์ด๋ ์น์ฌ์ดํธ๋ฅผ ์ด์ฉํด์ผ ํฉ๋๋ค.
Therefore, you need to use a separate app or website to create, edit, and delete posts.
๐๐ป Tusky(๋ชจ๋ฐ์ผ)๋ Pinafore(์น์ฌ์ดํธ)๋ฑ์ ์ด์ฉํ๋ฉด ๊ธ์ ์์ฑํ ์ ์์ต๋๋ค.
You can write posts using apps like Tusky(mobile) or Pinafore.(website)
๐๐ป ์ฌ๊ธฐ์๋ nextjs์ gotosocial์๋ฒ์ nextauth์ธ์ฆ๊ณผ ์๋ฒ์ ๊ธ์ ์ฝ๊ณ ์ญ์ ํ๋ ๊ธฐ๋ฅ์ ๋ํด์ ์ดํด ๋ด
๋๋ค.
Here, we examine NextAuth authentication for the GoToSocial server within a Next.js application, as well as the functionality for reading and deleting posts on the server.
๐ ํ๋ก์ ํธ ๊ตฌ์กฐ / Project Structure
myapp9/
โโ auth.ts <- ์ธ์ฆ ์ค์ / Authentication Settings
โโ app/api/auth/[...nextauth]/route.ts <- ์ธ์ฆ ์คํ / Authentication Settings
โโ lib/gotosocial.ts <- ํ์ ๋ผ์ธ ์ฝ๊ธฐ / Read Timeline
โโ app/api/gts/statuses/[id]/route.ts. <- ๊ธ ์ญ์ / Delete Post
โโ app/feed/page.tsx <- UI
๐ Nextjs ํ๋ก์ ํธ ์์ฑ
Create a Next.js project
npx create-next-app@latest
๐ next-auth ์ค์น
Install next-auth
npm install next-auth@beta
.๐ env.local ํ์ผ ์์ฑ
Create an .env.local file.
# .env.local
# GTS_BASE_URL=https://freelifemakers.com
NEXT_PUBLIC_GTS_BASE_URL=https://freelifemakers.com
GTS_URL="https://freelifemakers.com"
# GTS ID,Secret
AUTH_GOTOSOCIAL_ID=""
AUTH_GOTOSOCIAL_SECRET=""
# npx auth secret or openssl rand -base64 32 ๋ก ์์ฑ
AUTH_SECRET=
AUTH_URL="http://localhost:3000"
๐ ์ฑ ๋ฑ๋กํด์ ID/Secret ๋ฐ๊ธฐ(ํฐ๋ฏธ๋์์ ์คํ)
Register the app to obtain the ID/Secret (run in the terminal)
โ๏ธ client_id,ย client_secret๋ฅผ .env.local์ AUTH_GOTOSOIAL_ID์ AUTH_GOTOSOCIAL_SECRET์ ์
๋ ฅํฉ๋๋ค.
curl -X POST https://freelifemakers.com/api/v1/apps \
-H "Content-Type: application/json" \
-d '{
"client_name": "freelife-local-dev",
"redirect_uris": "http://localhost:3000/api/auth/callback/gotosocial",
"scopes": "read write follow push",
"website": "http://localhost:3000"
}'
1)http://localhost:3000์์ ์คํํ๊ธฐ ๋๋ฌธ์ AUTH_URL=”http://localhost:3000″์ด ๋ฉ๋๋ค.
Since it runs on http://localhost:3000, AUTH_URL becomes “http://localhost:3000”.
2)AUTH_SECRET์ npmx auth secret๋ก ํฐ๋ฏธ๋์์ ์์ฑ ํ ์ ์์ต๋๋ค.
You can generate AUTH_SECRET in the terminal using npmx auth secret.
๐ ๋ก๊ทธ์ธ , ์ฝ๋ฐฑ API (ํต์ฌ)
Login , Callback API (Core)
— app/api/auth/[...nextauth]/route.ts
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth"
export const { GET, POST } = handlers
— myapp9/auth.ts
// auth.ts (ํ๋ก์ ํธ ๋ฃจํธ์ ์์ฑ)
// auth.ts (Created in the project root)
import NextAuth from "next-auth"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
{
id: "gotosocial",
name: "freelifemakers",
type: "oauth",
authorization: {
url: "https://freelifemakers.com/oauth/authorize",
params: { scope: "read write follow push" }
},
token: "https://freelifemakers.com/oauth/token",
userinfo: "https://freelifemakers.com/api/v1/accounts/verify_credentials",
// .env.local๊ณผ ๋ณ์๋ช
์ผ์น์ํค๊ธฐ
// Match with variable names in .env.local
clientId: process.env.AUTH_GOTOSOCIAL_ID,
clientSecret: process.env.AUTH_GOTOSOCIAL_SECRET,
// GTS ํ ํฐ ๋ฐ๊ธ ์๋ฌ(400 Bad Request) ํด๊ฒฐ ์ ๋ ์กฐ๊ฑด
// Essential condition to resolve GTS token issuance error (400 Bad Request)
client: {
token_endpoint_auth_method: "client_secret_post",
},
profile(profile: any) {
return {
id: profile.id,
name: profile.display_name || profile.username,
image: profile.avatar,
email: profile.acct.includes('@') ? profile.acct : `${profile.acct}@freelifemakers.com`,
}
},
}
],
callbacks: {
async jwt({ token, account }: any) {
if (account) token.accessToken = account.access_token
return token
},
async session({ session, token }: any) {
session.accessToken = token.accessToken
return session
}
}
})
๐ GoToSocial ํด๋ผ์ด์ธํธ ํ์ผ
GoToSocial client files
โ๏ธ lib/gotosocial.tsย ๋ง๋ค๊ธฐ / Create lib/gotosocial.ts
— project/lib/gotosocial.ts
export const GTS_BASE = process.env.GTS_URL!
// ์ธ์
์์ ๊บผ๋ธ accessToken์ผ๋ก ํ์๋ผ์ธ ๊ฐ์ ธ์ค๊ธฐ
// etrieve the timeline using the accessToken obtained from the session
export async function getHomeTimeline(accessToken: string) {
const res = await fetch(`${GTS_BASE}/api/v1/timelines/home`, {
headers: { Authorization: `Bearer ${accessToken}` }
})
return res.json()
}
1) myapp9/app/fee/page.tsx์์ ํธ์ถํฉ๋๋ค.
It is called from myapp9/app/fee/page.tsx.
๐ ํ๋ก ํธ์๋ / Frontend
— ์๋ฒ์ ๊ธฐ๋ฅ๊ณผ ํด๋ผ์ด์ธํธ์ ๊ธฐ๋ฅ์ ๊ฐ์ ํ์ด์ง์ ์ฝ๋๋ฅผ ์์ฑ ํ ์ ์์ต๋๋ค.
You cannot write server-side and client-side code on the same page.
— ๊ทธ๋์ ์ฝ๋๋ฅผ page.tsx์ PostCard.tsx๋ก ๋ถ๋ฆฌํฉ๋๋ค.
So, I am splitting the code into page.tsx and PostCard.tsx.
โ๏ธ myapp9/app/feed/page.tsx
// app/feed/page.tsx
import { auth } from "@/auth"
import { getHomeTimeline } from "@/lib/gotosocial" // /lib/gotosocial.ts
import { PostCard } from './PostCard' // Post,Delete Card Component
export default async function Feed() {
// NextAuth v5 ์ ์ฉ ์ธ์
์ถ์ถ
// Extract session for NextAuth v5
const session = await auth()
// ํ ํฐ์ด ์ธ์
์ ์์ ํ๊ฒ ๋ค์ด์๋์ง ๊ฒ์ฆ
// Verify if the token has been safely retrieved in the session
if (!session?.accessToken) {
return (
<div className="p-10 text-center">
<p className="text-red-500 font-semibold mb-2">์ธ์ฆ ํ ํฐ์ ์ฐพ์ ์ ์์ต๋๋ค. / Unable to find authentication token.</p>
<a href="/api/auth/signin" className="text-blue-500 underline">๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ / Go to login page</a>
</div>
)
}
try {
// /lib/gotosocial.ts์ getHomeTimeline() ํจ์ ์ฌ์ฉ
// Use the getHomeTimeline() function from /lib/gotosocial.ts
const posts = await getHomeTimeline(session.accessToken)
return (
<div className="max-w-xl mx-auto p-4 space-y-4">
<h1 className="text-xl font-bold border-b pb-2">์ฐํฉ ์ฐ์ฃผ ํ์๋ผ์ธ</h1>
<h1 className="text-sm text-gray-500"> Federated Universe Timeline </h1>
{posts.length === 0 ? (
<p className="text-gray-500 py-10 text-center">์์ง ํ์๋ผ์ธ์ ํ์ํ ๊ธ์ด ์์ต๋๋ค. / No posts to display yet.</p>
) : (
posts.map((p: any) => <PostCard key={p.id} post={p} token={session.accessToken} />)
)}
</div>
)
} catch (error) {
console.error("ํผ๋ ๋ก๋ฉ ์ค ์น๋ช
์ ์๋ฌ / Fatal error while loading feed:", error);
return <div className="p-10 text-red-500">GTS ๋ฐฑ์๋ ์๋ฒ์ ํต์ ํ๋ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค. / Error occurred while communicating with GTS backend.</div>
}
}
// signout : http://localhost:3000/api/auth/signout
// signin : http://localhost:3000/api/auth/signin
โ๏ธ myapp9/app/feed/PostCard.tsx
// app/feed/PostCard.tsx
'use client'
import { deleteStatus } from '@/lib/gotosocial'
export function PostCard({ post, token }: { post: any, token: string }) {
return (
<div className="border rounded-xl p-4 bg-white shadow-sm space-y-2">
<div className="flex items-center space-x-2">
<img src={post.account.avatar} className="w-8 h-8 rounded-full" />
<span className="font-bold text-sm">@{post.account.acct}</span>
</div>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<button
onClick={async () => {
if(!confirm('์ญ์ ?')) return
await fetch(`/api/gts/statuses/${post.id}`, { method: 'DELETE' })
location.reload()
}}
className="text-red-500 text-sm"
>
์ญ์ /Delete
</button>
</div>
)
}
๐ ๊ธ ์ญ์ / Delete Post
— myapp9/api/gts/statuses/[id]/route.ts
// app/api/gts/statuses/[id]/route.ts
import { auth } from "@/auth"
// ์ญ์ / Delete
export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const session = await auth()
if (!session?.accessToken) return new Response("Unauthorized", { status: 401 })
const baseUrl = process.env.GTS_URL || "https://freelifemakers.com"
const res = await fetch(`${baseUrl}/api/v1/statuses/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${session.accessToken}` }
})
return new Response(null, { status: res.status })
}
1)๊ธ ์ญ์ ๋ถ๋ถ ๋ผ์ฐํฐ๋ฅผ ๋ฐ๋ก ๋ง๋ ์ด์ ๋ ์๋ฒ์ CORS์๋ฌ ๋๋ฌธ์
๋๋ค.
I created a separate router for the post deletion functionality due to a server-side CORS error.
2)gotosocial.ts์ ๊ธ์ญ์ ํจ์๊ฐ ์๋ ๊ฒฝ์ฐ PostCard.tsx์์ ํธ์ถํ๋ฉด PostCard.tsx๊ฐ ๋ธ๋ผ์ฐ์ ๊ฐ ๋๊ธฐ ๋๋ฌธ์ ์๋ฒ์์ ์ฐจ๋จํฉ๋๋ค.
If the post deletion function is located in gotosocial.ts and called from PostCard.tsx, the server will block the request because PostCard.tsx acts as the client (browser).
3)์๋ฒ์ ์๋ฒ๋ผ๋ฆฌ๋ CORS๋ฌธ์ ๊ฐ ์์ต๋๋ค.
There are no CORS issues between servers.
4) ๊ทธ๋์ PostCard.tsx๊ฐ ์คํํ ๋ผ์ฐํธ์์ gotosocial์๋ฒ์ ์ญ์ ์์ฒญ์ ํฉ๋๋ค.
Therefore, the route executed by PostCard.tsx sends a deletion request to the GoToSocial server.
๐ ์คํ / Start Server
npm run dev
๐ ๋ธ๋ผ์ฐ์ ์ ์ / Access Browser
http://localhost:3000/feed

