x
This commit is contained in:
1 parent
3965f4f4da
commit
70877882b0
82 files changed
+4353
-186
No files matched your search
@@ -0,0 +1,8 @@
|
||||
import prisma from './src/lib/prisma';
|
||||
|
||||
async function main() {
|
||||
const users = await prisma.user.findMany();
|
||||
console.log("Registered Users:", users);
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
@@ -0,0 +1,6 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Menu" ADD COLUMN "rootImage" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "MenuItem" ADD COLUMN "columnImage" TEXT,
|
||||
ADD COLUMN "hoverImage" TEXT;
|
||||
@@ -639,6 +639,7 @@ model Menu {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique // e.g., "header", "footer"
|
||||
payload Json?
|
||||
rootImage String?
|
||||
isActive Boolean @default(true)
|
||||
items MenuItem[]
|
||||
createdAt DateTime @default(now())
|
||||
@@ -652,6 +653,8 @@ model MenuItem {
|
||||
label String
|
||||
url String
|
||||
order Int @default(0)
|
||||
columnImage String?
|
||||
hoverImage String?
|
||||
translations MenuItemTranslation[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -24,19 +24,30 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||
}
|
||||
});
|
||||
if (!menu) {
|
||||
return { name, items: [], payload: null, isActive: false };
|
||||
return { name, items: [], payload: null, rootImage: null, isActive: false };
|
||||
}
|
||||
return menu;
|
||||
})
|
||||
.use(adminOnly)
|
||||
.post("/", async ({ body, set }) => {
|
||||
try {
|
||||
const { name, items } = body as { name: string; items?: { label: string; url: string; order: number }[] };
|
||||
const { name, rootImage, items } = body as {
|
||||
name: string;
|
||||
rootImage?: string | null;
|
||||
items?: { label: string; url: string; order: number; columnImage?: string | null; hoverImage?: string | null }[]
|
||||
};
|
||||
const menu = await prisma.menu.create({
|
||||
data: {
|
||||
name,
|
||||
rootImage,
|
||||
items: items ? {
|
||||
create: items
|
||||
create: items.map(item => ({
|
||||
label: item.label,
|
||||
url: item.url,
|
||||
order: item.order,
|
||||
columnImage: item.columnImage,
|
||||
hoverImage: item.hoverImage
|
||||
}))
|
||||
} : undefined
|
||||
},
|
||||
include: { items: true }
|
||||
@@ -50,25 +61,38 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||
}, {
|
||||
body: t.Object({
|
||||
name: t.String(),
|
||||
rootImage: t.Optional(t.Nullable(t.String())),
|
||||
items: t.Optional(t.Array(t.Object({
|
||||
label: t.String(),
|
||||
url: t.String(),
|
||||
order: t.Number()
|
||||
order: t.Number(),
|
||||
columnImage: t.Optional(t.Nullable(t.String())),
|
||||
hoverImage: t.Optional(t.Nullable(t.String()))
|
||||
})))
|
||||
})
|
||||
})
|
||||
.put("/:name", async ({ params: { name }, body, set }) => {
|
||||
try {
|
||||
const { items, payload, isActive } = body as {
|
||||
const { items, payload, isActive, rootImage } = body as {
|
||||
payload?: any;
|
||||
isActive?: boolean;
|
||||
items: { id?: number; label: string; url: string; order: number; translations?: { langCode: string; label: string }[] }[]
|
||||
rootImage?: string | null;
|
||||
items: {
|
||||
id?: number;
|
||||
label: string;
|
||||
url: string;
|
||||
order: number;
|
||||
columnImage?: string | null;
|
||||
hoverImage?: string | null;
|
||||
translations?: { langCode: string; label: string }[]
|
||||
}[]
|
||||
};
|
||||
let menu = await prisma.menu.findUnique({ where: { name } });
|
||||
|
||||
const updateData: any = {};
|
||||
if (payload !== undefined) updateData.payload = payload;
|
||||
if (isActive !== undefined) updateData.isActive = isActive;
|
||||
if (rootImage !== undefined) updateData.rootImage = rootImage;
|
||||
|
||||
if (!menu) {
|
||||
menu = await prisma.menu.create({ data: { name, ...updateData } });
|
||||
@@ -90,7 +114,13 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||
if (item.id && existingItemIds.includes(item.id)) {
|
||||
await tx.menuItem.update({
|
||||
where: { id: item.id },
|
||||
data: { label: item.label, url: item.url, order: item.order }
|
||||
data: {
|
||||
label: item.label,
|
||||
url: item.url,
|
||||
order: item.order,
|
||||
columnImage: item.columnImage,
|
||||
hoverImage: item.hoverImage
|
||||
}
|
||||
});
|
||||
if (item.translations?.length) {
|
||||
for (const t of item.translations) {
|
||||
@@ -108,6 +138,8 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||
label: item.label,
|
||||
url: item.url,
|
||||
order: item.order,
|
||||
columnImage: item.columnImage,
|
||||
hoverImage: item.hoverImage,
|
||||
translations: item.translations?.length ? {
|
||||
create: item.translations.map(t => ({
|
||||
langCode: t.langCode,
|
||||
@@ -133,11 +165,14 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
|
||||
body: t.Object({
|
||||
payload: t.Optional(t.Any()),
|
||||
isActive: t.Optional(t.Boolean()),
|
||||
rootImage: t.Optional(t.Nullable(t.String())),
|
||||
items: t.Array(t.Object({
|
||||
id: t.Optional(t.Number()),
|
||||
label: t.String(),
|
||||
url: t.String(),
|
||||
order: t.Number(),
|
||||
columnImage: t.Optional(t.Nullable(t.String())),
|
||||
hoverImage: t.Optional(t.Nullable(t.String())),
|
||||
translations: t.Optional(t.Array(t.Object({
|
||||
langCode: t.String(),
|
||||
label: t.String()
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { createHmac } from "crypto";
|
||||
|
||||
function base64url(buf: Buffer): string {
|
||||
return buf.toString("base64")
|
||||
.replace(/=/g, "")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_");
|
||||
}
|
||||
|
||||
function signJWT(payload: object, secret: string): string {
|
||||
const header = { alg: "HS256", typ: "JWT" };
|
||||
const headerStr = base64url(Buffer.from(JSON.stringify(header)));
|
||||
const payloadStr = base64url(Buffer.from(JSON.stringify(payload)));
|
||||
const signature = createHmac("sha256", secret)
|
||||
.update(`${headerStr}.${payloadStr}`)
|
||||
.digest();
|
||||
return `${headerStr}.${payloadStr}.${base64url(signature)}`;
|
||||
}
|
||||
|
||||
const API_BASE = "http://localhost:3051/api";
|
||||
|
||||
async function run() {
|
||||
console.log("=== STEP 1: Login via Auth Route ===");
|
||||
const loginRes = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: "admin", password: "homaroma123" })
|
||||
});
|
||||
|
||||
const loginData = await loginRes.json() as any;
|
||||
console.log("Login status:", loginRes.status);
|
||||
console.log("Login response:", loginData);
|
||||
|
||||
if (!loginData.success) {
|
||||
console.error("Login failed!");
|
||||
return;
|
||||
}
|
||||
|
||||
const loginToken = loginData.token;
|
||||
|
||||
console.log("\n=== STEP 2: Test POST /menus with login token ===");
|
||||
const testMenuPayload = {
|
||||
name: "challenger-test-menu",
|
||||
rootImage: "https://example.com/root.jpg",
|
||||
items: [
|
||||
{
|
||||
label: "Item 1",
|
||||
url: "/item1",
|
||||
order: 1,
|
||||
columnImage: "https://example.com/column1.jpg",
|
||||
hoverImage: "https://example.com/hover1.jpg"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const postRes = await fetch(`${API_BASE}/menus`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${loginToken}`
|
||||
},
|
||||
body: JSON.stringify(testMenuPayload)
|
||||
});
|
||||
|
||||
console.log("POST /menus with login token status:", postRes.status);
|
||||
const postData = await postRes.json();
|
||||
console.log("POST /menus response:", postData);
|
||||
|
||||
// If unauthorized, let's sign tokens with different secret fallbacks to see which works!
|
||||
let workingToken = loginToken;
|
||||
if (postRes.status === 401) {
|
||||
console.log("\n⚠️ Detected JWT Secret Mismatch! Trying manually signed tokens...");
|
||||
const payload = {
|
||||
id: 1,
|
||||
username: "admin",
|
||||
role: "ADMIN"
|
||||
};
|
||||
|
||||
const secrets = [
|
||||
"homaroma_secret_2026_scent",
|
||||
"homaroma_secret_2026_brand",
|
||||
"fallback-secret"
|
||||
];
|
||||
|
||||
for (const secret of secrets) {
|
||||
const token = signJWT(payload, secret);
|
||||
const testRes = await fetch(`${API_BASE}/menus`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(testMenuPayload)
|
||||
});
|
||||
console.log(`Testing secret '${secret}': status = ${testRes.status}`);
|
||||
if (testRes.status !== 401) {
|
||||
console.log(`✅ Success with secret '${secret}'!`);
|
||||
workingToken = token;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n=== STEP 3: Perform correct Menu creation & retrieval ===");
|
||||
// Ensure the menu doesn't exist already by trying to query / delete it first
|
||||
// Let's create it clean
|
||||
const menuName = "challenger-test-" + Date.now();
|
||||
const menuPayload = {
|
||||
name: menuName,
|
||||
rootImage: "https://example.com/challenger-root.png",
|
||||
items: [
|
||||
{
|
||||
label: "Challenger Link",
|
||||
url: "/challenger-link",
|
||||
order: 2,
|
||||
columnImage: "https://example.com/challenger-column.png",
|
||||
hoverImage: "https://example.com/challenger-hover.png"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const createRes = await fetch(`${API_BASE}/menus`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${workingToken}`
|
||||
},
|
||||
body: JSON.stringify(menuPayload)
|
||||
});
|
||||
|
||||
console.log("Creation status:", createRes.status);
|
||||
const createdMenu = await createRes.json() as any;
|
||||
console.log("Created menu:", JSON.stringify(createdMenu, null, 2));
|
||||
|
||||
// Verify DB retrieval via GET /api/menus/:name
|
||||
const getRes = await fetch(`${API_BASE}/menus/${menuName}`);
|
||||
console.log("GET /menus/:name status:", getRes.status);
|
||||
const retrievedMenu = await getRes.json() as any;
|
||||
console.log("Retrieved menu:", JSON.stringify(retrievedMenu, null, 2));
|
||||
|
||||
// Verify that rootImage, columnImage, and hoverImage are correctly stored and retrieved
|
||||
const rootImageOk = retrievedMenu.rootImage === menuPayload.rootImage;
|
||||
const item = retrievedMenu.items?.[0];
|
||||
const columnImageOk = item?.columnImage === menuPayload.items[0].columnImage;
|
||||
const hoverImageOk = item?.hoverImage === menuPayload.items[0].hoverImage;
|
||||
|
||||
console.log(`\nVerification Results:`);
|
||||
console.log(`- rootImage correct: ${rootImageOk} (Expected: ${menuPayload.rootImage}, Got: ${retrievedMenu.rootImage})`);
|
||||
console.log(`- columnImage correct: ${columnImageOk} (Expected: ${menuPayload.items[0].columnImage}, Got: ${item?.columnImage})`);
|
||||
console.log(`- hoverImage correct: ${hoverImageOk} (Expected: ${menuPayload.items[0].hoverImage}, Got: ${item?.hoverImage})`);
|
||||
|
||||
console.log("\n=== STEP 4: Perform PUT update menu ===");
|
||||
const updatePayload = {
|
||||
rootImage: "https://example.com/challenger-root-updated.png",
|
||||
items: [
|
||||
{
|
||||
id: item?.id,
|
||||
label: "Challenger Link Updated",
|
||||
url: "/challenger-link-updated",
|
||||
order: 5,
|
||||
columnImage: "https://example.com/challenger-column-updated.png",
|
||||
hoverImage: "https://example.com/challenger-hover-updated.png"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const updateRes = await fetch(`${API_BASE}/menus/${menuName}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${workingToken}`
|
||||
},
|
||||
body: JSON.stringify(updatePayload)
|
||||
});
|
||||
|
||||
console.log("PUT status:", updateRes.status);
|
||||
const updatedMenu = await updateRes.json() as any;
|
||||
console.log("Updated menu retrieved:", JSON.stringify(updatedMenu, null, 2));
|
||||
|
||||
console.log("\n=== STEP 5: Verify Validation Errors for Incorrect Types ===");
|
||||
const invalidPayload = {
|
||||
name: "challenger-invalid-menu",
|
||||
rootImage: 12345, // Should fail: expected string
|
||||
items: [
|
||||
{
|
||||
label: "Invalid Link",
|
||||
url: "/invalid",
|
||||
order: "not-a-number", // Should fail: expected number
|
||||
columnImage: true // Should fail: expected string
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const invalidRes = await fetch(`${API_BASE}/menus`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${workingToken}`
|
||||
},
|
||||
body: JSON.stringify(invalidPayload)
|
||||
});
|
||||
|
||||
console.log("POST invalid payload status:", invalidRes.status);
|
||||
const invalidData = await invalidRes.json();
|
||||
console.log("POST invalid payload response (expected validation error):", JSON.stringify(invalidData, null, 2));
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
Reference in new issue
Block a user