[nextjs]SNS Server-51(myapp61+)[Website Login]ย 

๐Ÿ‘‰๐Ÿป ์ด ๋ถ€๋ถ„์—์„œ๋Š” ์›น์‚ฌ์ดํŠธ ๋กœ๊ทธ์ธ ๋ถ€๋ถ„์˜ ๊ตฌ์กฐ๋ฅผ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.
In this section, we examine the structure of the website’s login area.

๐Ÿ‘‰๐Ÿป ์ด๋ฒˆ ํŒŒํŠธ์—์„œ๋Š” ์›น์‚ฌ์ดํŠธ ๋ถ€๋ถ„์˜ ๋กœ๊ทธ์ธ ๊ตฌ์กฐ๋งŒ ์‚ดํŽด๋ณด๊ณ  ์™ธ๋ถ€์•ฑ ๋ถ€๋ถ„์˜ ๋กœ๊ทธ์ธ์€ ๋‹ค์Œ ํฌ์ŠคํŠธ์—์„œ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.
In this part, we will look only at the login structure for the website; the login process for external apps will be covered in the next post.

๐Ÿ“ ์™ธ๋ถ€ ์•ฑ ๋ฐ ๋กœ์ปฌ 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)

๐Ÿ“ ์›น์‚ฌ์ดํŠธ ๋กœ๊ทธ์ธ / Website Login

โœ”๏ธ ๋ผ์šฐํŠธ / route

https://aloy-horizon.duckdns.org/auth/login

โœ”๏ธ ๋กœ๊ทธ์ธ ์ฒดํฌ ํ•จ์ˆ˜ / Login check function

  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();
  }, []);

1)๋ผ์šฐํŠธ ์‹คํ–‰์‹œ ํ•œ๋ฒˆ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
It is executed once when the route is triggered.

2)/api/auth/me ๋ผ์šฐํŠธ๋ฅผ ์‹คํ–‰ํ•ด์„œ(์ฟ ํ‚คํฌํ•จ ์ „์†ก -> credentials :’include’) ์—‘์„ธ์Šค ํ† ํฐ์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.
Execute the /api/auth/me route (sending it with cookiesโ€”i.e., credentials: 'include') to update the access token.

const res = await fetch('/api/auth/me', { credentials: 'include' });

3)/api/auth/me ๋ผ์šฐํŠธ์—์„œ ๋ฆฌํ„ด๋˜๋Š” username์„ currentUser๋ณ€์ˆ˜์— ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
Set the username returned from the /api/auth/me route to the currentUser variable.

... ...
 const [currentUser, setCurrentUser] = useState('');
... ...        
       if (res.ok) {
          const data = await res.json();
          if (data.username) {
            setCurrentUser(data.username);
          }
        }

4)currentUser๊ฐ€ ์—†์œผ๋ฉด ์ฆ‰ ๊ธฐ์กด์— ๋กœ๊ทธ์ธ๋˜์–ด ์žˆ์ง€ ์•Š์€ ๊ฒฝ์šฐ ์•„๋ž˜ UI์—์„œ ๋กœ๊ทธ์ธ ํผ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.
If currentUser is absentโ€”meaning no user is currently logged inโ€”the UI below displays a login form.

      {currentUser ? (
        <>
          <p style={{ color: 'green', fontSize: 18 }}>ํ˜„์žฌ ๋กœ๊ทธ์ธ: <strong>{currentUser}</strong></p>
          {next && next !== '/' ? (
            <>
              <p>์Šน์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค / You need to go to the approval page.</p>
              <button
                onClick={() => { window.location.href = next; }}
                style={{ marginTop: 20, padding: 12, width: '100%', background: '#6364ff', color: 'white', border: 0, borderRadius: 8, fontSize: 16 }}
              >
                ๐Ÿ” ์Šน์ธ ํŽ˜์ด์ง€๋กœ ๊ฐ€๊ธฐ / Go to Approval Page
              </button>
            </>
          ) : (
            <p>์ด๋ฏธ ๋กœ๊ทธ์ธ๋จ / Already logged in</p>
          )}
          <button
            onClick={handleLogout}
            style={{ marginTop: 20, padding: 12, width: '100%', background: '#ff4444', color: 'white', border: 0, borderRadius: 8, fontSize: 16 }}
          >
            ๋กœ๊ทธ์•„์›ƒ / Log out
          </button>
          <p style={{ marginTop: 20 }}><a href="/">ํ™ˆ์œผ๋กœ ๊ฐ€๊ธฐ / Go to Home</a></p>
        </>
      ) : ( ... )}
Login Form

โœ”๏ธ ๋กœ๊ทธ์ธ ํ•จ์ˆ˜ / Login function

