๐๐ป ํ์๊ฐ์
๋ฐ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์
๋๋ค.
These are the sign-up and login functions.
๐๐ป pinafore์ ๋ก๊ทธ์ธํ๋ ๊ธฐ๋ฅ(OAuth)์ ์ถ๊ฐํฉ๋๋ค.
Add a login feature (OAuth) to Pinafore.
๐๐ป ์์ด๋ ํจ์ค์๋ ์
๋ ฅ์์ด user1์ผ๋ก ๋ก๊ทธ์ธํฉ๋๋ค.
Log in as user1 without entering an ID or password.
๐๐ป ์ ์ฒด ์ฝ๋๋ ๊นํ๋ธ์์ ํ์ธ ํ ์ ์์ต๋๋ค.
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/v1/instance/route.ts -> auth
โ โโโ api/v1/apps/route.ts. -> auth
โ โโโ api/v1/accounts/verify_credentials/route.ts -> auth
โ โโโ api/v1/statuses/route.ts -> auth
โ โโโ api/v1/timelines/home/route.ts -> auth
โ โโโ oauth/authorize/route.ts -> auth
โ โโโ oauth/token/route.ts -> auth
โ โโโ 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
๐ ๊ฐ์ / Overview
Pinafore (Mastodon ํด๋ผ์ด์ธํธ) ๋ก๊ทธ์ธ ๊ฐ์
Pinafore (Mastodon Client) Login Overview
1. Pinafore์์ Instance์ https://aloy-horizon.duckdns.org ์
๋ ฅ
Enter https://aloy-horizon.duckdns.org into the Instance field in Pinafore.
2. Pinafore๊ฐ ์งํํ๋ ์ฒ๋ฆฌ ๊ณผ์
The process carried out by Pinafore
- ๋ด ์๋ฒ ๊ฒ์ / Search My Servers
GET /.well-known/webfinger?resource=acct:user1@aloy-horizon.duckdns.org
- ๋ด ์๋ฒ ์ ๋ณด ๊ฐ์ ธ๊ฐ๊ธฐ / Retrieve My Server Information
GET /api/v1/instance( ๋ด ์๋ฒ ์ ๋ณด๋ฅผ Pinafore๊ฐ ๊ฐ์ ธ๊ฐ)
- ์ฑ ๋ฑ๋ก / App Registration
(์ฑ ๋ฑ๋กํ๊ณ pinafore์ ์๋ต / Register the app and respond to Pinafore.)
POST /api/v1/apps
3. ๋ธ๋ผ์ฐ์ ํ์
(์น์ธํ๋ฉด) / Browser pop-up (approval screen):
- pinafore๊ฐ ๋ด ์๋ฒ์์ ์๋์ ๋ผ์ฐํธ GET์ผ๋ก ์คํํ์ฌ ์น์ธํ๋ฉด ์๋ด
Pinafore displays the authorization screen when the GET route below is executed on my server.
GET /oauth/authorize?client_id=...&scope=read write follow
4. ์น์ธ ๋ฒํผ ๋๋ฅด๊ธฐ / Press the approval button. :
- ์น์ธํ๋ฉด์์ ์น์ธ๋ฒํผ ๋๋ฅด๋ฉด POST /oauth/authorize ์คํ
Clicking the approval button on the approval screen executes POST /oauth/authorize.
- oauth_codes ๋ฐ๊ธํ๊ณ ์ฝ๋๋ฅผ DB์ ์ ์ฅ
Issue oauth_codes and store the codes in the database.
- ๋ค์ pinafor.social๋ก ๋ฆฌ๋ค์ด๋ ํธ ์คํ
Redirecting back to pinafor.social
- Pinafore๊ฐ POST /oauth/token ๋ผ์ฐํธ ์คํํด์ access_token ๋ฐ๊ธํ๊ณ DB์ ์ฅ
Pinafore executes the POST /oauth/token route to issue an access_token and save it to the DB.
5. Pinafore ๋ก๊ทธ์ธ ์ฑ๊ณต / Pinafore login successful :
- /api/v1/accounts/verify_credentials ๋ผ์ฐํธ ์คํ ๋ด ์๋ฒ ์ ๋ณด๋ฆฌํด
Return server information upon execution of the /api/v1/accounts/verify_credentials route.
- verify_credentials์์ ๋ฆฌํดํ ์ ๋ณด๋ฅผ ํ์๋ผ์ธ์ ํ์
Display the information returned by verify_credentials on the timeline.
- pinafore ํ์๋ผ์ธ์์ ๊ธ์ฐ๊ธฐ ๊ฐ๋ฅ Outbox POST ์ฌ์ฉ๊ฐ๋ฅ
Posting is enabled on the Pinafore timeline; Outbox POST is supported.
โ๏ธ ํ์ํ ๋ผ์ฐํธ / route needed
| ์์ / order | ๋ผ์ฐํธ / route | ์ญํ / role | ํ์ผ / file | ์ํ / ststus |
|---|---|---|---|---|
| 1 | /.well-known/webfinger | – Pinafore๊ฐ user@๋๋ฉ์ธ ์กด์ฌํ๋์ง ํ์ธ!Pinafore checks if the user@domain exists! | app/.well-known/webfinger/route.ts | ์ด๋ฏธ ์์ Already exists |
| 2 | /api/v1/instance | – ์๋ฒ ์ ๋ณด! version, titleServer Information! version, title | app/api/v1/instance/route.ts | myapp33 |
| 3 | /api/v1/apps POST | – ์ฑ ๋ฑ๋ก! client_id/secret ๋ฐ๊ธApp registration! Issue client_id/secret. | app/api/v1/apps/route.ts | myapp33 |
| 4 | /oauth/authorize GET | – ๋ก๊ทธ์ธ์น์ธ ํ๋ฉด,์น์ธ๋ฒํผ Login approval screen, approval button | app/oauth/authorize/route.ts | myapp33 |
| 5 | /oauth/authorize POST | ์น์ธ code ๋ฐ๊ธ | app/oauth/authorize/route.ts (POST) | myapp33 |
| 6 | /oauth/token POST | code -> access_token ๊ตํ | app/oauth/token/route.ts | myapp33 |
| 7 | /api/v1/accounts/verify_credentials | ๋ด ์ ๋ณด ๋ฆฌํด! ๋ก๊ทธ์ธ | app/api/v1/accounts/verify_credentials/route.ts | myapp33 |
| 8 | /api/v1/timelines/home | ๋ก๊ทธ์ธ ํ ํ์๋ผ์ธ | app/api/v1/timelines/home/route.ts | myapp33 |
| 9 | /api/v1/statuses POST | ๊ธ์ฐ๊ธฐ(pinafore) Outbox ์ ์ฅ | app/api/v1/statuses/route.ts | myapp33 |
โ๏ธ ์๋์์ฑ ํ ์ด๋ธ / Automatically generated table
# ์ฑ ๋ฑ๋ก / App Registration
CREATE TABLE oauth_apps (
client_id TEXT PRIMARY KEY,
client_secret TEXT NOT NULL,
client_name TEXT,
redirect_uri TEXT,
scopes TEXT,
website TEXT,
created_at INTEGER
);
# Oauth codes
CREATE TABLE oauth_codes (
code TEXT PRIMARY KEY,
client_id TEXT,
redirect_uri TEXT,
scope TEXT,
username TEXT,
created_at INTEGER
);
# OAuth tokens
CREATE TABLE oauth_tokens (
access_token TEXT PRIMARY KEY,
client_id TEXT,
username TEXT,
scope TEXT,
created_at INTEGER
);
๐ก
oauth_apps
- ์ฑ๋ฑ๋ก(pinaafore์๋ง ๋ก๊ทธ์ธ ํ์ผ๋ฉด ๋ฐ์ดํฐ 1๊ฐ)
App registration (1 data entry if logged into Pinaafore only)
oauth_tokens
- ์ฑํ ํฐ(์ ์ 100๋ช
์ด๋ฉด 100๋ช
์ด ๋ก๊ทธ์ธํ ํ์ ๋งํผ ,์ฆ 100+)
App tokens (based on the number of logins by the 100 usersโi.e., 100+).
oauth_codes
- ์ฐ๊ณ ๋ฒ๋ฆฌ๋ ์ฝ๋ ํญ์ ๋ฐ์ดํฐ ๊ฐฏ์๋ 0
Discarded code; the data count is always 0
๐ ๋ผ์ฐํธ ๋ฐ ํ์ผ ์ถ๊ฐ / Add Routes and Files
โ๏ธ app/api/v1/instance/route.ts
— ๋์ ๊ฐ์ / Operation Overview
/api/v1/instance GET
-> Pinafore๊ฐ ๋ด ์๋ฒ์ ๋ณด๋ฅผ ๊ฐ์ ธ๊ฐ๋๋ค.
Pinafore retrieves my server information.
-> ์ด๋ ๋ด ์๋ฒ๋ ๋ง์คํ ๋ ์๋ฒ๋ก ์ธ์ํ๋๋ก ์ค์ ์ ํฉ๋๋ค.
At this point, configure your server so that it is recognized as a Mastodon server.
— ์ฝ๋ / Code
// app/api/v1/instance/route.ts
import { NextResponse } from 'next/server';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';
export async function GET() {
return NextResponse.json({ ... ...
})}
— ํ ์คํธ / Test
# ํฐ๋ฏธ๋์์ ์คํ / Run in the terminal
% curl https://aloy-horizon.duckdns.org/api/v1/instance
# ์๋ต / Response
{"uri":"aloy-horizon.duckdns.org","title":"Aloy Horizon","short_description":"My ActivityPub server!","description":"My own Fediverse server running Next.js","email":"admin@aloy-horizon.duckdns.org","version":"4.3.0 (compatible; Aloy Horizon 1.0)","urls":{"streaming_api":"wss://aloy-horizon.duckdns.org"},"stats":{"user_count":1,"status_count":100,"domain_count":1000},"thumbnail":"https://aloy-horizon.duckdns.org/icon.png","languages":["en","ko"],"registrations":true,"approval_required":false,"invites_enabled":false,"configuration":{"statuses":{"max_characters":500,"max_media_attachments":4}},"rules":[]}
%

