๐๐ป ์ด ๋ถ๋ถ์์๋ ์น์ฌ์ดํธ ๋ก๊ทธ์ธ ๋ถ๋ถ์ ๊ตฌ์กฐ๋ฅผ ์ดํด๋ด
๋๋ค.
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 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: '/'
});

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>
</>
)}

โ๏ธ ํ์ด์ง์ด๋ / 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.”