[nextjs]SNS Server-51(myapp61+)[Signup,nextjs project upgrade,]ย 

๐Ÿ‘‰๐Ÿป ์—ฌ๊ธฐ์„œ๋Š” ํšŒ์›๊ฐ€์ž… ๋ถ€๋ถ„์˜ ๊ตฌ์กฐ์— ๋Œ€ํ•ด์„œ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.
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.”

Leave a Reply