diff --git a/03_source/cms_backend/prisma/schema.prisma b/03_source/cms_backend/prisma/schema.prisma index f4fa608..07c4d71 100644 --- a/03_source/cms_backend/prisma/schema.prisma +++ b/03_source/cms_backend/prisma/schema.prisma @@ -46,6 +46,9 @@ model Session { model User { id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + // name String? username String? @unique email String @unique @@ -54,8 +57,6 @@ model User { image String? bucketImage String? admin Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt accounts Account[] sessions Session[] info Json? @@ -70,6 +71,9 @@ model VerificationToken { @@unique([identifier, token]) } +// ---------------------------------------------------------------- +// frontend/src/_mock/_user.ts + // ---------------------------------------------------------------- // REQ0044/near_by_page @@ -99,6 +103,9 @@ model Member { music String[] @default([]) pets String[] @default([]) character String[] @default([]) + // + avatar String @default("") + sex String @default("") } model Order { @@ -118,7 +125,7 @@ model Event { id Int @id @default(autoincrement()) eventDate DateTime title String - joinMembers Member[] // Assuming Member model exists + joinMembers Json[] // Assuming Member model exists price Float currency String duration_m Int @@ -127,6 +134,8 @@ model Event { location String avatar String // Assuming avatar is stored as a file path or URL Order Order[] + Member Member? @relation(fields: [memberId], references: [id]) + memberId Int? } // cms_backend @@ -537,6 +546,8 @@ model UserItem { // username String password String + // + isAdmin Boolean @default(false) } model UserAccountBillingHistory { @@ -1078,3 +1089,96 @@ model Kanban { columns KanbanColumn[] tasks KanbanTask[] } + +// mapped to IEventReview +model EventReview { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + // + name String + rating Float + comment String + helpful Float + avatarUrl String + postedAt DateTime @default(now()) // Assuming IDateValue maps to DateTime + isPurchased Boolean + attachments String[] + // + EventItem EventItem? @relation(fields: [eventItemId], references: [id]) + eventItemId String? +} + +// mapped to IEventItem +model EventItem { + id String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + // + sku String + name String + code String + price Float + taxes Float + tags String[] + sizes String[] + publish String + gender String[] + coverUrl String + images String[] + colors String[] + quantity Int + category String + available Int + totalSold Int + description String + totalRatings Float + totalReviews Int + inventoryType String + subDescription String + priceSale Float? + newLabel Json + saleLabel Json + ratings Json[] + // + eventDate DateTime @default(now()) + joinMembers Json[] + title String + currency String + duration_m Float + ageBottom Float + ageTop Float + location String + avatar String[] + // + reviews EventReview[] +} + +model AppLog { + id String @id @default(uuid()) + timestamp DateTime @default(now()) + // + level Int? @default(1) + message String? @default("") + // + module String? + userId String? + metadata Json? + + @@index([timestamp]) + @@index([level]) + @@index([module]) +} + +model AccessLog { + id String @id @default(uuid()) + timestamp DateTime @default(now()) + // + userId String? @default("") + message String? @default("") + // + metadata Json? + + @@index([timestamp]) + @@index([userId]) +} diff --git a/03_source/cms_backend/prisma/seed.ts b/03_source/cms_backend/prisma/seed.ts index 263e2c5..f003585 100644 --- a/03_source/cms_backend/prisma/seed.ts +++ b/03_source/cms_backend/prisma/seed.ts @@ -25,6 +25,11 @@ import { FileStore } from './seeds/fileStore'; import { userItemSeed } from './seeds/userItem'; import { orderItemSeed } from './seeds/orderItem'; import { invoiceItemSeed } from './seeds/invoiceItem'; +// +import { EventItemSeed } from './seeds/eventItem'; +import { EventReviewSeed } from './seeds/eventReview'; +import { appLogSeed } from './seeds/AppLog'; +import { accessLogSeed } from './seeds/AccessLog'; // // import { Blog } from './seeds/blog'; @@ -44,8 +49,16 @@ import { invoiceItemSeed } from './seeds/invoiceItem'; await userItemSeed; await orderItemSeed; await invoiceItemSeed; + // + await EventReviewSeed; + await EventItemSeed; + // + await appLogSeed; + await accessLogSeed; + // await Blog; // await Mail; // await File; // await Chat; + console.log('seed done'); })(); diff --git a/03_source/cms_backend/prisma/seeds/AccessLog.ts b/03_source/cms_backend/prisma/seeds/AccessLog.ts new file mode 100644 index 0000000..9ff7e44 --- /dev/null +++ b/03_source/cms_backend/prisma/seeds/AccessLog.ts @@ -0,0 +1,36 @@ +import { PrismaClient } from '@prisma/client'; +import { format, parseISO } from 'date-fns'; + +const L_ERROR = 0; +const L_WARN = 1; +const L_INFO = 2; +const L_DEBUG = 3; +const L_TRACE = 4; + +const prisma = new PrismaClient(); + +async function accessLog() { + await prisma.accessLog.upsert({ + where: { id: '0' }, + update: {}, + create: { + userId: '0', + message: 'helloworld', + metadata: { hello: 'world' }, + }, + }); + + console.log('seed AccessLog done'); +} + +const accessLogSeed = accessLog() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); + +export { accessLogSeed }; diff --git a/03_source/cms_backend/prisma/seeds/AppLog.ts b/03_source/cms_backend/prisma/seeds/AppLog.ts new file mode 100644 index 0000000..4a281fe --- /dev/null +++ b/03_source/cms_backend/prisma/seeds/AppLog.ts @@ -0,0 +1,71 @@ +import { PrismaClient } from '@prisma/client'; +import { format, parseISO } from 'date-fns'; + +const L_ERROR = 0; +const L_WARN = 1; +const L_INFO = 2; +const L_DEBUG = 3; +const L_TRACE = 4; + +const prisma = new PrismaClient(); + +async function appLog() { + await prisma.appLog.upsert({ + where: { id: '0' }, + update: {}, + create: { + message: 'helloworld', + level: L_ERROR, + }, + }); + + await prisma.appLog.upsert({ + where: { id: '1' }, + update: {}, + create: { + message: 'warning message', + level: L_WARN, + }, + }); + + await prisma.appLog.upsert({ + where: { id: '2' }, + update: {}, + create: { + message: 'info message', + level: L_INFO, + }, + }); + + await prisma.appLog.upsert({ + where: { id: '3' }, + update: {}, + create: { + message: 'debug message', + level: L_DEBUG, + }, + }); + + await prisma.appLog.upsert({ + where: { id: '4' }, + update: {}, + create: { + message: 'trace message', + level: L_TRACE, + }, + }); + + console.log('seed AppLog done'); +} + +const appLogSeed = appLog() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); + +export { appLogSeed }; diff --git a/03_source/cms_backend/prisma/seeds/Member.ts b/03_source/cms_backend/prisma/seeds/Member.ts index 80bdb9a..a5a02be 100644 --- a/03_source/cms_backend/prisma/seeds/Member.ts +++ b/03_source/cms_backend/prisma/seeds/Member.ts @@ -2,12 +2,43 @@ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); async function member() { - for (let i = 0; i < 100; i++) { + [ + { + email: 'tom@exampl.com', + avatar: `https://plus.unsplash.com/premium_photo-1671656349322-41de944d259b`, + sex: 'M', + }, + { + email: 'may@exampl.com', + avatar: `https://images.unsplash.com/photo-1522075469751-3a6694fb2f61`, + sex: 'F', + }, + { + email: 'june@exampl.com', + avatar: `https://plus.unsplash.com/premium_photo-1723867331866-e112500178a4`, + sex: 'M', + }, + { + email: 'april@exampl.com', + avatar: `https://plus.unsplash.com/premium_photo-1682089894837-e01e5cb8e471`, + sex: 'F', + }, + { + email: 'susan@exampl.com', + avatar: `https://images.unsplash.com/photo-1485893086445-ed75865251e0`, + sex: 'M', + }, + { + email: 'peter@exampl.com', + avatar: `https://plus.unsplash.com/premium_photo-1722945763962-305a5a769cc8`, + sex: 'F', + }, + ].forEach(async (m, i) => { const john = await prisma.member.upsert({ - where: { email: `member${i}@example.com` }, + where: { email: m.email }, update: {}, create: { - email: `member${i}@example.com`, + email: m.email, name: `member_${i}`, age: 20 + i, rank: i % 2 ? 'VIP' : 'NON_VIP', @@ -25,11 +56,42 @@ async function member() { self_introduction: 'Get me know me before you love me. Get me know me before you love me.', music: ['Classic', 'Classic', 'Classic', 'Classic', 'Classic', 'Classic'], pets: ['Classic', 'Classic', 'Classic', 'Classic', 'Classic', 'Classic'], - character: ['Classic', 'Classic', 'Classic', 'Classic', 'Classic', 'Classic'] - } + character: ['Classic', 'Classic', 'Classic', 'Classic', 'Classic', 'Classic'], + avatar: m.avatar, + sex: m.sex, + }, + }); + }); + + for (let i = 0; i < 3; i++) { + const john = await prisma.member.upsert({ + where: { email: `member${i}@example.com` }, + update: {}, + create: { + email: `member${i}@example.com`, + name: `member_${i}`, + age: 20 + i, + rank: i % 2 ? 'VIP' : 'NON_VIP', + verified: i % 3 ? 'NOT_VERIFIED' : 'VERIFIED', + hobbies: ['fishing', 'basketball', 'piano'], + distance: `${40 + Math.random() * 40}km`, + location_area: 'Sai Kung', + greetings: 'Hi, I am ', + gender: 'man', + tall_cm: 172 + Math.random() * 10, + weight_kg: 60 + Math.random() * 50, + occupation: 'doctor', + language: ['English', 'French', 'Chinese'], + education: ['Degree of Computer'], + self_introduction: 'Get me know me before you love me. Get me know me before you love me.', + music: ['Classic', 'Classic', 'Classic', 'Classic', 'Classic', 'Classic'], + pets: ['Classic', 'Classic', 'Classic', 'Classic', 'Classic', 'Classic'], + character: ['Classic', 'Classic', 'Classic', 'Classic', 'Classic', 'Classic'], + avatar: '', + sex: i % 2 ? 'M' : 'F', + }, }); } - console.log('seed member done'); } diff --git a/03_source/cms_backend/prisma/seeds/_mock.ts b/03_source/cms_backend/prisma/seeds/_mock.ts index 2135e0f..0d9b3c9 100644 --- a/03_source/cms_backend/prisma/seeds/_mock.ts +++ b/03_source/cms_backend/prisma/seeds/_mock.ts @@ -78,11 +78,12 @@ export const _mock = { avatar: (index: number) => `${CONFIG.basePath}/assets/images/avatar/avatar-${index + 1}.webp`, travel: (index: number) => `${CONFIG.basePath}/assets/images/travel/travel-${index + 1}.webp`, course: (index: number) => `${CONFIG.basePath}/assets/images/course/course-${index + 1}.webp`, - company: (index: number) => - `${CONFIG.basePath}/assets/images/company/company-${index + 1}.webp`, - product: (index: number) => - `${CONFIG.basePath}/assets/images/m-product/product-${index + 1}.webp`, - portrait: (index: number) => - `${CONFIG.basePath}/assets/images/portrait/portrait-${index + 1}.webp`, + company: (index: number) => `${CONFIG.basePath}/assets/images/company/company-${index + 1}.webp`, + product: (index: number) => `${CONFIG.basePath}/assets/images/m-product/product-${index + 1}.webp`, + portrait: (index: number) => `${CONFIG.basePath}/assets/images/portrait/portrait-${index + 1}.webp`, + // + event: (index: number) => `${CONFIG.basePath}/assets/images/m-product/product-${index + 1}.webp`, }, + // + eventName: (index: number) => _eventNames[index], }; diff --git a/03_source/cms_backend/prisma/seeds/eventItem.ts b/03_source/cms_backend/prisma/seeds/eventItem.ts new file mode 100644 index 0000000..8d35668 --- /dev/null +++ b/03_source/cms_backend/prisma/seeds/eventItem.ts @@ -0,0 +1,277 @@ +import { _mock } from './_mock'; +import { _tags } from './assets'; + +import { PrismaClient, EventReview } from '@prisma/client'; +import { _eventsReview } from './eventReview'; +import { CONFIG } from './global-config'; + +import _ from 'lodash'; + +const prisma = new PrismaClient(); + +const COLORS = ['#FF4842', '#1890FF', '#FFC0CB', '#00AB55', '#FFC107', '#7F00FF', '#000000', '#FFFFFF']; + +const DESCRIPTION = ` +
Specifications
+ + + + + + + + + + + + + + + + + + + + + + + +
CategoryMobile
ManufacturerApple
Warranty12 Months
Serial number358607726380311
Ships fromUnited States
+ +
Event details
+ +
Benefits
+ +
Delivery and returns
+

Your order of $200 or more gets free standard delivery.

+ +

Orders are processed and delivered Monday-Friday (excluding public holidays)

+ +`; + +const getColorSliceForIndex = (index: number) => { + if (index === 0) return COLORS.slice(0, 2); + if (index === 1) return COLORS.slice(1, 3); + if (index === 2) return COLORS.slice(2, 4); + if (index === 3) return COLORS.slice(3, 6); + if (index === 4 || index === 16 || index === 19) return COLORS.slice(4, 6); + if (index === 5 || index === 17) return COLORS.slice(5, 6); + if (index === 6 || index === 18) return COLORS.slice(0, 2); + if (index === 7) return COLORS.slice(4, 6); + if (index === 8) return COLORS.slice(2, 4); + if (index === 9 || index === 11) return COLORS.slice(2, 6); + if (index === 10) return COLORS.slice(3, 6); + if (index === 12) return COLORS.slice(2, 7); + if (index === 13) return COLORS.slice(4, 7); + if (index === 14) return COLORS.slice(0, 2); + if (index === 15) return COLORS.slice(5, 8); + return COLORS.slice(2, 6); // Default case +}; + +const generateAttachments = () => Array.from({ length: 20 }, (_, index) => _mock.image.event(index)); + +const generateReviews = () => { + const attachments = generateAttachments(); +}; + +const generateRatings = () => + Array.from({ length: 5 }, (_, index) => ({ + name: `${index + 1} Star`, + starCount: _mock.number.nativeL(index), + reviewCount: _mock.number.nativeL(index + 1), + })); + +const generateImages = () => Array.from({ length: 8 }, (_, index) => _mock.image.event(index)); + +const _events = () => + Array.from({ length: 2 }, (_, index) => { + const reviews = generateReviews(); + const images = generateImages(); + const ratings = generateRatings(); + // + const publish = index % 3 ? 'published' : 'draft'; + + const category = (index % 2 && 'Shose') || (index % 3 && 'Apparel') || 'Accessories'; + + const gender = (index % 2 && ['Men']) || (index % 3 && ['Women', 'Kids']) || ['Kids']; + + const available = (index % 2 && 72) || (index % 3 && 10) || 0; + + const inventoryType = (index % 2 && 'in stock') || (index % 3 && 'low stock') || 'out of stock'; + + const priceSale = index % 3 ? undefined : _mock.number.price(index); + + return { + id: _mock.id(index).toString(), + sku: `WW75K521${index}YW/SV`, + name: _mock.eventName(index), + gender, + images, + reviews, + publish, + ratings, + category, + available, + priceSale, + taxes: 10, + quantity: 80, + inventoryType, + tags: _tags.slice(0, 5), + code: `38BEE27${index}`, + description: DESCRIPTION, + createdAt: _mock.time(index), + price: _mock.number.price(index), + coverUrl: _mock.image.event(index), + colors: getColorSliceForIndex(index), + totalRatings: _mock.number.rating(index), + totalSold: _mock.number.nativeM(index + 1), + totalReviews: _mock.number.nativeL(index + 1), + newLabel: { enabled: [1, 2, 3].includes(index), content: 'NEW' }, + saleLabel: { enabled: [4, 5].includes(index), content: 'SALE' }, + sizes: ['6', '7', '8', '8.5', '9', '9.5', '10', '10.5', '11', '11.5', '12', '13'], + subDescription: 'Featuring the original ripple design inspired by Japanese bullet trains, the Nike Air Max 97 lets you push your style full-speed ahead.', + }; + }); + +const memberList = (num_of_member: number) => + _.shuffle([ + { + email: 'tom@exampl.com', + avatar: `https://plus.unsplash.com/premium_photo-1671656349322-41de944d259b`, + sex: 'M', + }, + { + email: 'may@exampl.com', + avatar: `https://images.unsplash.com/photo-1522075469751-3a6694fb2f61`, + sex: 'F', + }, + { + email: 'june@exampl.com', + avatar: `https://plus.unsplash.com/premium_photo-1723867331866-e112500178a4`, + sex: 'M', + }, + { + email: 'april@exampl.com', + avatar: `https://plus.unsplash.com/premium_photo-1682089894837-e01e5cb8e471`, + sex: 'F', + }, + { + email: 'susan@exampl.com', + avatar: `https://images.unsplash.com/photo-1485893086445-ed75865251e0`, + sex: 'M', + }, + { + email: 'peter@exampl.com', + avatar: `https://plus.unsplash.com/premium_photo-1722945763962-305a5a769cc8`, + sex: 'F', + }, + ]).slice(0, num_of_member); + +async function eventItem() { + const temp_events = _events(); + + for (let i = 0; i < temp_events.length; i++) { + const temp_pr = _eventsReview(); + + const temp = await prisma.eventItem.upsert({ + where: { id: temp_events[i].id.toString() }, + update: {}, + create: { + id: temp_events[i].id, + // + name: temp_events[i].name, + code: temp_events[i].code, + price: temp_events[i].price, + taxes: temp_events[i].taxes, + tags: temp_events[i].tags, + sizes: temp_events[i].sizes, + publish: temp_events[i].publish, + gender: temp_events[i].gender, + coverUrl: temp_events[i].coverUrl, + images: temp_events[i].images, + colors: temp_events[i].colors, + quantity: temp_events[i].quantity, + category: temp_events[i].category, + available: temp_events[i].available, + totalSold: temp_events[i].totalSold, + description: temp_events[i].description, + totalRatings: temp_events[i].totalRatings, + totalReviews: temp_events[i].totalReviews, + inventoryType: temp_events[i].inventoryType, + subDescription: temp_events[i].subDescription, + priceSale: temp_events[i].priceSale, + newLabel: temp_events[i].newLabel, + saleLabel: temp_events[i].saleLabel, + ratings: temp_events[i].ratings, + // review: { create: temp_pr }, + reviews: { create: temp_pr }, + sku: temp_events[i].sku, + // + eventDate: new Date(_mock.time(i)), + title: temp_events[i].name, + currency: 'HKD', + duration_m: 120, + ageBottom: 18, + ageTop: 60, + location: 'Hong Kong', + avatar: temp_events[i].images, + // + joinMembers: memberList(5 - i), + }, + }); + } + console.log('seed eventItem done'); +} + +const EventItem = eventItem() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); + +export { EventItem as EventItemSeed }; diff --git a/03_source/cms_backend/prisma/seeds/eventReview.ts b/03_source/cms_backend/prisma/seeds/eventReview.ts new file mode 100644 index 0000000..3831cbf --- /dev/null +++ b/03_source/cms_backend/prisma/seeds/eventReview.ts @@ -0,0 +1,61 @@ +import { _mock } from './_mock'; +import { _tags } from './assets'; + +import { PrismaClient } from '@prisma/client'; +const prisma = new PrismaClient(); + +const generateAttachments = () => Array.from({ length: 20 }, (_, index) => _mock.image.event(index)); + +const generateReviews = () => { + const attachments = generateAttachments(); + + return Array.from({ length: 8 }, (_, index) => ({ + // id: _mock.id(index), + name: _mock.fullName(index), + postedAt: _mock.time(index), + comment: _mock.sentence(index), + isPurchased: _mock.boolean(index), + rating: _mock.number.rating(index), + avatarUrl: _mock.image.avatar(index), + helpful: _mock.number.nativeL(index), + attachments: (index === 1 && attachments.slice(0, 1)) || (index === 3 && attachments.slice(2, 4)) || (index === 5 && attachments.slice(5, 8)) || [], + })); +}; + +export const _eventsReview = () => { + return generateReviews(); +}; + +async function eventReview() { + const temp_pr = _eventsReview(); + + for (let i = 0; i < temp_pr.length; i++) { + const temp = await prisma.eventReview.upsert({ + where: { id: i.toString() }, + update: {}, + create: { + name: temp_pr[i].name, + rating: temp_pr[i].rating, + comment: temp_pr[i].comment, + helpful: temp_pr[i].helpful, + avatarUrl: temp_pr[i].avatarUrl, + isPurchased: temp_pr[i].isPurchased, + attachments: temp_pr[i].attachments, + postedAt: temp_pr[i].postedAt, + }, + }); + } + console.log('seed eventReview done'); +} + +const EventReview = eventReview() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); + +export { EventReview as EventReviewSeed }; diff --git a/03_source/cms_backend/prisma/seeds/productReview.ts b/03_source/cms_backend/prisma/seeds/productReview.ts index 89f845b..625a3fa 100644 --- a/03_source/cms_backend/prisma/seeds/productReview.ts +++ b/03_source/cms_backend/prisma/seeds/productReview.ts @@ -4,16 +4,7 @@ import { _tags } from './assets'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); -const COLORS = [ - '#FF4842', - '#1890FF', - '#FFC0CB', - '#00AB55', - '#FFC107', - '#7F00FF', - '#000000', - '#FFFFFF' -]; +const COLORS = ['#FF4842', '#1890FF', '#FFC0CB', '#00AB55', '#FFC107', '#7F00FF', '#000000', '#FFFFFF']; const DESCRIPTION = `
Specifications
@@ -113,8 +104,7 @@ const getColorSliceForIndex = (index: number) => { return COLORS.slice(2, 6); // Default case }; -const generateAttachments = () => - Array.from({ length: 20 }, (_, index) => _mock.image.product(index)); +const generateAttachments = () => Array.from({ length: 20 }, (_, index) => _mock.image.product(index)); const generateReviews = () => { const attachments = generateAttachments(); @@ -128,11 +118,7 @@ const generateReviews = () => { rating: _mock.number.rating(index), avatarUrl: _mock.image.avatar(index), helpful: _mock.number.nativeL(index), - attachments: - (index === 1 && attachments.slice(0, 1)) || - (index === 3 && attachments.slice(2, 4)) || - (index === 5 && attachments.slice(5, 8)) || - [] + attachments: (index === 1 && attachments.slice(0, 1)) || (index === 3 && attachments.slice(2, 4)) || (index === 5 && attachments.slice(5, 8)) || [], })); }; @@ -140,7 +126,7 @@ const generateRatings = () => Array.from({ length: 5 }, (_, index) => ({ name: `${index + 1} Star`, starCount: _mock.number.nativeL(index), - reviewCount: _mock.number.nativeL(index + 1) + reviewCount: _mock.number.nativeL(index + 1), })); const generateImages = () => Array.from({ length: 8 }, (_, index) => _mock.image.product(index)); @@ -164,8 +150,8 @@ async function productReview() { avatarUrl: temp_pr[i].avatarUrl, isPurchased: temp_pr[i].isPurchased, attachments: temp_pr[i].attachments, - postedAt: temp_pr[i].postedAt - } + postedAt: temp_pr[i].postedAt, + }, }); } console.log('seed productReview done'); diff --git a/03_source/cms_backend/prisma/seeds/superuser.ts b/03_source/cms_backend/prisma/seeds/superuser.ts index 2bc3004..2415cc0 100644 --- a/03_source/cms_backend/prisma/seeds/superuser.ts +++ b/03_source/cms_backend/prisma/seeds/superuser.ts @@ -8,8 +8,9 @@ async function superuser() { create: { email: 'admin1@123.com', name: 'Admin1', - password: 'Aa12345678' - } + password: 'Aa12345678', + admin: true, + }, }); // swagger test @@ -19,8 +20,9 @@ async function superuser() { create: { email: 'fake@example.com', name: 'swagger user', - password: 'password1' - } + password: 'password1', + admin: true, + }, }); console.log('seed superuser done'); diff --git a/03_source/cms_backend/prisma/seeds/user.ts b/03_source/cms_backend/prisma/seeds/user.ts index d3f36a3..f155d18 100644 --- a/03_source/cms_backend/prisma/seeds/user.ts +++ b/03_source/cms_backend/prisma/seeds/user.ts @@ -9,16 +9,29 @@ async function user() { email: 'alice@prisma.io', name: 'Alice', password: 'Aa12345678', + emailVerified: new Date(), }, }); - const bob = await prisma.user.upsert({ + await prisma.user.upsert({ + where: { email: 'demo@minimals.cc' }, + update: {}, + create: { + email: 'demo@minimals.cc', + name: 'Demo', + password: '@2Minimal', + emailVerified: new Date(), + }, + }); + + await prisma.user.upsert({ where: { email: 'bob@prisma.io' }, update: {}, create: { email: 'bob@prisma.io', name: 'Bob', password: 'Aa12345678', + emailVerified: new Date(), }, }); console.log('seed user done'); diff --git a/03_source/cms_backend/prisma/seeds/userItem.ts b/03_source/cms_backend/prisma/seeds/userItem.ts index bd9a055..7cd013a 100644 --- a/03_source/cms_backend/prisma/seeds/userItem.ts +++ b/03_source/cms_backend/prisma/seeds/userItem.ts @@ -39,10 +39,12 @@ async function userItem() { // username: 'admin@123.com', password: await generateHash('Aa1234567'), + // + isAdmin: true, }, }); - for (let i = 1; i < 20; i++) { + for (let i = 1; i < 3; i++) { const CJK_LOCALES = { en: enFaker, zh: zhFaker, @@ -77,6 +79,8 @@ async function userItem() { // username: randomFaker.internet.username(), password: await generateHash('Abc1234!'), + // + isAdmin: false, }, }); } diff --git a/03_source/cms_backend/src/_mock/_auth.ts b/03_source/cms_backend/src/_mock/_auth.ts index 937aeec..be6c105 100644 --- a/03_source/cms_backend/src/_mock/_auth.ts +++ b/03_source/cms_backend/src/_mock/_auth.ts @@ -4,7 +4,7 @@ import { _mock } from 'src/_mock'; export const JWT_SECRET = 'minimal-secret-key'; -export const JWT_EXPIRES_IN = '3 days'; +export const JWT_EXPIRES_IN = '14 days'; export const _users = [ { diff --git a/03_source/cms_backend/src/app/api/event/createEvent/route.ts b/03_source/cms_backend/src/app/api/event/createEvent/route.ts new file mode 100644 index 0000000..2c3c800 --- /dev/null +++ b/03_source/cms_backend/src/app/api/event/createEvent/route.ts @@ -0,0 +1,75 @@ +// src/app/api/product/createEvent/route.ts +// +// PURPOSE: +// create product to db +// +// RULES: +// T.B.A. +// + +import type { NextRequest } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +import prisma from '../../../lib/prisma'; + +// ---------------------------------------------------------------------- + +/** ************************************** + * POST - Events + *************************************** */ +export async function POST(req: NextRequest) { + // logger('[Event] list', events.length); + const { data } = await req.json(); + const createForm: CreateEventData = data as unknown as CreateEventData; + + console.log({ createForm }); + + try { + console.log({ data }); + await prisma.productItem.create({ data: createForm }); + return response({ hello: 'world' }, STATUS.OK); + } catch (error) { + console.log({ hello: 'world', data }); + return handleError('Event - Create', error); + } +} + +type CreateEventData = { + // id: string; + sku: string; + name: string; + code: string; + price: number; + taxes: number; + tags: string[]; + sizes: string[]; + publish: string; + gender: string[]; + coverUrl: string; + images: string[]; + colors: string[]; + quantity: number; + category: string; + available: number; + totalSold: number; + description: string; + totalRatings: number; + totalReviews: number; + inventoryType: string; + subDescription: string; + priceSale: number; + newLabel: { + content: string; + enabled: boolean; + }; + saleLabel: { + content: string; + enabled: boolean; + }; + // ratings: { + // name: string; + // starCount: number; + // reviewCount: number; + // }[]; +}; diff --git a/03_source/cms_backend/src/app/api/event/deleteEvent/route.ts b/03_source/cms_backend/src/app/api/event/deleteEvent/route.ts new file mode 100644 index 0000000..0695087 --- /dev/null +++ b/03_source/cms_backend/src/app/api/event/deleteEvent/route.ts @@ -0,0 +1,44 @@ +// src/app/api/event/deleteEvent/route.ts +// +// PURPOSE: +// delete event from db by id +// +// RULES: +// T.B.A. + +import type { NextRequest } from 'next/server'; + +import { logger } from 'src/utils/logger'; +import { STATUS, response, handleError } from 'src/utils/response'; + +import prisma from '../../../lib/prisma'; + +// ---------------------------------------------------------------------- + +/** ************************************** + * handle Delete Events + *************************************** */ +export async function DELETE(req: NextRequest) { + try { + const { searchParams } = req.nextUrl; + + // RULES: eventId must exist + const eventId = searchParams.get('eventId'); + if (!eventId) { + return response({ message: 'Event ID is required!' }, STATUS.BAD_REQUEST); + } + + // NOTE: eventId confirmed exist, run below + const event = await prisma.eventItem.delete({ where: { id: eventId } }); + + if (!event) { + return response({ message: 'Event not found!' }, STATUS.NOT_FOUND); + } + + logger('[Event] details', event.id); + + return response({ event }, STATUS.OK); + } catch (error) { + return handleError('Event - Get details', error); + } +} diff --git a/03_source/cms_backend/src/app/api/event/deleteEvent/test.http b/03_source/cms_backend/src/app/api/event/deleteEvent/test.http new file mode 100644 index 0000000..c88c093 --- /dev/null +++ b/03_source/cms_backend/src/app/api/event/deleteEvent/test.http @@ -0,0 +1,3 @@ +### + +DELETE http://localhost:7272/api/event/deleteEvent?eventId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b06 diff --git a/03_source/cms_backend/src/app/api/event/details/route.ts b/03_source/cms_backend/src/app/api/event/details/route.ts new file mode 100644 index 0000000..be0e574 --- /dev/null +++ b/03_source/cms_backend/src/app/api/event/details/route.ts @@ -0,0 +1,47 @@ +// src/app/api/event/details/route.ts +// +// PURPOSE: +// save event to db by id +// +// RULES: +// T.B.A. + +import type { NextRequest } from 'next/server'; + +import { logger } from 'src/utils/logger'; +import { STATUS, response, handleError } from 'src/utils/response'; + +import prisma from '../../../lib/prisma'; + +// ---------------------------------------------------------------------- + +/** ************************************** + * GET Event detail + *************************************** */ +export async function GET(req: NextRequest) { + try { + const { searchParams } = req.nextUrl; + + // RULES: eventId must exist + const eventId = searchParams.get('eventId'); + if (!eventId) { + return response({ message: 'Event ID is required!' }, STATUS.BAD_REQUEST); + } + + // NOTE: eventId confirmed exist, run below + const event = await prisma.eventItem.findFirst({ + include: { reviews: true }, + where: { id: eventId }, + }); + + if (!event) { + return response({ message: 'Event not found!' }, STATUS.NOT_FOUND); + } + + logger('[Event] details', event.id); + + return response({ event }, STATUS.OK); + } catch (error) { + return handleError('Event - Get details', error); + } +} diff --git a/03_source/cms_backend/src/app/api/event/details/test.http b/03_source/cms_backend/src/app/api/event/details/test.http new file mode 100644 index 0000000..9df1c50 --- /dev/null +++ b/03_source/cms_backend/src/app/api/event/details/test.http @@ -0,0 +1,3 @@ +### + +GET http://localhost:7272/api/event/details?eventId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01 diff --git a/03_source/cms_backend/src/app/api/event/image/upload/route.ts b/03_source/cms_backend/src/app/api/event/image/upload/route.ts new file mode 100644 index 0000000..a7f3d72 --- /dev/null +++ b/03_source/cms_backend/src/app/api/event/image/upload/route.ts @@ -0,0 +1,30 @@ +// src/app/api/event/image/upload/route.ts +// +// PURPOSE: +// handle upload event image +// +// RULES: +// T.B.A. + +import type { NextRequest } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +// import prisma from '../../../lib/prisma'; + +// ---------------------------------------------------------------------- + +/** ************************************** + * GET - Events + *************************************** */ +export async function POST(req: NextRequest) { + try { + const { data } = await req.json(); + console.log('helloworld'); + + return response({ hello: 'world' }, STATUS.OK); + } catch (error) { + console.log({ hello: 'world' }); + return handleError('Event - store event image', error); + } +} diff --git a/03_source/cms_backend/src/app/api/event/list/route.ts b/03_source/cms_backend/src/app/api/event/list/route.ts new file mode 100644 index 0000000..a5784a9 --- /dev/null +++ b/03_source/cms_backend/src/app/api/event/list/route.ts @@ -0,0 +1,22 @@ +// src/app/api/event/list/route.ts +import { logger } from 'src/utils/logger'; +import { STATUS, response, handleError } from 'src/utils/response'; + +import prisma from '../../../lib/prisma'; + +// ---------------------------------------------------------------------- + +/** ************************************** + * GET - Events + *************************************** */ +export async function GET() { + try { + const events = await prisma.eventItem.findMany(); + + logger('[Event] list', events.length); + + return response({ events }, STATUS.OK); + } catch (error) { + return handleError('Event - Get list', error); + } +} diff --git a/03_source/cms_backend/src/app/api/event/list/test.http b/03_source/cms_backend/src/app/api/event/list/test.http new file mode 100644 index 0000000..b82a74f --- /dev/null +++ b/03_source/cms_backend/src/app/api/event/list/test.http @@ -0,0 +1,3 @@ +### + +GET http://localhost:7272/api/event/list diff --git a/03_source/cms_backend/src/app/api/event/saveEvent/route.ts b/03_source/cms_backend/src/app/api/event/saveEvent/route.ts new file mode 100644 index 0000000..cda23c2 --- /dev/null +++ b/03_source/cms_backend/src/app/api/event/saveEvent/route.ts @@ -0,0 +1,129 @@ +// src/app/api/event/saveEvent/route.ts +// +// PURPOSE: +// save event to db by id +// +// RULES: +// T.B.A. + +import type { NextRequest } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +import prisma from '../../../lib/prisma'; + +// ---------------------------------------------------------------------- + +/** ************************************** + * GET - Events + *************************************** */ +export async function POST(req: NextRequest) { + // logger('[Event] list', events.length); + const { data } = await req.json(); + + try { + const events = await prisma.eventItem.updateMany({ + data: { + name: data.name, + sku: data.sku, + code: data.code, + price: data.price, + taxes: data.taxes, + tags: data.tags, + sizes: data.sizes, + publish: data.publish, + gender: data.gender, + coverUrl: data.coverUrl, + images: data.images, + colors: data.colors, + quantity: data.quantity, + category: data.category, + available: data.available, + totalSold: data.totalSold, + description: data.description, + totalRatings: data.totalRatings, + totalReviews: data.totalReviews, + inventoryType: data.inventoryType, + subDescription: data.subDescription, + priceSale: data.priceSale, + // + newLabel: { + content: data.newLabel?.content || '', + enabled: data.newLabel?.enabled ?? false, + }, + saleLabel: { + content: data.saleLabel?.content || '', + enabled: data.saleLabel?.enabled ?? false, + }, + ratings: { + set: data.ratings.map((rating: { name: string; starCount: number; reviewCount: number }) => ({ + name: rating.name, + starCount: rating.starCount, + reviewCount: rating.reviewCount, + })), + }, + }, + where: { id: data.id }, + }); + + return response({ data }, STATUS.OK); + } catch (error) { + console.log({ data }); + return handleError('Event - Get list', error); + } +} + +export type IEventItem = { + id: string; + sku: string; + name: string; + code: string; + price: number; + taxes: number; + tags: string[]; + sizes: string[]; + publish: string; + gender: string[]; + coverUrl: string; + images: string[]; + colors: string[]; + quantity: number; + category: string; + available: number; + totalSold: number; + description: string; + totalRatings: number; + totalReviews: number; + // createdAt: IDateValue; + inventoryType: string; + subDescription: string; + priceSale: number | null; + // reviews: IEventReview[]; + newLabel: { + content: string; + enabled: boolean; + }; + saleLabel: { + content: string; + enabled: boolean; + }; + ratings: { + name: string; + starCount: number; + reviewCount: number; + }[]; +}; + +export type IDateValue = string | number | null; + +export type IEventReview = { + id: string; + name: string; + rating: number; + comment: string; + helpful: number; + avatarUrl: string; + postedAt: IDateValue; + isPurchased: boolean; + attachments?: string[]; +}; diff --git a/03_source/cms_backend/src/app/api/event/search/route.ts b/03_source/cms_backend/src/app/api/event/search/route.ts new file mode 100644 index 0000000..af5ccdf --- /dev/null +++ b/03_source/cms_backend/src/app/api/event/search/route.ts @@ -0,0 +1,35 @@ +import type { NextRequest } from 'next/server'; + +import { logger } from 'src/utils/logger'; +import { STATUS, response, handleError } from 'src/utils/response'; + +import { _events } from 'src/_mock/_event'; + +// ---------------------------------------------------------------------- + +export const runtime = 'edge'; + +/** ************************************** + * GET - Search events + *************************************** */ +export async function GET(req: NextRequest) { + try { + const { searchParams } = req.nextUrl; + const query = searchParams.get('query')?.trim().toLowerCase(); + + if (!query) { + return response({ results: [] }, STATUS.OK); + } + + const events = _events(); + + // Accept search by name or sku + const results = events.filter(({ name, sku }) => name.toLowerCase().includes(query) || sku?.toLowerCase().includes(query)); + + logger('[Event] search-results', results.length); + + return response({ results }, STATUS.OK); + } catch (error) { + return handleError('Event - Get search', error); + } +} diff --git a/03_source/cms_backend/src/app/api/helloworld/_GUIDELINES.md b/03_source/cms_backend/src/app/api/helloworld/_GUIDELINES.md index 199ab1c..5695938 100644 --- a/03_source/cms_backend/src/app/api/helloworld/_GUIDELINES.md +++ b/03_source/cms_backend/src/app/api/helloworld/_GUIDELINES.md @@ -1,7 +1,8 @@ # GUIDELINE -this is a helloworld api endpoint -this is a demo to handle helloworld record +- this is a helloworld api endpoint +- this is a demo to handle helloworld record +- use single file for single db table/collection only ## `route.ts` diff --git a/03_source/cms_backend/src/app/api/helloworld/detail/route.ts b/03_source/cms_backend/src/app/api/helloworld/detail/route.ts new file mode 100644 index 0000000..aad3cef --- /dev/null +++ b/03_source/cms_backend/src/app/api/helloworld/detail/route.ts @@ -0,0 +1,39 @@ +// src/app/api/helloworld/detail/route.ts +// +// PURPOSE: +// Get single helloworld record detail +// +// RULES: +// - For helloworld requests, return simple response +// +import type { NextRequest } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +import prisma from '../../../lib/prisma'; + +// ---------------------------------------------------------------------- + +/** + ************************************** + * GET - Handle both helloworld and user details + ************************************** + */ +export async function GET(req: NextRequest) { + // Original user details functionality + try { + const { searchParams } = req.nextUrl; + + // RULES: helloworldId must exist + const helloworldId = searchParams.get('helloworldId'); + if (!helloworldId) return response({ message: 'helloworldId is required!' }, STATUS.BAD_REQUEST); + + const helloworld = await prisma.userItem.findFirst({ where: { id: helloworldId } }); + + if (!helloworld) return response({ message: 'User not found!' }, STATUS.NOT_FOUND); + + return response({ helloworld }, STATUS.OK); + } catch (error) { + return handleError('Product - Get details', error); + } +} diff --git a/03_source/cms_backend/src/app/api/helloworld/detail/test.http b/03_source/cms_backend/src/app/api/helloworld/detail/test.http new file mode 100644 index 0000000..60c39cc --- /dev/null +++ b/03_source/cms_backend/src/app/api/helloworld/detail/test.http @@ -0,0 +1,4 @@ +### + +GET http://localhost:7272/api/helloworld/details?helloworldId=1165ce3a-29b8-4e1a-9148-1ae08d7e8e01 + diff --git a/03_source/cms_backend/src/app/api/helloworld/route.ts b/03_source/cms_backend/src/app/api/helloworld/route.ts index b8a5daf..6b46568 100644 --- a/03_source/cms_backend/src/app/api/helloworld/route.ts +++ b/03_source/cms_backend/src/app/api/helloworld/route.ts @@ -6,6 +6,11 @@ import { listHelloworlds, deleteHelloworld, updateHelloworld, createNewHelloworl // import prisma from '../../lib/prisma'; +/** + *************************************** + * GET - list all Helloworlds + *************************************** + */ export async function GET(req: NextRequest, res: NextResponse) { try { const result = await listHelloworlds(); diff --git a/03_source/cms_backend/src/app/api/order/_GUIDELINES.md b/03_source/cms_backend/src/app/api/order/_GUIDELINES.md new file mode 100644 index 0000000..5695938 --- /dev/null +++ b/03_source/cms_backend/src/app/api/order/_GUIDELINES.md @@ -0,0 +1,23 @@ +# GUIDELINE + +- this is a helloworld api endpoint +- this is a demo to handle helloworld record +- use single file for single db table/collection only + +## `route.ts` + +handle `GET`, `POST`, `PUT`, `DELETE` + +## `test.http` + +store test request + +## `../../services/helloworld.service.ts` + +helloworld schema CRUD handler + +`listHelloworlds` - list helloworld record +`getHelloworld` - get helloworld record by id +`createNewHelloworld` - create helloworld record +`updateHelloworld` - update helloworld record by id +`deleteHelloworld` - delete helloworld record by id diff --git a/03_source/cms_backend/src/app/api/order/route.ts b/03_source/cms_backend/src/app/api/order/route.ts new file mode 100644 index 0000000..190381c --- /dev/null +++ b/03_source/cms_backend/src/app/api/order/route.ts @@ -0,0 +1,76 @@ +import type { NextRequest, NextResponse } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +import { listOrders, deleteOrder, updateOrder, createOrder } from 'src/app/services/order.service'; + +/** + ************************************** + * GET - Order + *************************************** + */ +export async function GET(req: NextRequest, res: NextResponse) { + try { + const orders = await listOrders(); + return response(orders, STATUS.OK); + } catch (error) { + return handleError('Order - Get list', error); + } +} + +/** + *************************************** + * POST - Create Order + *************************************** + */ +export async function POST(req: NextRequest) { + const { data } = await req.json(); + + try { + const order = await createOrder(data); + return response(order, STATUS.CREATED); + } catch (error) { + return handleError('Order - Create', error); + } +} + +/** + *************************************** + * PUT - Update Order + *************************************** + */ +export async function PUT(req: NextRequest) { + const { searchParams } = req.nextUrl; + const orderId = searchParams.get('orderId'); + const { data } = await req.json(); + + try { + if (!orderId) throw new Error('orderId cannot be null'); + const id: number = parseInt(orderId); + + const updatedOrder = await updateOrder(id, data); + return response(updatedOrder, STATUS.OK); + } catch (error) { + return handleError('Order - Update', error); + } +} + +/** + *************************************** + * DELETE - Delete Order + *************************************** + */ +export async function DELETE(req: NextRequest) { + const { searchParams } = req.nextUrl; + const orderId = searchParams.get('orderId'); + + try { + if (!orderId) throw new Error('orderId cannot be null'); + const id: number = parseInt(orderId); + + await deleteOrder(id); + return response({ success: true }, STATUS.OK); + } catch (error) { + return handleError('Order - Delete', error); + } +} diff --git a/03_source/cms_backend/src/app/api/order/test.http b/03_source/cms_backend/src/app/api/order/test.http new file mode 100644 index 0000000..8c89459 --- /dev/null +++ b/03_source/cms_backend/src/app/api/order/test.http @@ -0,0 +1,32 @@ +### +GET http://localhost:7272/api/order + +### +GET http://localhost:7272/api/order?orderId=1 + +### +POST http://localhost:7272/api/order +content-type: application/json + +{ + "data": { + "product": "Sample Product", + "quantity": 1, + "price": 99.99 + } +} + +### +PUT http://localhost:7272/api/order?orderId=1 +content-type: application/json + +{ + "data": { + "product": "Updated Product", + "quantity": 2, + "price": 199.98 + } +} + +### +DELETE http://localhost:7272/api/order?orderId=1 diff --git a/03_source/cms_backend/src/app/api/product/route.ts b/03_source/cms_backend/src/app/api/product/route.ts new file mode 100644 index 0000000..d30e80b --- /dev/null +++ b/03_source/cms_backend/src/app/api/product/route.ts @@ -0,0 +1,74 @@ +import type { NextRequest, NextResponse } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +import { listProducts, deleteProduct, updateProduct, createProduct } from 'src/app/services/product.service'; + +/** + ************************************** + * GET - Product + *************************************** + */ +export async function GET(req: NextRequest, res: NextResponse) { + try { + const products = await listProducts(); + return response(products, STATUS.OK); + } catch (error) { + return handleError('Product - Get list', error); + } +} + +/** + *************************************** + * POST - Create Product + *************************************** + */ +export async function POST(req: NextRequest) { + const { data } = await req.json(); + + try { + const product = await createProduct(data); + return response(product, STATUS.CREATED); + } catch (error) { + return handleError('Product - Create', error); + } +} + +/** + *************************************** + * PUT - Update Product + *************************************** + */ +export async function PUT(req: NextRequest) { + const { searchParams } = req.nextUrl; + const productId = searchParams.get('productId'); + const { data } = await req.json(); + + try { + if (!productId) throw new Error('productId cannot be null'); + + const result = await updateProduct(productId, data); + return response(result, STATUS.OK); + } catch (error) { + return handleError('Product - Update', error); + } +} + +/** + *************************************** + * DELETE - Delete Product + *************************************** + */ +export async function DELETE(req: NextRequest) { + const { searchParams } = req.nextUrl; + const productId = searchParams.get('productId'); + + try { + if (!productId) throw new Error('productId cannot be null'); + + await deleteProduct(productId); + return response({ success: true }, STATUS.OK); + } catch (error) { + return handleError('Product - Delete', error); + } +} diff --git a/03_source/cms_backend/src/app/api/product/test.http b/03_source/cms_backend/src/app/api/product/test.http new file mode 100644 index 0000000..221b429 --- /dev/null +++ b/03_source/cms_backend/src/app/api/product/test.http @@ -0,0 +1,33 @@ +### +GET http://localhost:7272/api/product + +### +GET http://localhost:7272/api/product?productId=1 + +### +POST http://localhost:7272/api/product +content-type: application/json + +{ + "data": { + "sku": "PROD-001", + "name": "Premium Product", + "price": 99.99, + "quantity": 100, + "category": "Electronics", + } +} + +### +PUT http://localhost:7272/api/product?productId=1 +content-type: application/json + +{ + "data": { + "price": 89.99, + "quantity": 95 + } +} + +### +DELETE http://localhost:7272/api/product?productId=1 diff --git a/03_source/cms_backend/src/app/api/user/changeToAdmin/route.ts b/03_source/cms_backend/src/app/api/user/changeToAdmin/route.ts new file mode 100644 index 0000000..7157583 --- /dev/null +++ b/03_source/cms_backend/src/app/api/user/changeToAdmin/route.ts @@ -0,0 +1,36 @@ +// src/app/api/user/promoteToAdmin/route.ts +// +import type { NextRequest } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +import { changeToAdmin } from 'src/app/services/userItem.service'; + +/** + *************************************** + * PUT - promote user to admin + *************************************** + */ +export async function PUT(req: NextRequest) { + const { searchParams } = req.nextUrl; + + // userId, the userId going to be admin + // {data: requestUserId}, the userId sending request to promote admin + // the requestUserId should be a admin admin already + + const userId = searchParams.get('userId'); + const { + data: { requestUserId }, + } = await req.json(); + + try { + if (!userId) return response('userId cannot be null', STATUS.BAD_REQUEST); + if (!requestUserId) return response('requestUserId cannot be null', STATUS.BAD_REQUEST); + + const result = await changeToAdmin(userId, requestUserId); + + return response(result, STATUS.OK); + } catch (error) { + return handleError('promote to admin', JSON.stringify(error)); + } +} diff --git a/03_source/cms_backend/src/app/api/user/changeToAdmin/test.http b/03_source/cms_backend/src/app/api/user/changeToAdmin/test.http new file mode 100644 index 0000000..16a83f6 --- /dev/null +++ b/03_source/cms_backend/src/app/api/user/changeToAdmin/test.http @@ -0,0 +1,33 @@ +# step 1 check not a admin +### +GET http://localhost:7272/api/user/details?userId=00b8b53b-dfe6-4b07-8d23-0dc9f75e7a2c + +# step 1 check not a admin +### +GET http://localhost:7272/api/user/details?userId=5e072d02-919c-49c4-98b4-9659b8eff231 + + +# step 2 request to change admin +### +PUT http://localhost:7272/api/user/changeToAdmin?userId=00b8b53b-dfe6-4b07-8d23-0dc9f75e7a2c +content-type: application/json + +{ + "data": {"requestUserId": "5e072d02-919c-49c4-98b4-9659b8eff231"} +} + + + +# step 2 request to change user +### +PUT http://localhost:7272/api/user/changeToUser?userId=00b8b53b-dfe6-4b07-8d23-0dc9f75e7a2c +content-type: application/json + +{ + "data": {"requestUserId": "5e072d02-919c-49c4-98b4-9659b8eff231"} +} + + +# step 3 it is now admin +### +GET http://localhost:7272/api/user/details?userId=00b8b53b-dfe6-4b07-8d23-0dc9f75e7a2c diff --git a/03_source/cms_backend/src/app/api/user/changeToUser/route.ts b/03_source/cms_backend/src/app/api/user/changeToUser/route.ts new file mode 100644 index 0000000..4191a4d --- /dev/null +++ b/03_source/cms_backend/src/app/api/user/changeToUser/route.ts @@ -0,0 +1,36 @@ +// src/app/api/user/changeToUser/route.ts +// +import type { NextRequest } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +import { changeToUser } from 'src/app/services/userItem.service'; + +/** + *************************************** + * PUT - change admin back to regular user + *************************************** + */ +export async function PUT(req: NextRequest) { + const { searchParams } = req.nextUrl; + + // userId, the userId going to be changed to regular user + // {data: requestUserId}, the userId sending request to change role + // the requestUserId should be an admin + + const userId = searchParams.get('userId'); + const { + data: { requestUserId }, + } = await req.json(); + + try { + if (!userId) throw new Error('userId cannot be null'); + if (!requestUserId) throw new Error('requestUserId cannot be null'); + + const result = await changeToUser(userId, requestUserId); + + return response(result, STATUS.OK); + } catch (error) { + return handleError('change to regular user', JSON.stringify(error)); + } +} diff --git a/03_source/cms_backend/src/app/api/user/changeToUser/test.http b/03_source/cms_backend/src/app/api/user/changeToUser/test.http new file mode 100644 index 0000000..b43a24a --- /dev/null +++ b/03_source/cms_backend/src/app/api/user/changeToUser/test.http @@ -0,0 +1,17 @@ +### +PUT http://localhost:7272/api/user/changeToUser?userId=cmbfx38e30000ssek6ufjewcp +content-type: application/json + +{ + "data": {"requestUserId": "cmbfx38kk000esseka9qo8lpt"} +} + + +### +GET http://localhost:7272/api/user/changeToUser?userId= + +### +GET http://localhost:7272/api/user + +### +GET http://localhost:7272/api/user?userId=cmbfx38e30000ssek6ufjewcp diff --git a/03_source/cms_backend/src/app/api/user/checkAdmin/route.ts b/03_source/cms_backend/src/app/api/user/checkAdmin/route.ts new file mode 100644 index 0000000..85f2557 --- /dev/null +++ b/03_source/cms_backend/src/app/api/user/checkAdmin/route.ts @@ -0,0 +1,21 @@ +import type { NextRequest, NextResponse } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +import { isAdmin } from 'src/app/services/userItem.service'; + +// import prisma from '../../lib/prisma'; + +export async function GET(req: NextRequest, res: NextResponse) { + const { searchParams } = req.nextUrl; + const userId = searchParams.get('userId'); + + try { + if (!userId) throw new Error('userId cannot be null'); + const result = await isAdmin(userId); + + return response(result, STATUS.OK); + } catch (error) { + return handleError('Post - Get latest', error); + } +} diff --git a/03_source/cms_backend/src/app/api/user/checkAdmin/test.http b/03_source/cms_backend/src/app/api/user/checkAdmin/test.http new file mode 100644 index 0000000..eb4600a --- /dev/null +++ b/03_source/cms_backend/src/app/api/user/checkAdmin/test.http @@ -0,0 +1,9 @@ +### +GET http://localhost:7272/api/user/checkAdmin?userId=cmbfx38ey0001ssek5pr05o70 + +### +GET http://localhost:7272/api/user/checkAdmin?userId=cmbfvonkx000411ykuah8pp8s + + +### +GET http://localhost:7272/api/user/checkAdmin diff --git a/03_source/cms_backend/src/app/api/user/details/route.ts b/03_source/cms_backend/src/app/api/user/details/route.ts index a1165ec..5455efd 100644 --- a/03_source/cms_backend/src/app/api/user/details/route.ts +++ b/03_source/cms_backend/src/app/api/user/details/route.ts @@ -1,4 +1,4 @@ -// src/app/api/product/details/route.ts +// src/app/api/user/details/route.ts // // PURPOSE: // read user from db by id @@ -24,9 +24,7 @@ export async function GET(req: NextRequest) { // RULES: userId must exist const userId = searchParams.get('userId'); - if (!userId) { - return response({ message: 'userId is required!' }, STATUS.BAD_REQUEST); - } + if (!userId) return response({ message: 'userId is required!' }, STATUS.BAD_REQUEST); // NOTE: userId confirmed exist, run below const user = await prisma.userItem.findFirst({ @@ -34,9 +32,7 @@ export async function GET(req: NextRequest) { where: { id: userId }, }); - if (!user) { - return response({ message: 'User not found!' }, STATUS.NOT_FOUND); - } + if (!user) return response({ message: 'User not found!' }, STATUS.NOT_FOUND); logger('[User] details', user.id); diff --git a/03_source/cms_backend/src/app/api/user/helloworld/route.ts b/03_source/cms_backend/src/app/api/user/helloworld/route.ts new file mode 100644 index 0000000..85f2557 --- /dev/null +++ b/03_source/cms_backend/src/app/api/user/helloworld/route.ts @@ -0,0 +1,21 @@ +import type { NextRequest, NextResponse } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +import { isAdmin } from 'src/app/services/userItem.service'; + +// import prisma from '../../lib/prisma'; + +export async function GET(req: NextRequest, res: NextResponse) { + const { searchParams } = req.nextUrl; + const userId = searchParams.get('userId'); + + try { + if (!userId) throw new Error('userId cannot be null'); + const result = await isAdmin(userId); + + return response(result, STATUS.OK); + } catch (error) { + return handleError('Post - Get latest', error); + } +} diff --git a/03_source/cms_backend/src/app/api/user/helloworld/test.http b/03_source/cms_backend/src/app/api/user/helloworld/test.http new file mode 100644 index 0000000..341bca1 --- /dev/null +++ b/03_source/cms_backend/src/app/api/user/helloworld/test.http @@ -0,0 +1,2 @@ +### +GET http://localhost:7272/api/user diff --git a/03_source/cms_backend/src/app/api/user/list/route.ts b/03_source/cms_backend/src/app/api/user/list/route.ts index 097f217..9324e74 100644 --- a/03_source/cms_backend/src/app/api/user/list/route.ts +++ b/03_source/cms_backend/src/app/api/user/list/route.ts @@ -1,17 +1,20 @@ // src/app/api/product/list/route.ts +// import { logger } from 'src/utils/logger'; import { STATUS, response, handleError } from 'src/utils/response'; -import prisma from '../../../lib/prisma'; +import { listUsers } from 'src/app/services/userItem.service'; // ---------------------------------------------------------------------- -/** ************************************** +/** + *************************************** * GET - Products - *************************************** */ + *************************************** + */ export async function GET() { try { - const users = await prisma.userItem.findMany(); + const users = await listUsers(); logger('[User] list', users.length); diff --git a/03_source/cms_backend/src/app/api/user/route.ts b/03_source/cms_backend/src/app/api/user/route.ts new file mode 100644 index 0000000..9a151a0 --- /dev/null +++ b/03_source/cms_backend/src/app/api/user/route.ts @@ -0,0 +1,80 @@ +import type { NextRequest, NextResponse } from 'next/server'; + +import { STATUS, response, handleError } from 'src/utils/response'; + +import { listUsers, deleteUser, updateUser, createNewUser } from 'src/app/services/userItem.service'; + +// import prisma from '../../lib/prisma'; + +export async function GET(req: NextRequest, res: NextResponse) { + try { + const result = await listUsers(); + + return response(result, STATUS.OK); + } catch (error) { + return handleError('User - Get latest', error); + } +} + +/** + *************************************** + * POST - create User + *************************************** + */ +export async function POST(req: NextRequest) { + const { data } = await req.json(); + + try { + const createResult = await createNewUser(data); + + return response(createResult, STATUS.OK); + } catch (error) { + return handleError('User - Create', error); + } +} + +/** + *************************************** + * PUT - update User + *************************************** + */ +export async function PUT(req: NextRequest) { + const { searchParams } = req.nextUrl; + const userId = searchParams.get('userId'); + + const { data } = await req.json(); + + try { + if (!userId) throw new Error('userId cannot null'); + const id: number = parseInt(userId); + + const updateResult = await updateUser(id, data); + + return response(updateResult, STATUS.OK); + } catch (error) { + return handleError('User - Update', error); + } +} + +/** + *************************************** + * DELETE - update User + *************************************** + */ +export async function DELETE(req: NextRequest) { + const { searchParams } = req.nextUrl; + const userId = searchParams.get('userId'); + + const { data } = await req.json(); + + try { + if (!userId) throw new Error('userId cannot null'); + const id: number = parseInt(userId); + + const deleteResult = await deleteUser(id); + + return response(deleteResult, STATUS.OK); + } catch (error) { + return handleError('User - Update', error); + } +} diff --git a/03_source/cms_backend/src/app/api/user/test.http b/03_source/cms_backend/src/app/api/user/test.http new file mode 100644 index 0000000..9a1598f --- /dev/null +++ b/03_source/cms_backend/src/app/api/user/test.http @@ -0,0 +1,26 @@ +### +GET http://localhost:7272/api/user + +### +GET http://localhost:7272/api/user?userId=cmbfvonhl000011ykdi345yc9 + +### +POST http://localhost:7272/api/user?userId=1 +content-type: application/json + +{ + "data":{"name": "John Doe"} +} + +### +PUT http://localhost:7272/api/user?userId=1 +content-type: application/json + +{ + "data": {"name": "John Doe"} +} + + +### +DELETE http://localhost:7272/api/user?userId=1 + diff --git a/03_source/cms_backend/src/app/services/AccessLog.service.ts b/03_source/cms_backend/src/app/services/AccessLog.service.ts new file mode 100644 index 0000000..a936a9d --- /dev/null +++ b/03_source/cms_backend/src/app/services/AccessLog.service.ts @@ -0,0 +1,66 @@ +// src/app/services/AccessLog.service.ts +// +// PURPOSE: +// Service for handling AccessLog records +// +// RULES: +// - All methods return Promises +// - Input validation should be done at controller level +// - Errors should be propagated to caller + +import type { AccessLog } from '@prisma/client'; + +import prisma from '../lib/prisma'; + +// type CreateAccessLog = { +// userId?: string; +// message?: string; +// metadata?: Record; +// }; + +// type UpdateAccessLog = { +// status?: number; +// metadata?: object; +// }; + +async function listAccessLogs(): Promise { + return prisma.accessLog.findMany({ + orderBy: { timestamp: 'desc' }, + take: 100, + }); +} + +async function getAccessLog(id: string): Promise { + return prisma.accessLog.findUnique({ where: { id } }); +} + +async function createAccessLog(userId?: string, message?: string, metadata?: Record): Promise { + return prisma.accessLog.create({ + data: { + userId, + message, + metadata, + }, + }); +} + +// async function update(id: string, data: UpdateAccessLog): Promise { +// return prisma.accessLog.update({ +// where: { id }, +// data: { +// ...data, +// metadata: data.metadata || {}, +// }, +// }); +// } + +// async function deleteAccessLog(id: string): Promise { +// return prisma.accessLog.delete({ where: { id } }); +// } + +export { + // + getAccessLog, + listAccessLogs, + createAccessLog, +}; diff --git a/03_source/cms_backend/src/app/services/AppLog.service.ts b/03_source/cms_backend/src/app/services/AppLog.service.ts new file mode 100644 index 0000000..abc6956 --- /dev/null +++ b/03_source/cms_backend/src/app/services/AppLog.service.ts @@ -0,0 +1,52 @@ +// src/app/services/AppLog.service.ts +// +// REQ0047/T.B.A. +// +// PURPOSE: +// - AppLog example for handling AppLog Record +// +// RULES: +// - T.B.A. +// + +import type { AppLog } from '@prisma/client'; + +import prisma from '../lib/prisma'; + +type CreateAppLog = { + level: number; + message: string; +}; + +// type UpdateAppLog = { +// level: number; +// message: string; +// }; + +async function listAppLogs(): Promise { + return prisma.appLog.findMany(); +} + +async function getAppLog(appLogId: string) { + return prisma.appLog.findFirst({ where: { id: appLogId } }); +} + +async function createNewAppLog(createForm: CreateAppLog) { + return prisma.appLog.create({ data: createForm }); +} + +// async function updateAppLog(appLogId: string, updateForm: UpdateAppLog) { +// return prisma.appLog.update({ where: { id: appLogId }, data: updateForm }); +// } + +async function deleteAppLog(appLogId: string) { + return prisma.appLog.delete({ where: { id: appLogId } }); +} + +export { + getAppLog, + listAppLogs, + // updateAppLog, + deleteAppLog, + createNewAppLog, +}; diff --git a/03_source/cms_backend/src/app/services/_PROMPTS/clone_srevice.md b/03_source/cms_backend/src/app/services/_PROMPTS/clone_srevice.md new file mode 100644 index 0000000..c0f6ead --- /dev/null +++ b/03_source/cms_backend/src/app/services/_PROMPTS/clone_srevice.md @@ -0,0 +1,7 @@ +with knowledge in schema.prisma file, +please refer the below helloworld example `helloworld.service.ts` +and create `user.service.ts` to cover user record + +thanks + +`/home/logic/_wsl_workspace/001_github_ws/HKSingleParty-ws/HKSingleParty/03_source/cms_backend/src/app/services/helloworld.service.ts` diff --git a/03_source/cms_backend/src/app/services/event.service.ts b/03_source/cms_backend/src/app/services/event.service.ts new file mode 100644 index 0000000..8c22679 --- /dev/null +++ b/03_source/cms_backend/src/app/services/event.service.ts @@ -0,0 +1,65 @@ +// src/app/services/event.service.ts +// +// PURPOSE: +// - Service for handling Event records +// +// RULES: +// - Follows same pattern as helloworld.service.ts +// + +import type { Event } from '@prisma/client'; + +import prisma from '../lib/prisma'; + +type CreateEvent = { + eventDate: DateTime; + title: string; + joinMembers?: Json[]; + price: number; + currency: string; + duration_m: number; + ageBottom: number; + ageTop: number; + location: string; + avatar?: string; + memberId?: number; +}; + +type UpdateEvent = { + eventDate?: DateTime; + title?: string; + joinMembers?: Json[]; + price?: number; + currency?: string; + duration_m?: number; + ageBottom?: number; + ageTop?: number; + location?: string; + avatar?: string; + memberId?: number; +}; + +async function listEvents(): Promise { + return prisma.event.findMany(); +} + +async function getEvent(eventId: number) { + return prisma.event.findFirst({ where: { id: eventId } }); +} + +async function createNewEvent(createForm: CreateEvent) { + return prisma.event.create({ data: createForm }); +} + +async function updateEvent(eventId: number, updateForm: UpdateEvent) { + return prisma.event.update({ + where: { id: eventId }, + data: updateForm, + }); +} + +async function deleteEvent(eventId: number) { + return prisma.event.delete({ where: { id: eventId } }); +} + +export { getEvent, listEvents, updateEvent, deleteEvent, createNewEvent }; diff --git a/03_source/cms_backend/src/app/services/order.service.ts b/03_source/cms_backend/src/app/services/order.service.ts new file mode 100644 index 0000000..2d8c9f6 --- /dev/null +++ b/03_source/cms_backend/src/app/services/order.service.ts @@ -0,0 +1,70 @@ +// src/app/services/order.service.ts +// +// PURPOSE: +// - Handle Order Record CRUD operations +// - Manage order status transitions +// - Handle order-event relationships +// + +import type { OrderItem } from '@prisma/client'; + +import prisma from '../lib/prisma'; + +type CreateOrderItem = { + orderNumber?: string; + // status?: string; + // eventIds?: number[]; +}; + +type UpdateOrderItem = { + orderNumber?: string; + // status?: string; + // last_payment_date?: Date; + // eventIds?: number[]; +}; + +async function listOrders(): Promise { + return prisma.orderItem.findMany(); +} + +async function getOrder(orderId: string): Promise { + return prisma.orderItem.findFirst({ + where: { id: orderId }, + }); +} + +async function createOrder(createForm: CreateOrderItem): Promise { + return prisma.orderItem.create({ + data: createForm, + }); +} + +async function updateOrder(orderId: string, updateForm: UpdateOrderItem): Promise { + return prisma.orderItem.update({ + where: { id: orderId }, + data: updateForm, + }); +} + +async function deleteOrder(orderId: string): Promise { + return prisma.orderItem.delete({ + where: { id: orderId }, + }); +} + +async function getOrdersByStatus(status: string): Promise { + return prisma.orderItem.findMany({ + where: { status }, + }); +} + +export { + getOrder, + listOrders, + createOrder, + updateOrder, + deleteOrder, + getOrdersByStatus, + type CreateOrderItem as CreateOrder, + type UpdateOrderItem as UpdateOrder, +}; diff --git a/03_source/cms_backend/src/app/services/product.service.ts b/03_source/cms_backend/src/app/services/product.service.ts new file mode 100644 index 0000000..c8b5967 --- /dev/null +++ b/03_source/cms_backend/src/app/services/product.service.ts @@ -0,0 +1,114 @@ +// src/app/services/product.service.ts +// +// PURPOSE: +// - Service for handling ProductItem Record +// + +import type { ProductItem } from '@prisma/client'; + +import prisma from '../lib/prisma'; + +type CreateProduct = { + sku: string; + name: string; + // price: number; + // code?: string; + // taxes?: number; + // tags?: string[]; + // sizes?: string[]; + // gender?: string[]; + // colors?: string[]; + // category?: string; + // quantity?: number; + // available?: number; + // coverUrl?: string; + // images?: string[]; + // description?: string; + // subDescription?: string; + // publish?: string; + // totalSold?: number; + // totalRatings?: number; + // totalReviews?: number; + // inventoryType?: string; + // ratings?: number[]; + // reviews?: string[]; + // result?: string; + // thanks?: string; +}; + +type UpdateProduct = { + sku?: string; + name?: string; + // price?: number; + // code?: string; + // taxes?: number; + // tags?: string[]; + // sizes?: string[]; + // gender?: string[]; + // colors?: string[]; + // category?: string; + // quantity?: number; + // available?: number; + // coverUrl?: string; + // images?: string[]; + // description?: string; + // subDescription?: string; + // publish?: string; + // totalSold?: number; + // totalRatings?: number; + // totalReviews?: number; + // inventoryType?: string; + // ratings?: number[]; + // reviews?: string[]; + // result?: string; + // thanks?: string; +}; + +async function listProducts(): Promise { + return prisma.productItem.findMany(); +} + +async function getProduct(productId: string) { + return prisma.productItem.findUnique({ where: { id: productId } }); +} + +async function createProduct(createForm: CreateProduct) { + // return prisma.productItem.create({ + // data: { + // ...createForm, + // code: createForm.code || '', + // taxes: createForm.taxes || 0, + // tags: createForm.tags || [], + // sizes: createForm.sizes || [], + // gender: createForm.gender || [], + // colors: createForm.colors || [], + // category: createForm.category || '', + // quantity: createForm.quantity || 0, + // available: createForm.available || 0, + // coverUrl: createForm.coverUrl || '', + // images: createForm.images || [], + // description: createForm.description || '', + // subDescription: createForm.subDescription || '', + // publish: createForm.publish || 'published', + // totalSold: createForm.totalSold || 0, + // totalRatings: createForm.totalRatings || 0, + // totalReviews: createForm.totalReviews || 0, + // inventoryType: createForm.inventoryType || '', + // ratings: createForm.ratings || [], + // reviews: createForm.reviews || [], + // }, + // }); +} + +async function updateProduct(productId: string, updateForm: UpdateProduct) { + return prisma.productItem.update({ + where: { id: productId }, + data: updateForm, + }); +} + +async function deleteProduct(productId: string) { + return prisma.productItem.delete({ where: { id: productId } }); +} + +export { getProduct, listProducts, createProduct, updateProduct, deleteProduct, type CreateProduct, type UpdateProduct }; diff --git a/03_source/cms_backend/src/app/services/userItem.service.ts b/03_source/cms_backend/src/app/services/userItem.service.ts new file mode 100644 index 0000000..8b9cadc --- /dev/null +++ b/03_source/cms_backend/src/app/services/userItem.service.ts @@ -0,0 +1,122 @@ +// src/app/services/user.service.ts +// +// PURPOSE: +// - Handle User Record CRUD operations +// +// RULES: +// - Follow Prisma best practices for database operations +// - Validate input data before processing +// + +import type { UserItem } from '@prisma/client'; + +import prisma from '../lib/prisma'; + +type CreateUser = { + email: string; + // name?: string; + // password: string; + // role?: Role; + // isEmailVerified?: boolean; + // admin?: boolean; +}; + +type UpdateUser = { + email?: string; + // name?: string; + // password?: string; + // role?: Role; + // isEmailVerified?: boolean; + isAdmin?: boolean; +}; + +async function listUsers(): Promise { + return prisma.userItem.findMany(); +} + +async function getUserItem(userId: string): Promise { + return prisma.userItem.findFirst({ where: { id: userId } }); +} + +async function updateUser(userId: string, updateForm: UpdateUser): Promise { + return prisma.userItem.update({ + where: { id: userId }, + data: updateForm, + }); +} + +// check if userId is a admin +// check if userId is a admin +async function isAdmin(userId: string): Promise { + const user = await getUserItem(userId); + return user?.isAdmin === true; +} + +async function changeToAdmin(userIdToPromote: string, userIdOfApplicant: string) { + // check the applicant is admin or not + const userApplicant = await getUserItem(userIdOfApplicant); + let promoteResult = {}; + + if (userApplicant && userApplicant.isAdmin) { + // applicant is an admin + promoteResult = await updateUser(userIdToPromote, { isAdmin: true }); + } else { + promoteResult = { status: 'failed', message: 'applicant is not a admin' }; + } + + return promoteResult; +} + +async function changeToUser(userIdToPromote: string, userIdOfApplicant: string) { + // check the applicant is admin or not + const userApplicant = await getUserItem(userIdOfApplicant); + let promoteResult = {}; + + if (userApplicant && userApplicant.isAdmin) { + // applicant is an admin + promoteResult = await updateUser(userIdToPromote, { isAdmin: false }); + } else { + promoteResult = { status: 'failed', message: 'applicant is not a admin' }; + } + + return promoteResult; +} + +async function getUserByEmail(email: string): Promise { + // return prisma.userItem.findUnique({ + // where: { email }, + // include: { + // Token: true, + // }, + // }); +} + +async function createNewUser(createForm: CreateUser): Promise { + // return prisma.userItem.create({ + // data: { + // email: createForm.email, + // name: createForm.name, + // password: createForm.password, + // role: createForm.role || 'USER', + // isEmailVerified: createForm.isEmailVerified || false, + // }, + // }); +} + +async function deleteUser(userId: number): Promise { + // return prisma.userItem.delete({ where: { id: userId } }); +} + +export { + // getUser, + isAdmin, + listUsers, + updateUser, + deleteUser, + changeToUser, + createNewUser, + changeToAdmin, + getUserByEmail, + type CreateUser, + type UpdateUser, +}; diff --git a/03_source/cms_backend/src/utils/response.ts b/03_source/cms_backend/src/utils/response.ts index 81bdaaa..78b3e09 100644 --- a/03_source/cms_backend/src/utils/response.ts +++ b/03_source/cms_backend/src/utils/response.ts @@ -7,7 +7,7 @@ export const STATUS = { UNAUTHORIZED: 401, }; -export function response(data: string | Record, status: number): Response { +export function response(data: string | Record | Record[], status: number): Response { const value = typeof data === 'string' ? data : JSON.stringify(data); return new Response(value, {