fix: 로컬 네트워크 접근 시 로그인 불가 문제 해결
- frontend: crypto.subtle(Secure Context 전용)을 Next.js Route Handler로 대체
- src/app/api/auth/login/route.ts 신규 생성
- Node.js crypto.createHash('sha256')로 서버사이드 해싱
- http://192.168.x.x 등 insecure HTTP 환경에서도 동작
- api.ts에서 클라이언트 사이드 hashPassword() 제거
- backend: UserResponse 응답 직렬화 시 500 에러 수정
- UserResponse.email을 EmailStr → str로 변경
- admin@local 처럼 TLD 없는 내부 이메일도 응답 가능
- UserCreate.email은 EmailStr 유지(입력 검증은 엄격하게)
This commit is contained in:
parent
e8eabe61ab
commit
efbe0f5c3e
@ -7,15 +7,18 @@ from pydantic import BaseModel, EmailStr
|
|||||||
|
|
||||||
class UserBase(BaseModel):
|
class UserBase(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
email: EmailStr
|
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(UserBase):
|
class UserCreate(UserBase):
|
||||||
|
"""Input schema — strict EmailStr validation for user-supplied email."""
|
||||||
|
email: EmailStr
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
|
||||||
class UserResponse(UserBase):
|
class UserResponse(UserBase):
|
||||||
|
"""Response schema — email is serialised as-is from DB (no re-validation)."""
|
||||||
id: int
|
id: int
|
||||||
|
email: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|||||||
46
frontend/src/app/api/auth/login/route.ts
Normal file
46
frontend/src/app/api/auth/login/route.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Next.js Route Handler for login.
|
||||||
|
*
|
||||||
|
* Handles SHA-256 password hashing server-side so the login flow works
|
||||||
|
* in non-secure HTTP contexts (e.g. http://192.168.x.x) where the browser
|
||||||
|
* blocks window.crypto.subtle (Web Crypto API requires a Secure Context).
|
||||||
|
*
|
||||||
|
* This handler intercepts /api/auth/login before the next.config rewrite
|
||||||
|
* forwards it to the backend, computes the SHA-256 hash using Node.js
|
||||||
|
* built-in crypto, then proxies the request to the backend.
|
||||||
|
*/
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:8000';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
let body: { username: string; password: string };
|
||||||
|
try {
|
||||||
|
body = await request.json();
|
||||||
|
} catch {
|
||||||
|
return NextResponse.json({ detail: 'Invalid request body' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { username, password } = body;
|
||||||
|
|
||||||
|
// Hash the raw password with SHA-256 (matches backend verify_password expectation)
|
||||||
|
const hashedPassword = createHash('sha256').update(password).digest('hex');
|
||||||
|
|
||||||
|
const backendResponse = await fetch(`${BACKEND_URL}/api/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password: hashedPassword }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await backendResponse.json();
|
||||||
|
const response = NextResponse.json(data, { status: backendResponse.status });
|
||||||
|
|
||||||
|
// Forward the HttpOnly access_token cookie set by the backend
|
||||||
|
const setCookie = backendResponse.headers.get('set-cookie');
|
||||||
|
if (setCookie) {
|
||||||
|
response.headers.set('set-cookie', setCookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
@ -1,13 +1,5 @@
|
|||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || '';
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || '';
|
||||||
|
|
||||||
async function hashPassword(password: string): Promise<string> {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const data = encoder.encode(password);
|
|
||||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
||||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
||||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
|
|
||||||
@ -61,15 +53,17 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async login(username: string, password: string) {
|
async login(username: string, password: string) {
|
||||||
const hashedPassword = await hashPassword(password);
|
// Raw password is sent to the Next.js Route Handler (/api/auth/login),
|
||||||
|
// which hashes it server-side with Node.js crypto and forwards to the backend.
|
||||||
|
// This avoids dependency on window.crypto.subtle, which is only available
|
||||||
|
// in Secure Contexts (HTTPS or localhost) and fails over plain HTTP + LAN IP.
|
||||||
const response = await fetch(`${this.baseUrl}/api/auth/login`, {
|
const response = await fetch(`${this.baseUrl}/api/auth/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify({ username, password: hashedPassword }),
|
body: JSON.stringify({ username, password }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user