async function handleLogin(e: React.FormEvent) {
    e.preventDefault();
    setError('');

    try {
      const res = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ email: loginId, username: loginId, password })
      });

      const data = await res.json();
      if (!res.ok) {
        setError(data.error || '๋กœ๊ทธ์ธ ์‹คํŒจ/Login failed');
        return;
      }

      localStorage.setItem('username', data.username || loginId);

      console.log('โœ… ๋กœ๊ทธ์ธ ์„ฑ๊ณต / login success next:', next);

      // - router.push ๋Œ€์‹  window.location.href์‚ฌ์šฉ(๋ Œ๋”๋ง ๋”œ๋ ˆ์ด ๋•Œ๋ฌธ์—) 
      //  Use window.location.href instead of router.push (due to rendering delays).
      if (next && next !== '/') {
        if (next.startsWith('http')) {
          window.location.href = next;
        } else {
          window.location.href = next;
        }
      } else {
        window.location.href = '/';
      }
    } catch (err) {
      setError('๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ / Network error');
    }
  }

1)/api/auth/login ๋ผ์šฐํŠธ๋กœ ๋กœ๊ทธ์ธ ํผ์—์„œ ์ž…๋ ฅ๋œ ๊ฐ’์„ ์ „์†กํ•ด์„œ ๋กœ๊ทธ์ธ์„ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
Log in by sending the values โ€‹โ€‹entered in the login form to the /api/auth/login route.

      const res = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ email: loginId, username: loginId, password })
      });

A.app/api/auth/login/route.ts ๋ผ์šฐํŠธ์—์„œ ๋กœ๊ทธ์ธ ์ •๋ณด๊ฐ€ ์ผ์น˜ํ•˜๋ฉด์ฟ ํ‚ค์— ํ† ํฐ์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
In the app/api/auth/login/route.ts route, if the login credentials match, the token is stored in a cookie.

— ์ฟ ํ‚ค์— ์ €์žฅ๋  ํ† ํฐ์€ ์—‘์„ธ์Šคํ† ํฐ๊ณผ ๋ฆฌํ”„๋ ˆ์‹œํ† ํฐ์ž…๋‹ˆ๋‹ค.
The tokens to be stored in the cookie are the access token and the refresh token.

   // โœ… 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',//'lax', // ๋‹ค๋ฅธ ์‚ฌ์ดํŠธ์—์„œ POST์š”์ฒญ์‹œ ์ฟ ํ‚ค ์•ˆ๋ณด๋ƒ„ / Cookies are not sent during POST requests from other sites.
      maxAge: 60*15, // 15๋ถ„ / 15minute
      path: '/'
    });

    // โœ… Refresh Token - 7์ผ! - /api/auth/refresh ์—์„œ๋งŒ ์‚ฌ์šฉ
    // Refresh Token - 7 days! - Used only at /api/auth/refresh
    res.cookies.set('refresh_token', refreshToken, {
      httpOnly: true,
      secure: IS_PROD, // only https 
      sameSite: 'lax',//'lax', // ๋‹ค๋ฅธ ์‚ฌ์ดํŠธ์—์„œ POST์š”์ฒญ์‹œ ์ฟ ํ‚ค ์•ˆ๋ณด๋ƒ„ / Cookies are not sent during POST requests from other sites.
      maxAge: 60*60*24*7, // 7์ผ / 7days
      path: '/'
    });
Refresh Token,Access Token

2)๋กœ๊ทธ์ธ์ด ์„ฑ๊ณตํ•˜๋ฉด ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ๋ธŒ๋ผ์šฐ์ € ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
Upon successful login, the username is saved to the browser’s local storage.

const data = await res.json();
... ...
localStorage.setItem('username', data.username || loginId);

โœ”๏ธ ๋กœ๊ทธ์•„์›ƒ ํ•จ์ˆ˜ / Logout function

  async function handleLogout() {
    await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
    localStorage.removeItem('token');
    localStorage.removeItem('username');
    setCurrentUser('');
    window.location.href = '/auth/login';
  }

1)/api/auth/logout๋ผ์šฐํŠธ๋ฅผ ์‹คํ–‰์‹œ์ผœ์„œ ์ฟ ํ‚ค์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ํ…Œ์ด๋ธ”์—์„œ ๋กœ๊ทธ์ธํ•œ ์ •๋ณด๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.
Execute the /api/auth/logout route to remove login information from both the cookies and the database table.

1.๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์‚ญ์ œ / Delete from database
- app/api/auth/logout/route.ts
deleteSessionByRefreshToken(refreshToken); 

