๐๐ป ์ฌ๊ธฐ์๋ ํ์๊ฐ์
๋ถ๋ถ์ ๊ตฌ์กฐ์ ๋ํด์ ์ดํด๋ด
๋๋ค.
Here, we examine the structure of the sign-up section.
๐๐ป nextjs์
๊ทธ๋ ์ด๋ํ๋ ค๋ฉด ์๋์ฒ๋ผ ํ์๋ฉด ๋ฉ๋๋ค.
To upgrade Next.js, you can follow the steps below.
๐Nextjs ์ ๊ทธ๋ ์ด๋ / Upgrade
โ๏ธ ํ๋ก์ ํธ์ ๋ค์๊ณผ ๊ฐ์ ๋ฉ์ธ์ง๊ฐ ๋ณด์ด๋ฉด ์
๊ทธ๋ ์ด๋ํ๋ฉด ๋ฉ๋๋ค.
If you see the following message in your project, you can perform an upgrade.
โ Installed Next.js version 16.4.0 is affected by a known security vulnerability.
Next.js security version upgrade available: 16.4.0 -> 16.4.2
โฏ Upgrade now
Skip
Skip until next version
1)AI Agent๊ฐ ์๋์
๊ทธ๋ ์ด๋ ํ๋๋ก ํ์ฉํ๋ ค๋ฉด ์๋์ ๊ฐ์ด next.config.tsํ์ผ์ ์์ ํด์ผํฉ๋๋ค.
To allow the AI โโAgent to perform automatic upgrades, you must modify the next.config.ts file as shown below.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* Allow auto securiy upgrade for AI Agent */
experimental: {
agentUpgrade: "security"
},
/* allow domain*/
allowedDevOrigins: ['aloy-horizon.duckdns.org', '*.duckdns.org'],
/* host.domain.org/@userid */
async rewrites() {
return [
{
source: '/@:username',
destination: '/usersui/:username',
},
{
source: '/@:username/following',
destination: '/usersui/:username?view=following', // โ
myapp43 - initialView
},
{
source: '/@:username/followers',
destination: '/usersui/:username?view=followers',
},
{ source: '/@:username/notifications', // โ
myapp44 - initialView for notifications
destination: '/usersui/:username?view=notifications'
},
]
},
};
export default nextConfig;
2)์๋์ผ๋ก ์
๊ทธ๋ ์ด๋ํ๋ ค๋ฉด ์๋์ฒ๋ผ ํ๋ฉด๋ฉ๋๋ค.
To perform a manual upgrade, follow the steps below.
npm install next@16.4.2
๐ ์ธ๋ถ ์ฑ ๋ฐ ๋ก์ปฌ UI๋ถ๋ถ ์ธ์ฆ ๋ถ๋ถ ๊ตฌ์กฐ
Authentication-related structure for external apps and the local UI
app/api/auth/
โโโ signup/route.ts -> ์น์ฌ์ดํธ ํ์๊ฐ์
/ website signup
โโโ login/route.ts -> ์น์ฌ์ดํธ ๋ก๊ทธ์ธ / website login
โโโ logout/route.ts -> ๋ก๊ทธ์์ / logout
โโโ refresh/route.ts -> refresh_token์ผ๋ก access token์ฌ๋ฐ๊ธ
| -> Reissuing an access token using a refresh token
โโโ me/route.ts -> ๋ก๊ทธ์ธ ์ฒดํฌ / Login check
app/auth/
โโโ signup/page.tsx -> ํ์๊ฐ์
UI / Sign-up UI
โโโ login/page.tsx -> ๋ก๊ทธ์ธ UI / Login UI
oauth/authorize/route.ts -> OAuth ๋์ ํ๋ฉด / OAuth consent screen
oauth/token/route.ts -> access_token ๋ฐ๊ธ / Issuance of an access token
lib/auth.ts -> ์ค์ ์ ์ ๊ฒ์ฆ ๋ก์ง (getUser, ์ธ์
ํ์ธ)
-> Actual user verification logic (getUser, session check)
๐ ์ธ์ฆ๊ด๋ จ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ
์ด๋ธ
Authentication-related database tables
users -> ํ์๊ฐ์
, ์ ์ ์ ๋ณด (username, password hash, mastodon_id ๋ฑ)
User registration, user information
(username, password hash, mastodon_id, etc.)
sessions -> ์น์ฌ์ดํธ ๋ก๊ทธ์ธ ์ธ์
(refresh_token ์ฟ ํค)
Website login session (refresh_token cookie)
oauth_apps -> ์ธ๋ถ ์ฑ ๋ฑ๋ก (Pinafore, Tusky๊ฐ ๋ฑ๋กํ๋ฉด client_id ๋ฐ๊ธ)
Registering an external app
(a `client_id` is issued once an app like Pinafore or Tusky registers)
oauth_codes -> OAuth authorize ์ฝ๋ (์์)
OAuth authorization code (temporary)
oauth_tokens -> access_token, refresh_token (์ธ๋ถ ์ฑ ๋ก๊ทธ์ธ ์๋ฃ ํ)
access_token, refresh_token (after external app login is complete)
๐ ํ์๊ฐ์ / join the membership
— ๊ธฐ๋ณธ ๋์ ํ๋ฆ์ ์๋์ ๊ฐ์ต๋๋ค.
The basic operational flow is as follows.
โ๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง / Database Schema
CREATE TABLE users (
username TEXT PRIMARY KEY,
display_name TEXT DEFAULT '',
summary TEXT DEFAULT '',
private_key TEXT,
public_key TEXT,
email TEXT UNIQUE,
password_hash TEXT,
email_verified INTEGER DEFAULT 0,
verification_token TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
, mastodon_id TEXT);
โ๏ธ ๋ผ์ฐํธ ์คํ / Execute route
https://aloy-horizon.duckdns.org/auth/signup
โ๏ธ ํ์๊ฐ์ ํ๋ฉด / Sign-up Screen

