251206:1710 specs: frontend plan P1,P3 wait Verification
This commit is contained in:
6
frontend/.env.example
Normal file
6
frontend/.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# API Configuration
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3001/api
|
||||
|
||||
# NextAuth Configuration
|
||||
# Generate a secret with `openssl rand -base64 32`
|
||||
AUTH_SECRET=changeme
|
||||
@@ -33,6 +33,7 @@ export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// ตั้งค่า React Hook Form
|
||||
const {
|
||||
@@ -50,6 +51,7 @@ export default function LoginPage() {
|
||||
// ฟังก์ชันเมื่อกด Submit
|
||||
async function onSubmit(data: LoginValues) {
|
||||
setIsLoading(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
// เรียกใช้ NextAuth signIn (Credential Provider)
|
||||
@@ -63,8 +65,7 @@ export default function LoginPage() {
|
||||
if (result?.error) {
|
||||
// กรณี Login ไม่สำเร็จ
|
||||
console.error("Login failed:", result.error);
|
||||
// TODO: เปลี่ยนเป็น Toast Notification ในอนาคต
|
||||
alert("เข้าสู่ระบบไม่สำเร็จ: ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง");
|
||||
setErrorMessage("เข้าสู่ระบบไม่สำเร็จ: ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -73,7 +74,7 @@ export default function LoginPage() {
|
||||
router.refresh(); // Refresh เพื่อให้ Server Component รับรู้ Session ใหม่
|
||||
} catch (error) {
|
||||
console.error("Login error:", error);
|
||||
alert("เกิดข้อผิดพลาดที่ไม่คาดคิด");
|
||||
setErrorMessage("เกิดข้อผิดพลาดที่ไม่คาดคิด กรุณาลองใหม่อีกครั้ง");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -89,9 +90,14 @@ export default function LoginPage() {
|
||||
กรอกชื่อผู้ใช้งานและรหัสผ่านเพื่อเข้าสู่ระบบ
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<CardContent className="grid gap-4">
|
||||
{errorMessage && (
|
||||
<div className="bg-destructive/15 text-destructive text-sm p-3 rounded-md border border-destructive/20">
|
||||
{errorMessage}
|
||||
</div>
|
||||
)}
|
||||
{/* Username Field */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">ชื่อผู้ใช้งาน</Label>
|
||||
@@ -162,4 +168,4 @@ export default function LoginPage() {
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,57 @@ import Credentials from "next-auth/providers/credentials";
|
||||
import { z } from "zod";
|
||||
import type { User } from "next-auth";
|
||||
|
||||
// Schema สำหรับ Validate ข้อมูลขาเข้าอีกครั้งเพื่อความปลอดภัย
|
||||
// Schema for input validation
|
||||
const loginSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api";
|
||||
|
||||
// Helper to parse JWT expiry
|
||||
function getJwtExpiry(token: string): number {
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||
return payload.exp * 1000; // Convert to ms
|
||||
} catch (e) {
|
||||
return Date.now(); // If invalid, treat as expired
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAccessToken(token: any) {
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/auth/refresh`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.refreshToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
const refreshedTokens = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw refreshedTokens;
|
||||
}
|
||||
|
||||
const data = refreshedTokens.data || refreshedTokens;
|
||||
|
||||
return {
|
||||
...token,
|
||||
accessToken: data.access_token,
|
||||
accessTokenExpires: getJwtExpiry(data.access_token),
|
||||
refreshToken: data.refresh_token ?? token.refreshToken,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log("RefreshAccessTokenError", error);
|
||||
|
||||
return {
|
||||
...token,
|
||||
error: "RefreshAccessTokenError",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const {
|
||||
handlers: { GET, POST },
|
||||
auth,
|
||||
@@ -25,55 +70,39 @@ export const {
|
||||
},
|
||||
authorize: async (credentials) => {
|
||||
try {
|
||||
// 1. Validate ข้อมูลที่ส่งมาจากฟอร์ม
|
||||
const { username, password } = await loginSchema.parseAsync(credentials);
|
||||
|
||||
// อ่านค่าจาก ENV หรือใช้ Default (ต้องมั่นใจว่าชี้ไปที่ Port 3001 และมี /api)
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api";
|
||||
|
||||
|
||||
console.log(`Attempting login to: ${baseUrl}/auth/login`);
|
||||
|
||||
// 2. เรียก API ไปยัง NestJS Backend
|
||||
const res = await fetch(`${baseUrl}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
// ถ้า Backend ตอบกลับมาว่าไม่สำเร็จ (เช่น 401, 404, 500)
|
||||
if (!res.ok) {
|
||||
const errorMsg = await res.text();
|
||||
console.error("Login failed:", errorMsg);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. รับข้อมูล JSON จาก Backend
|
||||
// โครงสร้างที่ Backend ส่งมา: { statusCode: 200, message: "...", data: { access_token: "...", user: {...} } }
|
||||
const responseJson = await res.json();
|
||||
|
||||
// เจาะเข้าไปเอาข้อมูลจริงใน .data
|
||||
const backendData = responseJson.data;
|
||||
const backendData = responseJson.data || responseJson;
|
||||
|
||||
// ตรวจสอบว่ามี Token หรือไม่
|
||||
if (!backendData || !backendData.access_token) {
|
||||
console.error("No access token received in response data");
|
||||
console.error("No access token received");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 4. Return ข้อมูล User เพื่อส่งต่อไปยัง JWT Callback
|
||||
// ต้อง Map ชื่อ Field ให้ตรงกับที่ NextAuth คาดหวัง และเก็บ Access Token
|
||||
return {
|
||||
// Map user_id จาก DB ให้เป็น id (string) ตามที่ NextAuth ต้องการ
|
||||
id: backendData.user.user_id.toString(),
|
||||
// รวมชื่อจริงนามสกุล
|
||||
name: `${backendData.user.firstName} ${backendData.user.lastName}`,
|
||||
email: backendData.user.email,
|
||||
username: backendData.user.username,
|
||||
// Role (ถ้า Backend ยังไม่ส่ง role มา อาจต้องใส่ Default หรือปรับ Backend เพิ่มเติม)
|
||||
role: backendData.user.role || "User",
|
||||
role: backendData.user.role || "User",
|
||||
organizationId: backendData.user.primaryOrganizationId,
|
||||
// เก็บ Token ไว้ใช้งาน
|
||||
accessToken: backendData.access_token,
|
||||
refreshToken: backendData.refresh_token,
|
||||
} as User;
|
||||
|
||||
} catch (error) {
|
||||
@@ -84,37 +113,48 @@ export const {
|
||||
}),
|
||||
],
|
||||
pages: {
|
||||
signIn: "/login", // กำหนดหน้า Login ของเราเอง
|
||||
error: "/login", // กรณีเกิด Error ให้กลับมาหน้า Login
|
||||
signIn: "/login",
|
||||
error: "/login",
|
||||
},
|
||||
callbacks: {
|
||||
// 1. JWT Callback: ทำงานเมื่อสร้าง Token หรืออ่าน Token
|
||||
async jwt({ token, user }) {
|
||||
// ถ้ามี user เข้ามา (คือตอน Login ครั้งแรก) ให้บันทึกข้อมูลลง Token
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
token.role = user.role;
|
||||
token.organizationId = user.organizationId;
|
||||
token.accessToken = user.accessToken;
|
||||
return {
|
||||
...token,
|
||||
id: user.id,
|
||||
role: user.role,
|
||||
organizationId: user.organizationId,
|
||||
accessToken: user.accessToken,
|
||||
refreshToken: user.refreshToken,
|
||||
accessTokenExpires: getJwtExpiry(user.accessToken!),
|
||||
};
|
||||
}
|
||||
return token;
|
||||
|
||||
// Return previous token if valid (minus 10s buffer)
|
||||
if (Date.now() < (token.accessTokenExpires as number) - 10000) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// Token expired, refresh it
|
||||
return refreshAccessToken(token);
|
||||
},
|
||||
// 2. Session Callback: ทำงานเมื่อฝั่ง Client เรียก useSession()
|
||||
async session({ session, token }) {
|
||||
// ส่งข้อมูลจาก Token ไปให้ Client ใช้งาน
|
||||
if (token && session.user) {
|
||||
session.user.id = token.id as string;
|
||||
session.user.role = token.role as string;
|
||||
session.user.organizationId = token.organizationId as number;
|
||||
|
||||
session.accessToken = token.accessToken as string;
|
||||
session.refreshToken = token.refreshToken as string;
|
||||
session.error = token.error as string;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: 8 * 60 * 60, // 8 ชั่วโมง
|
||||
maxAge: 24 * 60 * 60, // 24 hours
|
||||
},
|
||||
secret: process.env.AUTH_SECRET,
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
});
|
||||
});
|
||||
|
||||
8
frontend/types/next-auth.d.ts
vendored
8
frontend/types/next-auth.d.ts
vendored
@@ -9,6 +9,8 @@ declare module "next-auth" {
|
||||
organizationId?: number;
|
||||
} & DefaultSession["user"]
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
@@ -16,6 +18,7 @@ declare module "next-auth" {
|
||||
role: string;
|
||||
organizationId?: number;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,5 +28,8 @@ declare module "next-auth/jwt" {
|
||||
role: string;
|
||||
organizationId?: number;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
accessTokenExpires?: number;
|
||||
error?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user