Add Login and Dashboard
This commit is contained in:
145
src/app/api/auth/login/route.ts
Normal file
145
src/app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { API_BASE, TENANT_KEY, COOKIE_SECURE, COOKIE_SAMESITE } from "@/lib/config";
|
||||
import type { UpstreamLoginResponse } from "@/types/auth";
|
||||
|
||||
const DEBUG_AUTH = (process.env.DEBUG_AUTH ?? "true").toLowerCase() === "true";
|
||||
const TIMEOUT_MS = Number(process.env.LOGIN_TIMEOUT_MS ?? 15000);
|
||||
|
||||
function redact<T extends Record<string, unknown>>(obj: T): T {
|
||||
try {
|
||||
if (!obj || typeof obj !== "object") return obj;
|
||||
const clone = { ...obj } as Record<string, unknown>;
|
||||
if ("password" in clone) clone.password = "***REDACTED***";
|
||||
return clone as T;
|
||||
} catch {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const url = `${API_BASE}/api/authentication/login`;
|
||||
let upstreamStatus = 0;
|
||||
|
||||
try {
|
||||
const body = (await req.json().catch(() => ({}))) as Record<string, unknown>;
|
||||
const printBody = redact(body);
|
||||
|
||||
if (DEBUG_AUTH) {
|
||||
console.log("[auth/login] → upstream", { url, tenant: TENANT_KEY, apiBase: API_BASE, body: printBody });
|
||||
}
|
||||
|
||||
const ac = new AbortController();
|
||||
const t = setTimeout(() => ac.abort(), TIMEOUT_MS);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "X-Tenant": TENANT_KEY },
|
||||
body: JSON.stringify(body),
|
||||
redirect: "manual",
|
||||
signal: ac.signal,
|
||||
}).catch((e) => {
|
||||
const err = e as Error;
|
||||
throw new Error(`Upstream fetch error: ${err.message}`);
|
||||
});
|
||||
clearTimeout(t);
|
||||
|
||||
upstreamStatus = res.status;
|
||||
|
||||
const rawText = await res.text();
|
||||
let data: UpstreamLoginResponse | { _raw?: string };
|
||||
try {
|
||||
data = rawText ? (JSON.parse(rawText) as UpstreamLoginResponse) : ({} as UpstreamLoginResponse);
|
||||
} catch {
|
||||
data = { _raw: rawText };
|
||||
}
|
||||
|
||||
if (DEBUG_AUTH) {
|
||||
const setCookieHeader = res.headers.get("set-cookie");
|
||||
console.log("[auth/login] ← upstream", {
|
||||
status: res.status,
|
||||
hasAccessToken: Boolean((data as UpstreamLoginResponse).access_token),
|
||||
setCookiePresent: Boolean(setCookieHeader),
|
||||
setCookiePreview: setCookieHeader?.slice(0, 160),
|
||||
bodyPreview: rawText.slice(0, 200),
|
||||
});
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json(
|
||||
{ message: (data as { message?: string })?.message ?? "Login failed", upstreamStatus: res.status, upstreamBodyPreview: rawText.slice(0, 400) },
|
||||
{ status: res.status },
|
||||
);
|
||||
}
|
||||
|
||||
const login = data as UpstreamLoginResponse;
|
||||
|
||||
const nextRes = NextResponse.json(
|
||||
{ user: login.user, token_type: login.token_type, expires_at: login.expires_at },
|
||||
{ status: 200 },
|
||||
);
|
||||
|
||||
// user_info cookie
|
||||
try {
|
||||
const userInfo = {
|
||||
userId: login?.user?.userId,
|
||||
email: login?.user?.email,
|
||||
tenantKey: login?.user?.tenantKey,
|
||||
tenantId: login?.user?.tenantId,
|
||||
};
|
||||
const b64 = Buffer.from(JSON.stringify(userInfo)).toString("base64");
|
||||
nextRes.cookies.set("user_info", b64, {
|
||||
httpOnly: false,
|
||||
secure: COOKIE_SECURE,
|
||||
sameSite: COOKIE_SAMESITE,
|
||||
path: "/",
|
||||
expires: login?.expires_at ? new Date(login.expires_at) : undefined,
|
||||
});
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
console.warn("[auth/login] could not set user_info cookie:", err.message);
|
||||
}
|
||||
|
||||
if (login?.access_token && login?.expires_at) {
|
||||
nextRes.cookies.set("access_token", login.access_token, {
|
||||
httpOnly: false,
|
||||
secure: COOKIE_SECURE,
|
||||
sameSite: COOKIE_SAMESITE,
|
||||
path: "/",
|
||||
expires: new Date(login.expires_at),
|
||||
});
|
||||
}
|
||||
|
||||
const setCookieHeader = res.headers.get("set-cookie") ?? "";
|
||||
const parts = setCookieHeader.split(/,(?=\s*[a-zA-Z0-9_\-]+=)/).map((s) => s.trim());
|
||||
const refreshPart = parts.find((p) => p.toLowerCase().startsWith("refresh_token="));
|
||||
|
||||
if (refreshPart) {
|
||||
const match = refreshPart.match(/^refresh_token=([^;]+)/i);
|
||||
if (match) {
|
||||
const refreshValue = decodeURIComponent(match[1]);
|
||||
const expMatch = refreshPart.match(/expires=([^;]+)/i);
|
||||
const expires = expMatch ? new Date(expMatch[1]) : undefined;
|
||||
|
||||
nextRes.cookies.set("refresh_token", refreshValue, {
|
||||
httpOnly: true,
|
||||
secure: COOKIE_SECURE,
|
||||
sameSite: COOKIE_SAMESITE,
|
||||
path: "/",
|
||||
expires,
|
||||
});
|
||||
}
|
||||
} else if (DEBUG_AUTH) {
|
||||
console.warn("[auth/login] No refresh_token in upstream Set-Cookie.");
|
||||
}
|
||||
|
||||
return nextRes;
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
const msg = err.name === "AbortError" ? `Upstream timeout > ${TIMEOUT_MS}ms` : err.message;
|
||||
console.error("[auth/login] ERROR", { message: msg, upstreamStatus });
|
||||
return NextResponse.json(
|
||||
{ message: "Server error", detail: msg, upstreamStatus },
|
||||
{ status: upstreamStatus || 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
86
src/app/api/auth/refresh/route.ts
Normal file
86
src/app/api/auth/refresh/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { API_BASE, COOKIE_SECURE, COOKIE_SAMESITE } from "@/lib/config";
|
||||
import { resolveTenantFromRequest } from "@/lib/server-tenant";
|
||||
import type { UpstreamRefreshResponse } from "@/types/auth";
|
||||
|
||||
const DEBUG_AUTH = (process.env.DEBUG_AUTH ?? "true").toLowerCase() === "true";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const tenantKey = resolveTenantFromRequest(req);
|
||||
const url = `${API_BASE}/api/authentication/refresh`;
|
||||
const refresh = req.cookies.get("refresh_token")?.value;
|
||||
|
||||
try {
|
||||
const r = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Tenant": tenantKey,
|
||||
...(refresh ? { Cookie: `refresh_token=${encodeURIComponent(refresh)}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
const rawText = await r.text();
|
||||
let data: UpstreamRefreshResponse | { _raw?: string };
|
||||
try {
|
||||
data = rawText ? (JSON.parse(rawText) as UpstreamRefreshResponse) : ({} as UpstreamRefreshResponse);
|
||||
} catch {
|
||||
data = { _raw: rawText };
|
||||
}
|
||||
|
||||
if (DEBUG_AUTH) {
|
||||
console.log("[auth/refresh] ← upstream", {
|
||||
status: r.status,
|
||||
hasAccessToken: Boolean((data as UpstreamRefreshResponse)?.access_token),
|
||||
bodyPreview: rawText.slice(0, 160),
|
||||
});
|
||||
}
|
||||
|
||||
if (!r.ok) {
|
||||
return NextResponse.json({ message: (data as { message?: string })?.message ?? "Refresh failed" }, { status: r.status });
|
||||
}
|
||||
|
||||
const nextRes = NextResponse.json(
|
||||
{ token_type: (data as UpstreamRefreshResponse).token_type ?? "Bearer", expires_at: (data as UpstreamRefreshResponse).expires_at },
|
||||
{ status: 200 },
|
||||
);
|
||||
|
||||
if ((data as UpstreamRefreshResponse)?.access_token && (data as UpstreamRefreshResponse)?.expires_at) {
|
||||
nextRes.cookies.set("access_token", (data as UpstreamRefreshResponse).access_token!, {
|
||||
httpOnly: false,
|
||||
secure: COOKIE_SECURE,
|
||||
sameSite: COOKIE_SAMESITE,
|
||||
path: "/",
|
||||
expires: new Date((data as UpstreamRefreshResponse).expires_at!),
|
||||
});
|
||||
}
|
||||
|
||||
const setCookieHeader = r.headers.get("set-cookie") ?? "";
|
||||
const parts = setCookieHeader.split(/,(?=\s*[a-zA-Z0-9_\-]+=)/).map((s) => s.trim());
|
||||
const refreshPart = parts.find((p) => p.toLowerCase().startsWith("refresh_token="));
|
||||
if (refreshPart) {
|
||||
const match = refreshPart.match(/^refresh_token=([^;]+)/i);
|
||||
if (match) {
|
||||
const refreshValue = decodeURIComponent(match[1]);
|
||||
const expMatch = refreshPart.match(/expires=([^;]+)/i);
|
||||
const expires = expMatch ? new Date(expMatch[1]) : undefined;
|
||||
|
||||
nextRes.cookies.set("refresh_token", refreshValue, {
|
||||
httpOnly: true,
|
||||
secure: COOKIE_SECURE,
|
||||
sameSite: COOKIE_SAMESITE,
|
||||
path: "/",
|
||||
expires,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return nextRes;
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
console.error("[auth/refresh] error:", err.message);
|
||||
return NextResponse.json({ message: "Server error", detail: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
47
src/app/api/crm/redeem/route.ts
Normal file
47
src/app/api/crm/redeem/route.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// File: src/app/api/public/redeem/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
// ถ้ามี type RedeemRequest/RedeemResponse อยู่แล้ว ใช้ของโปรเจกต์
|
||||
// import type { RedeemRequest, RedeemResponse } from "@/types/crm";
|
||||
|
||||
const API_BASE = process.env.EOP_PUBLIC_API_BASE ?? "http://127.0.0.1:5063";
|
||||
|
||||
// ถ้ายังไม่มีไฟล์ types/crm ให้ใช้ fallback แบบหลวมๆ (ไม่ใช่ any)
|
||||
type RedeemRequestFallback = Record<string, unknown>;
|
||||
type RedeemResponseFallback = unknown;
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// อ่าน body แบบ unknown แล้วค่อย stringify กลับ — ปลอดภัยกว่า any
|
||||
const body = (await req.json().catch(() => ({}))) as unknown;
|
||||
|
||||
// header จากฝั่ง client
|
||||
const tenant = req.headers.get("x-tenant-key") ?? "default";
|
||||
const idem = req.headers.get("idempotency-key") ?? undefined;
|
||||
|
||||
const r = await fetch(`${API_BASE}/api/v1/loyalty/redeem`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Tenant-Key": tenant,
|
||||
...(idem ? { "Idempotency-Key": idem } : {}),
|
||||
},
|
||||
body: JSON.stringify(body as RedeemRequestFallback),
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
const text = await r.text();
|
||||
return new NextResponse(text, {
|
||||
status: r.status,
|
||||
headers: {
|
||||
"Content-Type": r.headers.get("Content-Type") ?? "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
const err = e as Error;
|
||||
return NextResponse.json(
|
||||
{ message: err.message ?? "Server error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
53
src/app/api/proxy/[...path]/route.ts
Normal file
53
src/app/api/proxy/[...path]/route.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { API_BASE } from "@/lib/config";
|
||||
import { resolveTenantFromRequest } from "@/lib/server-tenant";
|
||||
|
||||
type Ctx = { params: Promise<{ path: string[] }> };
|
||||
|
||||
function buildHeaders(req: NextRequest, tenantKey: string, extra?: Record<string, string>) {
|
||||
const access = req.cookies.get("access_token")?.value;
|
||||
return {
|
||||
"X-Tenant": tenantKey,
|
||||
...(access ? { Authorization: `Bearer ${access}` } : {}),
|
||||
...(extra ?? {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function forward(req: NextRequest, ctx: Ctx, method: string) {
|
||||
const { path } = await ctx.params;
|
||||
const tenantKey = resolveTenantFromRequest(req);
|
||||
const url = `${API_BASE}/${path.join("/")}${req.nextUrl.search}`;
|
||||
|
||||
try {
|
||||
const init: RequestInit = { method, headers: buildHeaders(req, tenantKey) };
|
||||
|
||||
if (method !== "GET" && method !== "HEAD") {
|
||||
const raw = await req.text();
|
||||
init.body = raw;
|
||||
const ct = req.headers.get("content-type");
|
||||
if (ct) (init.headers as Record<string, string>)["Content-Type"] = ct;
|
||||
(init.headers as Record<string, string>)["Accept"] = req.headers.get("accept") ?? "application/json";
|
||||
} else {
|
||||
(init.headers as Record<string, string>)["Accept"] = req.headers.get("accept") ?? "application/json";
|
||||
}
|
||||
|
||||
const r = await fetch(url, init);
|
||||
const text = await r.text();
|
||||
|
||||
return new NextResponse(text, {
|
||||
status: r.status,
|
||||
headers: { "Content-Type": r.headers.get("Content-Type") ?? "application/json" },
|
||||
});
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
console.error("[proxy]", method, url, "error:", err.message);
|
||||
return NextResponse.json({ message: "Proxy error", detail: err.message }, { status: 502 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest, ctx: Ctx) { return forward(req, ctx, "GET"); }
|
||||
export async function POST(req: NextRequest, ctx: Ctx) { return forward(req, ctx, "POST"); }
|
||||
export async function PUT(req: NextRequest, ctx: Ctx) { return forward(req, ctx, "PUT"); }
|
||||
export async function PATCH(req: NextRequest, ctx: Ctx) { return forward(req, ctx, "PATCH"); }
|
||||
export async function DELETE(req: NextRequest, ctx: Ctx) { return forward(req, ctx, "DELETE"); }
|
||||
export async function HEAD(req: NextRequest, ctx: Ctx) { return forward(req, ctx, "HEAD"); }
|
||||
Reference in New Issue
Block a user