โ๏ธ /api/v1/apps POST
— ๋์๊ฐ์ / Operation Overview
# Pinafore ์ฑ์ ๋ด ์๋ฒ์ ์ฑ๋ฑ๋ก ํ๊ธฐ
# Registering the Pinafore app on my server
# - /api/v1/apps POST -> Pinafore ์ฑ ๋ฑ๋ก! client_id ๋ฐ๊ธ!
# - /api/v1/apps POST -> Register Pinafore app! Issue client_id!
# Pinafore๊ฐ ๋ณด๋ด๋ ์ ๋ณด
# Information from Pinafore
POST /api/v1/apps
{
"client_name": "Pinafore",
"redirect_uris": "https://pinafore.social/settings/instances/add",
"scopes": "read write follow push",
"website": "https://pinafore.social"
}
— ์ฝ๋ / Code
// app/api/v1/apps/route.ts - โ
myapp33-2 - Pinafore ์ฑ ๋ฑ๋ก / Pinafore App Registration
import { NextResponse } from 'next/server';
import db from '@/lib/db';
import { randomUUID } from 'crypto';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';
export async function POST(req: Request) { ... ... }
— ํ ์คํธ / Test
# ํฐ๋ฏธ๋์์ ์คํ / Run in the terminal
% curl -X POST https://aloy-horizon.duckdns.org/api/v1/apps \
-H "Content-Type: application/json" \
-d '{
"client_name": "Pinafore",
"redirect_uris": "urn:ietf:wg:oauth:2.0:oob",
"scopes": "read write follow push",
"website": "https://pinafore.social"
}'
# ์๋ต / Response
{"id":"e6988bbc-3ef3-4e7d-8c28-620be9a2602b","name":"Pinafore","website":"https://pinafore.social","redirect_uri":"urn:ietf:wg:oauth:2.0:oob","client_id":"e6988bbc-3ef3-4e7d-8c28-620be9a2602b","client_secret":"c8b558db-7dc1-40dd-a711-9ff322ad29ccc1d71931-09d6-491c-8d59-17968d1f3f1c","vapid_key":""}
%