- lib/auth.ts
export function deleteSessionByRefreshToken(refreshToken: string) {
  return db.prepare('DELETE FROM sessions WHERE refresh_token =?').run(refreshToken);
}

- app/api/auth/logout/route.ts
 db.prepare('DELETE FROM oauth_tokens WHERE access_token =?').run(decodeURIComponent(tokenMatch[1]));

2.ํ† ํฐ ์ดˆ๊ธฐํ™” / Token Initialization
- app/api/auth/logout/route.ts
  res.cookies.set('token', '', { maxAge: 0, path: '/' });
  res.cookies.set('refresh_token', '', { maxAge: 0, path: '/' });

A.sessionsํ…Œ์ด๋ธ”์€ ๋กœ์ปฌ UI์šฉ์œผ๋กœ ์‚ฌ์šฉ๋˜๊ณ  oauth_tokens๋Š” ์™ธ๋ถ€์•ฑ ์šฉ์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
The sessions table is used for the local UI, while oauth_tokens is used for external apps.

2)fetch๊ฐ€ ๋๋‚˜๋ฉด ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€์—์„œ username์„ ์‚ญ์ œํ•˜๊ณ  currentUser๋ณ€์ˆ˜๋ฅผ ์ดˆ๊ธฐํ™” ํ•ฉ๋‹ˆ๋‹ค.
Once the fetch operation is complete, remove the username from local storage and reset the currentUser variable.

    localStorage.removeItem('token');
    localStorage.removeItem('username');
    setCurrentUser('');

3)๋กœ๊ทธ์•„์›ƒ์ด ์™„๋ฃŒ๋˜๋ฉด /auth/login(https://aloy-horizon.duckdns.org/auth/login) ๋ผ์šฐํŠธ๋ฅผ ์‹คํ–‰ ์‹œ์ผœ์„œ ๋กœ๊ทธ์ธ์ •๋ณด ์ž…๋ ฅํผ UI๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.
Once logout is complete, the /auth/login (https://aloy-horizon.duckdns.org/auth/login) route is triggered to display the login form UI.

{currentUser ? (
... ...
) : (
        <>
          {next && next !== '/' && (
            <p style={{ background: '#f5f5ff', padding: 10, borderRadius: 5, fontSize: 13 }}>
              ๋กœ๊ทธ์ธ ํ›„ <code>{next.substring(0, 60)}...</code>๋กœ ์ด๋™!
            </p>
          )}
          <form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 20 }}>
            <input placeholder="email or username" value={loginId} onChange={e => setLoginId(e.target.value)} style={{ padding: 10, fontSize: 16 }} required />
            <input type="password" placeholder="password" value={password} onChange={e => setPassword(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: '#6364ff', color: 'white', border: 0, borderRadius: 8 }}>
              ๋กœ๊ทธ์ธ / Login
            </button>
          </form>
          <p style={{ marginTop: 20 }}><a href="/auth/signup">ํšŒ์›๊ฐ€์ž…/Signup</a></p>
        </>
      )}
Logout

โœ”๏ธ ํŽ˜์ด์ง€์ด๋™ / Navigate to page

... ...
const next = searchParams.get('next') || '/';
... ... 
     if (next && next !== '/') {
        if (next.startsWith('http')) {
          window.location.href = next;
        } else {
          window.location.href = next;
        }
      } else {
        // โœ… myapp61+ 
        // ์ง€๊ธˆ์€ proxy.ts์— next์„ค์ •์ด ์—…์–ด์„œ ๋ฌด์กฐ๊ฑด '/'๋ผ์šฐํŠธ ์‹คํ–‰๋จ
        // Currently, there is no 'next' configuration in proxy.ts, so the '/' route is always executed.
        window.location.href = '/';
      }

1)์ง€๊ธˆ์€ proxy.ts๋ถ€๋ถ„์— next์„ค์ •์ด ์—†์–ด์„œ ๋กœ๊ทธ์ธ ์™„๋ฃŒํ›„ ๋ฌด์กฐ๊ฑด ‘/’๋ผ์šฐํŠธ๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
Currently, there is no next configuration in proxy.ts, so the / route is always executed after a successful login.

๋งˆํƒœ๋ณต์Œ 21์žฅ 22์ ˆ / Matthew 21: 22

“๋„ˆํฌ๊ฐ€ ๊ธฐ๋„ํ•  ๋•Œ์— ๋ฌด์—‡์ด๋“ ์ง€ ๋ฏฟ๊ณ  ๊ตฌํ•˜๋Š” ๊ฒƒ์€ ๋‹ค ๋ฐ›์œผ๋ฆฌ๋ผ ํ•˜์‹œ๋‹ˆ๋ผ”

“And everything you ask in prayer, believing, you will receive.”

Leave a Reply