โ๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ์ฅ ํ์ธ / Confirm Verify database storage
sqlite> select username,display_name,email from users where username='hello';
hello|hello_display|hello@test.com
sqlite>
โ๏ธ ํค ์์ฑ ํ์ธ / Confirm Key Generation
... path... /hello % pwd
... path... /data/keys/hello
hello % ls
private.pem public.pem
โ๏ธ app/auth/signup/page.tsx – UI
... ...
async function handleSignup(e: React.FormEvent) {
e.preventDefault();
setError('');
const res = await fetch('/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, password, display_name: displayName })
});
const data = await res.json();
if (!res.ok) {
setError(data.error || 'ํ์๊ฐ์
์คํจ/Join failed');
return;
}
alert(`โ
ํ์๊ฐ์
์ฑ๊ณต/join membership success! ${data.username} ์ด์ ๋ก๊ทธ์ธ / Already login`);
router.push('/auth/login');
}
... ...
return (
<div style={{ maxWidth: 400, margin: '80px auto', padding: 20 }}>
<h1>๐ ํ์๊ฐ์
/ Signup- myapp39</h1>
<form onSubmit={handleSignup}
style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<input placeholder="username (์๋ฌธ, ์ซ์, _)" value={username} onChange={e=>setUsername(e.target.value)} style={{ padding: 10, fontSize: 16 }} required />
... ...
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit" style={{ padding: 12, fontSize: 16, background: '#2b90d9', color: 'white', border: 0, borderRadius: 8 }}>
ํ์๊ฐ์
/ Signup
</button>
</form>
<p style={{ marginTop: 20 }}>
<a href="/auth/login">๋ก๊ทธ์ธ / Login</a>
</p>
</div>
);
... ...
A.ํผ ์ ์ถ๋ฐฉ์ง – ์ด ์ฝ๋๊ฐ ์์ด์ผ ํ์ด์ง ์ด๋ ํ์์ด ์ผ์ด๋์ง ์์
Preventing form submission โ this code is required to ensure the page does not navigate away.
e.preventDefault();
B.API ๋ผ์ฐํธ๋ก ํผ ๋ฐ์ดํฐ ์ ์ก
Sending form data to an API route
async function handleSignup(e: React.FormEvent) {
... ...
const res = await fetch('/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, password, display_name: displayName })
});
... ...
}
... ...
<form onSubmit={handleSignup} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
... ...
<button type="submit" style={{ padding: 12, fontSize: 16, background: '#2b90d9', color: 'white', border: 0, borderRadius: 8 }}>
ํ์๊ฐ์
/ Signup
</button>
... ...
</form>
C.API์๋ฌ ๋ฐ์์ UI์ ์๋ฌํ์
Display an error in the UI when an API error occurs.
... ...
const data = await res.json();
if (!res.ok) {
setError(data.error || 'ํ์๊ฐ์
์คํจ/Join failed');
return;
}
... ...
{error && <p style={{ color: 'red' }}>{error}</p>}
โ๏ธ app/api/auth/signup/route.ts – API
— ๋ฐ์ดํฐ ์ฒดํฌ ๋ฐ ์ ์ฅ / Data check and storage
... ...
export async function POST(req: Request) {
try {
const { username, email, password, display_name } = await req.json();
if (!username || !email || !password) {
// error : No required input values
return NextResponse.json({ error: 'ํ์ ์
๋ ฅ๊ฐ ์์' }, { status: 400 });
}
if (username.length < 3 || !/^[a-zA-Z0-9_]+$/.test(username)) {
// error : Usernames must be at least 3 characters long and consist only of English letters and numbers.
return NextResponse.json({ error: 'username์ 3์ ์ด์ ์๋ฌธ, ์ซ์๋ง' }, { status: 400 });
}
if (password.length < 6) {
// error : Password must be at least 6 characters long
return NextResponse.json({ error: '๋น๋ฐ๋ฒํธ 6์ ์ด์' }, { status: 400 });
}
// ์ค๋ณต ์ฒดํฌ / Duplicate check
if (getUser(username)) {
// error : Username already exists
return NextResponse.json({ error: '์ด๋ฏธ ์กด์ฌํ๋ username' }, { status: 409 });
}
if (getUserByEmail(email)) {
// error : Email already exists!
return NextResponse.json({ error: '์ด๋ฏธ ์กด์ฌํ๋ email!' }, { status: 409 });
}
const passwordHash = await hashPassword(password);
createUser(username, email, passwordHash, display_name || username);
console.log(`โ
ํ์๊ฐ์
/join the membership: ${username} / ${email}`);
return NextResponse.json({ ok: true, username }, { status: 201 });
} catch (e) {
console.error('signup ์๋ฌ / signup error', e);
return NextResponse.json({ error: '์๋ฒ ์๋ฌ / server error' }, { status: 500 });
}
}
1) ํจ์ค์๋๋ฅผ hashPasswordํจ์๋ก ์ํธํ ํฉ๋๋ค.
Encrypt the password using the hashPassword function.
const passwordHash = await hashPassword(password);
2) UI์์ ์ ๋ฌ ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํ๊ณ ํค ํ์ผ ์์ฑ
Save data received from the UI to the database and generate a key file
createUser(username, email, passwordHash, display_name || username);
โ๏ธ lib/auth.ts(hashPassword,createUser)
— hashPassword ํจ์์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
The hashPassword function is as follows:
import bcrypt from 'bcrypt';
... ...
export async function hashPassword(password: string) {
return await bcrypt.hash(password, 10);
}
1)hashPasswordํจ์์ ํ๋ผ๋ฉํฐ๋ก ์ ๋ฌ๋ฐ์ ๋ด์ฉ์ bcrypt๋ชจ๋๋ก ์ํธํ ํฉ๋๋ค.
The content passed as a parameter to the hashPassword function is encrypted using the bcrypt module.
— createUser ํจ์์ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
The structure of the createUser function is as follows:
import db from './db';
... ...
export function createUser(username: string, email: string, passwordHash: string, displayName: string = '') {
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
const userKeyDir = path.join(process.cwd(), `data/keys/${username}`);
if (!fs.existsSync(userKeyDir)) fs.mkdirSync(userKeyDir, { recursive: true });
fs.writeFileSync(path.join(userKeyDir, 'private.pem'), privateKey);
fs.writeFileSync(path.join(userKeyDir, 'public.pem'), publicKey);
console.log(`โ
ํค ํ์ผ ์ ์ฅ/save key file: data/keys/${username}/`);
// return db.prepare(`
// INSERT INTO users (username, display_name, summary, private_key, public_key, email, password_hash, email_verified, created_at)
// VALUES (?,?, '',?,?,?,?, 1, CURRENT_TIMESTAMP)
// `).run(username, displayName || username, privateKey, publicKey, email, passwordHash);
// โ
myapp58 - mastodon_id ์์ฑ / create mastodon_id
const mastodonId = crypto.randomUUID().slice(0, 8);
return db.prepare(`
INSERT INTO users (username, display_name, summary, private_key, public_key, email, password_hash, email_verified, mastodon_id, created_at)
VALUES (?,?, '',?,?,?,?, 1, ?, CURRENT_TIMESTAMP)
`).run(username, displayName || username, privateKey, publicKey, email, passwordHash, mastodonId);
}
1)crypto๋ชจ๋๋ก ํคํ์ผ ์์ฑํ๋ ๋ถ๋ถ์
๋๋ค.
This part handles the generation of key files using the crypto module.
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
const userKeyDir = path.join(process.cwd(), `data/keys/${username}`);
if (!fs.existsSync(userKeyDir)) fs.mkdirSync(userKeyDir, { recursive: true });
fs.writeFileSync(path.join(userKeyDir, 'private.pem'), privateKey);
fs.writeFileSync(path.join(userKeyDir, 'public.pem'), publicKey);
A.ํค 2๊ฐ ์์ฑ / Generate 2 keys
1. ์ฝ๋์ค๋ช
/ Code Explanation
modulusLength: 2048
- ํค ๊ธธ์ด. 2048๋นํธ. ActivityPub ํ์ค. ๊ธธ์๋ก ์์ , ๋๋ฆผ.
Key length. 2048 bits. ActivityPub standard. Longer keys are more secure, but slower.
publicKeyEncoding: spki, pem
- ๊ณต๊ฐํค ์ ์ฅ ํ์ / Public Key Storage Format
privateKeyEncoding: pkcs8, pem
- ๊ฐ์ธํค ์ ์ฅ ํ์ / Private Key Storage Format
2. ๋ค์๊ณผ ๊ฐ์ ํ์์ผ๋ก ์ ์ฅ / Save in the following format
privateKey = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADAN..."
publicKey = "-----BEGIN PUBLIC KEY-----\nMIIBIjAN..."
B.๋๋ ํ ๋ฆฌ ๋ง๋ค๊ธฐ / Create a directory
const userKeyDir = path.join(process.cwd(), `data/keys/${username}`);
if (!fs.existsSync(userKeyDir)) fs.mkdirSync(userKeyDir, { recursive: true });
# ---- ----
1.์ฝ๋์ค๋ช
/ Code Explanation
process.cwd() = ํ๋ก์ ํธ ๋ฃจํธ / Project root (/Users/.../myapp61)
path = data/keys/bob(username)
๊ฒฝ๋ก์ ๋๋ ํ ๋ฆฌ๊ฐ ์์ผ๋ฉด mkdir -p ๋ก ๋๋ ํ ๋ฆฌ ์์ฑ
If the directory does not exist in the path, create it using `mkdir -p`.
2.๋๋ ํ ๋ฆฌ ์์ฑ ์์ / Example of directory creation
data/keys/
โโโ bob/
โโโ alice/
โโโ charlie/
C.ํ์ผ๋ก ์ ์ฅ / Save as a file
fs.writeFileSync(path.join(userKeyDir, 'private.pem'), privateKey);
fs.writeFileSync(path.join(userKeyDir, 'public.pem'), publicKey);
# ---- ----
1. ์ฝ๋์ค๋ช
/ Code Explanation
userKeyDir(์ ์ฒด๊ฒฝ๋ก)์๋ private.pem publick.pemํ์ผ์ ์์ฑํจ.
Generates `private.pem` and `public.pem` files under `userKeyDir` (full path).
D. ๋ฐ์ดํฐ๋ฒ ์ด์ค์๋ ํค ์ ์ฅ / Storing keys in the database as well
return db.prepare(`
INSERT INTO users (... ... private_key, public_key, ... ...)
VALUES (?,?, '',?,?,?,?, 1, ?, CURRENT_TIMESTAMP)
`).run(... ..., privateKey, publicKey, ... ...);
2)๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ ๋ถ๋ถ์
๋๋ค.
This is the part that stores information in the database.
return db.prepare(`
INSERT INTO users (username, display_name, summary, private_key, public_key, email, password_hash, email_verified, mastodon_id, created_at)
VALUES (?,?, '',?,?,?,?, 1, ?, CURRENT_TIMESTAMP)
`).run(username, displayName || username, privateKey, publicKey, email, passwordHash, mastodonId);
1)์์ ํค์ ํจ๊ป ์ ๋ฌ๋ฐ์ ํ์๊ฐ์
์ ๋ณด๋ฅผ ๋ฐ์ดํฐ ๋ฒ ์ด์ค์ ์ ์ฅํฉ๋๋ค.
Store the user registration information received along with the key mentioned above in the database.
๐ก lib/db.ts ํ์ผ์ค๋ช
์ ์๋ตํฉ๋๋ค.
I will skip the explanation for the lib/db.ts file.
๋งํ๋ณต์ 21์ฅ 22์ / Matthew 21: 22
“๋ํฌ๊ฐ ๊ธฐ๋ํ ๋์ ๋ฌด์์ด๋ ์ง ๋ฏฟ๊ณ ๊ตฌํ๋ ๊ฒ์ ๋ค ๋ฐ์ผ๋ฆฌ๋ผ ํ์๋๋ผ”
“And everything you ask in prayer, believing, you will receive.”