[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