251206:1710 specs: frontend plan P1,P3 wait Verification
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2025-12-06 17:10:56 +07:00
parent be3b71007a
commit 5c49bac772
40 changed files with 977 additions and 244 deletions

View File

@@ -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",
});
});