โ๏ธ app/oauth/authorize/route.ts
— ๋์๊ฐ์ / Operation Overview
# Pinafore์์ ์๋์ ๋ผ์ฐํธ ์คํํด์ ๋ธ๋ผ์ฐ์ ์น์ธํ๋ฉด ํ์
:
# Execute the route below in Pinafore to open the browser approval screen popup
https://aloy-horizon.duckdns.org/oauth/authorize?
client_id=bdbaf975...
&redirect_uri=urn:ietf:wg:oauth:2.0:oob
&response_type=code
&scope=read write follow push
# ํ๋ฉด์ ์น์ธ ๋๋ฅด๋ฉด code ๋ฐ๊ธ
# Press Approve on the screen to issue a code
— ์ฝ๋ / Code
// app/oauth/authorize/route.ts - โ
myapp33-3 - ๋ก๊ทธ์ธ ์น์ธ ํ์ด์ง! / Login Authorization Page!
import { NextResponse } from 'next/server';
import db from '@/lib/db';
import { randomUUID } from 'crypto';
export async function GET(req: Request) { ... ... }
export async function POST(req: Request) { ... ... }
— ํ ์คํธ / Test
# Terminal
% curl -X POST https://aloy-horizon.duckdns.org/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=bdbaf975-d04b-4dc6-8525-6b94e9d09752&client_secret=ee233e0e-a99f-4ca4-8487-09cce8219a1c4115f412-d65b-4268-944f-f0e6aed5199d&code=df066428-3240-45cf-aa2a-6c4f809f875c&grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob"
# ์๋ต / Response
{"access_token":"2f7f89d8-20e0-4e63-acac-5d3bea32deb11e323217-71b3-455a-8320-fc348e305611","token_type":"Bearer","scope":"read write follow","created_at":1788651972}
%
— ์๋์ ํ๋ฉด์์ pinafore์ ์ ๋ณด๊ฐ ์์ผ๋ฉด auth ์ฝ๋๊ฐ ๋ณด์
๋๋ค.(ํ
์คํธ ๋จ๊ณ)
If there is no information for Pinafore on the screen below, the auth code will be displayed.(Testing phase)
— pinafore์ ๋ณด๋ฅผ ์ ์์ ์ผ๋ก ํ์ธ๋๋ฉด pinafore ํ์๋ผ์ธ์ ์ด๋ํฉ๋๋ค.
Once the Pinafore information is successfully verified, you will navigate to the Pinafore timeline.


