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):
|
||||
username: str
|
||||
email: EmailStr
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
"""Input schema — strict EmailStr validation for user-supplied email."""
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
|
||||
class UserResponse(UserBase):
|
||||
"""Response schema — email is serialised as-is from DB (no re-validation)."""
|
||||
id: int
|
||||
email: str
|
||||
created_at: datetime
|
||||
|
||||
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 || '';
|
||||
|
||||
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 {
|
||||
private baseUrl: string;
|
||||
|
||||
@ -61,15 +53,17 @@ class ApiClient {
|
||||
}
|
||||
|
||||
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`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ username, password: hashedPassword }),
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user