ππ» νμκ°μ
λ° λ‘κ·ΈμΈκ³Ό κ΄λ ¨λ κΈ°λ₯μ
λλ€.
These are features related to sign-up and login.
ππ» myapp40μμλ κΈ°μ‘΄ λ‘κ·ΈμΈ λ³΄μμ μ
κ·Έλ μ΄λν©λλ€.
myapp40 is upgrading its existing login security.
ππ» κΈ°μ‘΄ access tokenμΈμ refresh tokenμ μΆκ°νκ³ session ν
μ΄λΈμ μ μ₯ν©λλ€.
In addition to the existing access token, a refresh token is added and stored in the session table.
ππ» access tokenμ 15λΆ refresh tokenμ 7μΌλ‘ μ€μ νκ³ λ‘κ·ΈμΈνλ©΄ access tokenκ³Ό refresh tokenμ λ°κΈνκ³ session ν μ΄λΈμ μ μ₯ν©λλ€.
ππ» λ‘κ·ΈμΈν μνμμ access tokenμ΄ λ§λ£λλ©΄(15λΆ) sessionν
μ΄λΈμ refresh token(7μΌ)μ΄ μλμ§ νμΈνκ³ access tokenμ μ¬λ°κΈν©λλ€.
If the access token expires (after 15 minutes) while logged in, the system checks the session table for a refresh token (valid for 7 days) and issues a new access token.
ππ» μλ°μ€ν¬λ¦½νΈμμλ μΏ ν€λ₯Ό μ½μ μ μλλ‘ μ€μ νκ³ (httpOnly: true) νλ‘λμ
λͺ¨λμμ httpsλ§ νμ©νκ³ (secure: IS_PROD) λ€λ₯Έ μ¬μ΄νΈμμ κΈμ°κΈ°λ₯Ό λ§λ μ€μ μ μΆκ°ν©λλ€.(sameSite: ‘lax’)
In JavaScript, configure settings to prevent cookies from being read (httpOnly: true), allow only HTTPS in production mode (secure: IS_PROD), and add a setting to block write operations from other sites (sameSite: 'lax').
ππ» μ 체 μ½λλ κΉνλΈμμ νμΈ ν μ μμ΅λλ€.
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 -> 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/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/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
β β βββ _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
π ν μ΄λΈ μΆκ° / Add Table
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
refresh_token TEXT NOT NULL UNIQUE,
access_token TEXT,
user_agent TEXT,
ip TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
last_used_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_sessions_refresh ON sessions(refresh_token);
CREATE INDEX IF NOT EXISTS idx_sessions_username ON sessions(username);
CREATE INDEX idx_sessions_expires ON sessions(expires_at);
π sessionν μ΄λΈ νλ μ€λͺ / Description of session table fields
— κΈ°μ‘΄ oauth_tokensν
μ΄λΈμ pinaforeλ± μΈλΆ μ±μ©
The existing oauth_tokens table is for external apps such as pinafore.
— sessionν
μ΄λΈμ λ‘컬 μ μ©
The session table is local-only
| μ»¬λΌ / Column | κ° μμ / Value Example | μ€λͺ / Explanation |
|---|---|---|
id | a1b2c3d4-... | μΈμ
κ³ μ ID Session Unique ID |
username | user1 | λ‘κ·ΈμΈν μ¬λ Logged-in user |
refresh_token | 9f8e7d... (128κΈμ!) | 7μΌμ§λ¦¬ ν ν°- DBμμ μ§μ νμΈμ© 7-day token β for direct verification in the DB |
access_token | eyJ... (JWT) | 15λΆμ§λ¦¬ ν ν° – μ§κΈ μ¬μ©νκ³ μλ ν ν° 15-minute token β the token currently in use |
user_agent | Mozilla/5.0... Chrome | ν¬λ‘¬μμ λ‘κ·ΈμΈνλμ§ Pinafore!μμ!νλμ§ νμΈμ© To check if you logged in on Chrome or on Pinafore! |
ip | 1.2.3.4 | μ΄λμ λ‘κ·ΈμΈ νλμ§ (ν΄νΉ νμΈμ©) Where the login occurred (to check for hacking) |
expires_at | 2026-09-22 10:00:00 | λ§λ£ μκ°μ 7μΌ ν The expiration time is 7 days later |
π μ½λ μμ / Code Modification
βοΈ lib/db.ts
— λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν / Initialize Database
db.exec(`
... ...
-- myapp40 β
Session
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
refresh_token TEXT NOT NULL UNIQUE,
access_token TEXT,
user_agent TEXT,
ip TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL,
last_used_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
db.exec(`
... ...
-- session -- β
myapp40
CREATE INDEX IF NOT EXISTS idx_sessions_refresh ON sessions(refresh_token);
CREATE INDEX IF NOT EXISTS idx_sessions_username ON sessions(username);
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
`);
βοΈ lib/auth.ts
// β
myapp40 - 15λΆμ§λ¦¬ Access Token / 15-minute access token
export function createAccessToken(username: string) {
return jwt.sign({ username, type: 'access' }, JWT_SECRET, { expiresIn: '15m' });
}
// β
myapp40 - 7μΌμ§λ¦¬ Refresh Token (λλ€ λ¬Έμμ΄ + DB μ μ₯μ©)
// 7-day Refresh Token (random string + for DB storage)
export function createRefreshToken() {
return crypto.randomBytes(64).toString('hex'); // 128κΈμ λλ€ / 128-character random string
}
// - κΈ°μ‘΄ νΈνμ© (Pinafore OAuthλ μμ§ 7μΌμ§λ¦¬ μ¨λ λ¨)
export function createToken(username: string) {
return jwt.sign({ username }, JWT_SECRET, { expiresIn: '7d' });
}
... ...
// β
myapp40 - sessions ν
μ΄λΈμ μ μ₯ / Save to the sessions table.
export function createSession(username: string, userAgent: string | null, ip: string | null) {
const id = crypto.randomUUID();
const refreshToken = createRefreshToken();
const accessToken = createAccessToken(username);
db.prepare(`
INSERT INTO sessions (id, username, refresh_token, access_token, user_agent, ip, expires_at)
VALUES (?,?,?,?,?,?, datetime('now', '+7 days'))
`).run(id, username, refreshToken, accessToken, userAgent, ip);
return { id, refreshToken, accessToken };
}
βοΈ app/api/auth/login/route.ts
— μ΄ λΆλΆμμ 보μμ€μ / Security settings in this section
Security settings in this section
// β
Access Token - 15λΆ - λͺ¨λ κ²½λ‘μμ μ¬μ© / Use for all routes
res.cookies.set('token', accessToken, {
httpOnly: true, // β
true - JSμμ λͺ» μ½μ XSS λ°©μ΄ / XSS protection: Unreadable by JS
secure: IS_PROD, // only https
sameSite: 'lax', // λ€λ₯Έ μ¬μ΄νΈμμ POSTμμ²μ μΏ ν€ μ보λ / Cookies are not sent during POST requests from other sites.
maxAge: 60*15, // 15λΆ / 15minute
path: '/'
});
// β
Refresh Token - 7μΌ! - /api/auth/refresh μμλ§ μ¬μ©
res.cookies.set('refresh_token', refreshToken, {
httpOnly: true,
secure: IS_PROD,
sameSite: 'lax',
maxAge: 60*60*24*7, // 7μΌ / 7days
path: '/'
});
βοΈ app/api/auth/refresh/route.ts – λΌμ°νΈ μΆκ° / Add route
export async function POST(req: Request) {
... ...
if (!result) {
const res = NextResponse.json({ error: 'refresh_token λ§λ£ λ€μ λ‘κ·ΈμΈ / Refresh token expired; please log in again.' }, { status: 401 });
res.cookies.set('token', '', { maxAge: 0, path: '/' });
res.cookies.set('refresh_token', '', { maxAge: 0, path: '/' });
return res;
}
... ...
}
βοΈ app/api/auth/logout/route.ts
export async function POST(req: Request) {
... ...
// oauth_tokensλ μμ (Pinafore ν ν°)
// Delete oauth_tokens as well (Pinafore tokens)
const tokenMatch = cookie.match(/token=([^;]+)/);
if (tokenMatch) {
try {
const db = (await import('@/lib/db')).default;
db.prepare('DELETE FROM oauth_tokens WHERE access_token =?').run(decodeURIComponent(tokenMatch[1]));
} catch {}
}
... ...
}
βοΈ app/auth/login/page.tsx
— λ‘κ·ΈμΈ μ μ© / Apply login
— useEffectμμ checkLoginμΌλ‘ λ‘κ·ΈμΈ νμΈ
Check login status using checkLogin within useEffect.
— λ‘κ·ΈμΈλμ΄ μμΌλ©΄ currentUser μ
ν
Set currentUser if logged in.
— currentUserλ³μ κ°μ μ‘΄μ¬ μ¬λΆμ λ°λΌ 보μ¬μ€ νμ΄μ§ μ€μ
Configure the page to be displayed based on the presence of the currentUser variable.
'use client';
import { useState, useEffect, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
function LoginForm() {
const [loginId, setLoginId] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [currentUser, setCurrentUser] = useState('');
const [loading, setLoading] = useState(true);
const router = useRouter();
const searchParams = useSearchParams();
const next = searchParams.get('next') || '/';
useEffect(() => {
async function checkLogin() {
try {
const res = await fetch('/api/auth/me', { credentials: 'include' });
if (res.ok) {
const data = await res.json();
if (data.username) {
setCurrentUser(data.username);
}
}
} catch {}
setLoading(false);
}
checkLogin();
}, []);
... ...
return (
<div style={{ maxWidth: 400, margin: '80px auto', padding: 20 }}>
<h1>π λ‘κ·ΈμΈ / Login {currentUser ? `${currentUser} - ` : ''} myapp40</h1>
{currentUser ? (
<>
<p style={{ color: 'green', fontSize: 18 }}>νμ¬ λ‘κ·ΈμΈ: <strong>{currentUser}</strong></p>
) : (
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 20 }}>
... ...
</form>
)}
... ...
}
βοΈ app/api/auth/me/route.tsx – λΌμ°νΈ μΆκ° / Add route
// β
myapp40 - app/api/auth/me/route.ts
import { NextResponse } from 'next/server';
import { verifyToken } from '@/lib/auth';
export async function GET(req: Request) {
const cookie = req.headers.get('cookie') || '';
const m = cookie.match(/token=([^;]+)/);
if (!m) {
return NextResponse.json({ error: 'Not logged in' }, { status: 401 });
}
const token = decodeURIComponent(m[1]);
const decoded = verifyToken(token);
if (!decoded?.username) {
return NextResponse.json({ error: 'Token expired' }, { status: 401 });
}
return NextResponse.json({ ok: true, username: decoded.username });
}
βοΈ middleware.ts
— λ‘κ·ΈμΈνμ§μμΌλ©΄ νμ΄μ§ μ°¨λ¨κΈ°λ₯ μ¬μ©ν κ²½μ°
If you use the page blocking function without logging inβ¦
export function middleware(req: NextRequest) {
const origin = req.headers.get('origin') || '';
const path = req.nextUrl.pathname;
const DOMAIN = process.env.DOMAIN;
// β
νμ©ν origin λͺ©λ‘!
const allowedOrigins = [
`https://${DOMAIN}`,
`https://pinafore.social`,
`http://localhost:3000`,
`http://localhost:3001`
];
const isAllowedOrigin = allowedOrigins.includes(origin) ||!origin;
// // ===== 1. λ‘κ·ΈμΈ μ²΄ν¬ (νμ΄μ§ 보νΈ) / Login check (page protection) =====
// const token = req.cookies.get('token')?.value;
// const refreshToken = req.cookies.get('refresh_token')?.value;
// λ‘κ·ΈμΈμ΄ νμν νμ΄μ§ / Page requiring login
// const protectedPaths = ['/admin', '/settings', '/oauth/authorize'];
// const isProtected = protectedPaths.some(p => path.startsWith(p));
// if (isProtected &&!token &&!refreshToken) {
// // ν ν° μμΌλ©΄ λ‘κ·ΈμΈ νμ΄μ§λ‘ / Redirect to the login page if there is no token.
// const loginUrl = new URL('/auth/login', req.url);
// loginUrl.searchParams.set('next', path);
// return NextResponse.redirect(loginUrl);
// }
... }
π ν μ€νΈ / Test
βοΈ μΏ ν€ μ μ₯ λ° λ³΄μμ€μ μν
Cookie storage and security settings status
— https://aloy-horizon.duckdns.org/auth/login μ΄ λΌμ°νΈμμ λ‘κ·ΈμΈμ΄ μλ£λλ©΄ μΏ ν€μ λ‘컬μ€ν 리μ§μ μ μ₯λ ν ν° λ° μμ΄λλ₯Ό νμΈ ν μ μμ΅λλ€.
Once login is completed at the route https://aloy-horizon.duckdns.org/auth/login, you can verify the tokens and IDs stored in cookies and local storage.
— cmd+option+I(macOS)λ‘ νμΈ ν μ μμ΅λλ€.
You can check it using Cmd+Option+I (macOS).


βοΈ μλ²λ‘κ·Έ / Server Log
— μμΈμ€ ν ν° λ§λ£μ μλ‘μ΄ μμΈμ€ ν ν°μ λ°κΈν©λλ€.
A new access token is issued when the access token expires.

μν볡μ 8μ₯ 32μ / John 8:32
“κ·Έλ¦¬κ³ λν¬λ μ§λ¦¬λ₯Ό μκ² λ κ²μ΄λ©°, μ§λ¦¬κ° λν¬λ₯Ό μμ λ‘κ² ν κ²μ΄λ€.”
“Then you will know the truth ,and the truth will set you free”