โ๏ธ app/api/v1/accounts/verify_credentials/route.ts
— Pinafore๊ฐ ๋ณด๋ ๋ด ์๋ฒ์ ๋ณด,ํ์๋ผ์ธ์ ํ์๋๋ ์ ๋ณด
My server information as seen by Pinafore, information displayed on the timeline
import { NextResponse } from 'next/server';
import db from '@/lib/db';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';
export async function GET(req: Request) { ... }
export async function OPTIONS() { ... }
— ํ ์คํธ / Test
curl https://aloy-horizon.duckdns.org/api/v1/accounts/verify_credentials \
-H "Authorization: Bearer 2f7f89d8-20e0-4e63-acac-5d3bea32deb11e323217-71b3-455a-8320-fc348e305611"
% curl https://aloy-horizon.duckdns.org/api/v1/accounts/verify_credentials \
-H "Authorization: Bearer 2f7f89d8-20e0-4e63-acac-5d3bea32deb11e323217-71b3-455a-8320-fc348e305611"
# ์๋ต / Response
{"id":"1","username":"user1","acct":"user1@aloy-horizon.duckdns.org","display_name":"user1","locked":false,"bot":false,"created_at":"2026-09-05T23:52:44.236Z","note":"My ActivityPub server!","url":"https://aloy-horizon.duckdns.org/users/user1","avatar":"https://aloy-horizon.duckdns.org/icon.png","avatar_static":"https://aloy-horizon.duckdns.org/icon.png","header":"https://aloy-horizon.duckdns.org/icon.png","header_static":"https://aloy-horizon.duckdns.org/icon.png","followers_count":1,"following_count":1,"statuses_count":100,"source":{"privacy":"public","sensitive":false,"language":"en","note":"","fields":[]}}
%
โ๏ธ app/api/v1/timelines/home/route.ts
— ๋ก๊ทธ์ธ ํ ํ์๋ผ์ธ (์์ง ๋น ํ์๋ผ์ธ)
Timeline after logging in (currently empty)
export async function GET() {
// โ
๋น ํ์๋ผ์ธ! (๋์ค์ Outbox ๊ธ ๊ฐ์ ธ์ค๊ฒ ํ ์ ์์!) / Empty timeline! (Later, you can fetch posts from the Outbox!)
return NextResponse.json([]);
}
export async function OPTIONS() { ... }
โ๏ธ app/api/v1/statuses/route.ts
— ๋ก๊ทธ์ธ ํ ํ์๋ผ์ธ์ ๋ณด์ด๋ ๋ด๊ฐ ์์ฑํ๊ธ(์์ง DB์ ์ฅ ๊ตฌํ ์ํจ.)
Posts I’ve written that appear on the timeline after logging in (DB storage implementation not yet complete).
import { NextResponse } from 'next/server';
import db from '@/lib/db';
import { randomUUID } from 'crypto';
const DOMAIN = process.env.DOMAIN || 'aloy-horizon.duckdns.org';
export async function POST(req: Request) { ... }
export async function OPTIONS() { ... }
๐ CORS์ค์ ์ถ๊ฐ / Add CORS configuration
โ๏ธ ํ๋ก์ ํธ ๋ฃจํธ์ middlewatre.ts ํ์ผ ์ถ๊ฐ
Add a middleware.ts file to the project root.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(req: NextRequest) { ... }
export const config = {
matcher: ['/api/:path*', '/oauth/:path*', '/.well-known/:path*', '/users/:path*'],
};
โ๏ธ OPTIONS ํจ์ ์ถ๊ฐ / Add OPTIONS function
— ์๋์ ํ์ผ๋ค์ ๊ฐ์ฅํ๋จ์ OPTIONSํจ์๋ฅผ ์ถ๊ฐํฉ๋๋ค.
Add the OPTIONS function to the very bottom of the files listed below.
1) OPTIONS ํจ์ / OPTIONS Function
// CORS ์๋ฌ ๋ฐฉ์ง์ฉ OPTIONS ์ฒ๋ฆฌ / Handle OPTIONS to prevent CORS errors
export async function OPTIONS() {
return new NextResponse(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
2)ํ์ผ / Files
app/api/v1/instance/route.ts
app/api/v1/apps/route.ts
app/api/v1/accounts/verify_credentials/route.ts
app/api/v1/timelines/home/route.ts
app/api/v1/statuses/route.ts
app/oauth/token/route.ts
app/oauth/authorize/route.ts
— ์ค์ ์ ์ํ๋ฉด pinafore.social์์ ์๋์ ๊ฐ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
If you do not configure the settings, an error like the one below may occur on pinafore.social.
Error: FetchEvent.respondWith received an error: TypeError: Load failed. Is this a valid Mastodon instance? Is a browser extension blocking the request? Are you in private browsing mode? If you believe this is a problem with your instance, please send this link to the administrator of your instance.
— ํ ์คํธ / Test
# preflight Test
curl -i -X OPTIONS https://aloy-horizon.duckdns.org/api/v1/instance \
-H "Origin: https://pinafore.social" \
-H "Access-Control-Request-Method: GET"
# ์์ curl์ ํฐ๋ฏธ๋์์ ์คํํ๋ฉด ๋ธ๋ผ์ฐ์ ์ ์ธ์ฆ ํ์ฉ ํ๋ฉด ์คํ ๋จ.
# Running the curl command above in the terminal triggers the authentication approval screen in your browser.
๐ ์ต์ข ํ ์คํธ / Final Test
โ๏ธ ์์ ๋ชจ๋ ํ
์คํธ๊ฐ ์ ์์ ์ผ๋ก ์๋ํ๋ฉด ๋ก์ปฌ ์๋ฒ๋ฅผ ์คํํ๊ณ pinafore.social์์ ๋ก๊ทธ์ธ์ ํด๋ด
๋๋ค.
If all the tests above run successfully, start the local server and try logging in at pinafore.social.
โ๏ธ ๋ก๊ทธ์ธ์ ์๋ฃ ํ๋ฉด ์ค์ ๋ก ๊ธ์ฐ๊ธฐ๊ฐ ์คํ๋๋์ง ํ์ธํด ๋ด
๋๋ค.
After logging in, verify that the writing function actually works.
โ๏ธ ๊ธ์ฐ๊ธฐ ๋ฐ์ดํฐ๋ ์ง๊ธ์ outbox์ ์ ์ฅํ์ง ์๊ณ pinafore์ ๋ณด์ด๋์ง๋ง ํ์ธํฉ๋๋ค.
For now, writing data is not saved to the outbox; we only verify that it appears in Pinafore.

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