Compare commits

...

66 Commits

Author SHA1 Message Date
louiscklaw
7c1ac6b546 feat: add new CarousellMe routes for OffersMade and MyProfile pages with corresponding API integrations and UI components 2025-06-20 03:15:15 +08:00
louiscklaw
53b498d881 feat: implement Hotel Service Wi-Fi intro page with Swiper slider and HTML content display 2025-06-20 02:04:49 +08:00
louiscklaw
d865fca058 feat: enhance CarousellMe UI with updated filter button styling, end of list indicator, and improved content padding 2025-06-20 01:40:35 +08:00
louiscklaw
871313449e "fix: update CAROUSELL_ME_SETTINGS path to remove redundant tabs prefix for consistency" 2025-06-19 23:20:01 +08:00
louiscklaw
76840a8e1b feat: add comprehensive ServiceMenu and MainTabs components with hotel service listings, QR code access, and user profile settings integration 2025-06-19 23:06:51 +08:00
louiscklaw
a68cb01585 feat: add new SVG icons for MainTabs including air conditioning, alert, assistance, baggage, and banking symbols 2025-06-19 18:51:59 +08:00
louiscklaw
d6b36a0ca6 feat: add getButtonSvg API to fetch button SVG file as Promise 2025-06-19 18:51:43 +08:00
louiscklaw
360da364ff feat: add CarousellMe feature with profile, listings and reviews components 2025-06-19 17:45:45 +08:00
louiscklaw
1fdf10c0da feat: add requirement doc for importing demo page from old projects (REQ0190) 2025-06-19 17:10:54 +08:00
louiscklaw
c0d8d0cd05 feat: add react-i18next and react-star-ratings packages with dependencies update and typescript upgrade 2025-06-19 17:10:15 +08:00
louiscklaw
b923410f99 feat: add react-i18next package for internationalization support 2025-06-19 17:10:03 +08:00
louiscklaw
65f9b83c9f feat: add react-star-ratings package for star rating component 2025-06-19 17:09:07 +08:00
louiscklaw
d8166d8a3d feat: upgrade typescript from 4.9.3 to 5.8.3 2025-06-19 17:08:28 +08:00
louiscklaw
f59a382d8f feat: update payment route paths to use centralized PATHS constants and add missing dummy pay page route 2025-06-19 16:41:48 +08:00
louiscklaw
c4f8a6902c feat: implement payment flow for event joining including success/failure pages and navigation 2025-06-19 16:21:11 +08:00
louiscklaw
7b230d4f8b feat: add 99_references folder to workspace paths 2025-06-19 16:20:45 +08:00
louiscklaw
13c3399a6e update, 2025-06-18 13:42:35 +08:00
louiscklaw
80a2636f90 feat: remove debug logging from event count endpoint 2025-06-18 13:39:09 +08:00
louiscklaw
9c4637528c feat: add event count endpoints and improve auth logging with constants 2025-06-18 13:38:14 +08:00
louiscklaw
661de6e8d7 refactor: enhance tsc watch script with file touch fallback to prevent watch termination 2025-06-18 13:35:36 +08:00
louiscklaw
4a0ae590b0 feat: extract API endpoints to separate endpoints.ts file and enhance axios configuration with centralized setup and error handling 2025-06-18 13:23:47 +08:00
louiscklaw
53162ed333 feat: update AI initialization guide with git staged files instruction and file comment update reminder 2025-06-18 12:51:14 +08:00
louiscklaw
79c292d943 "feat: update workspace settings with increased editor font size from 15 to 18" 2025-06-18 12:51:07 +08:00
louiscklaw
10a6375347 feat: add party user auth endpoints for authentication flow (me, signIn, signUp) 2025-06-18 12:50:43 +08:00
louiscklaw
f950617372 feat: extend auth endpoint to support both User and PartyUser models with fallback retrieval logic 2025-06-18 12:43:03 +08:00
louiscklaw
99fafda624 feat: enhance party user auth endpoint with token validation, error logging, and security improvements 2025-06-18 12:34:48 +08:00
louiscklaw
3ed3f2fecb update FAQ, 2025-06-18 11:02:00 +08:00
louiscklaw
b80939c78d update eslint to disable export sorting, 2025-06-18 10:54:30 +08:00
louiscklaw
779984f65c feat: enhance party user authentication endpoint with improved error handling, logging, and PartyUser model integration 2025-06-18 10:52:00 +08:00
louiscklaw
8bb6c9e992 update FAQ, 2025-06-18 10:39:57 +08:00
louiscklaw
60ecca48b4 feat: update workspace settings with font size configuration and recommended extensions 2025-06-18 10:32:12 +08:00
louiscklaw
7a6014a115 feat: implement event joining flow with dummy payment page, including route configuration, Redux state management, and UI updates for event detail page 2025-06-18 04:06:16 +08:00
louiscklaw
37ace98e60 feat: add party payment flow with dummy pay page implementation and route configuration 2025-06-18 02:32:09 +08:00
louiscklaw
44091e0432 feat: add party user gender field to schema and implement event joining functionality with gender tracking 2025-06-18 02:28:29 +08:00
louiscklaw
279496ea38 feat: remove AppRoute component and adjust related route configurations 2025-06-18 02:18:18 +08:00
louiscklaw
a450747670 feat: add new pages for event detail, member profile and order detail with corresponding route configurations 2025-06-18 01:20:27 +08:00
louiscklaw
215476cfaa feat: add party user metadata storage and display support, including storage API, Redux actions, state management, and UI updates for profile page 2025-06-18 01:14:37 +08:00
louiscklaw
c93b31b2f6 feat: implement party user authentication system with signin/signup routes, JWT token validation, and frontend integration including mobile route configuration and API service updates 2025-06-18 01:14:05 +08:00
louiscklaw
4cf93f431e feat: refine Account model schema with consistent formatting and add default rank field to PartyUser model 2025-06-18 00:23:12 +08:00
louiscklaw
2b09261f0a feat: implement login and signup pages with shared authentication styles, including form validation and Redux integration for authentication state management 2025-06-17 23:47:18 +08:00
louiscklaw
1325a361dc feat: update requirement REQ0188 dependencies, add frontend/party-user-auth/trunk to development paths 2025-06-17 22:11:45 +08:00
louiscklaw
a4d0d8b746 feat: clarify FAQ guidance on updating git staged files, adding reference to sibling files in same directory 2025-06-17 20:15:57 +08:00
louiscklaw
f8919b8c84 feat: update FAQ with guidance on updating git staged files, specifying to preserve format and detail level while avoiding unintended code changes 2025-06-17 20:12:39 +08:00
louiscklaw
583e31fd4d feat: refactor party event action types and functions, update imports and naming conventions to use PartyEvent naming scheme, add createPartyUser function to party-user actions 2025-06-17 20:06:47 +08:00
louiscklaw
ae7f005236 feat: enhance party user schema with address fields, update frontend form and API calls to use PartyUser naming convention 2025-06-17 19:59:09 +08:00
louiscklaw
834f9360ba feat: update import path for UserCreateView to use party-user view module 2025-06-17 19:58:56 +08:00
louiscklaw
1cb018d4d5 "feat: simplify db push script by removing restart loop and directly executing db:push and seed" 2025-06-17 19:58:45 +08:00
louiscklaw
448825545e "feat: refactor party user data fetching to use new endpoint URL builder and implement 2025-06-17 19:18:21 +08:00
louiscklaw
7c7a532381 fix: disable i18n debug mode in production 2025-06-17 19:18:13 +08:00
louiscklaw
eb515dbe68 feat: enhance party user schema with company, status, role and verification fields, update seeding and frontend form 2025-06-17 18:31:20 +08:00
louiscklaw
7a793be610 feat: update imports and component structure in party-user list view 2025-06-17 18:31:07 +08:00
louiscklaw
1d89134ea2 fix: remove unused tax calculation code in product form 2025-06-17 18:30:30 +08:00
louiscklaw
44d324b40e feat: update frontend references from User to PartyUser for party user management module 2025-06-16 10:20:09 +08:00
louiscklaw
ee2a377bf6 feat: add frontend environment variables template with server URL, API keys and service configurations 2025-06-16 10:20:01 +08:00
louiscklaw
0941ab6dd1 feat: update frontend component names from User to PartyUser for consistency with party-user module 2025-06-16 10:08:43 +08:00
louiscklaw
e93c5dcf62 in the middle of party-user frontend, 2025-06-16 03:03:54 +08:00
louiscklaw
17aaf97722 feat: add REQ0188 frontend party-user CRUD functionality with backend API endpoints and database schema 2025-06-16 02:26:41 +08:00
louiscklaw
47660be0cd docs: update FAQ with question about introducing new format or ideas 2025-06-16 02:24:47 +08:00
louiscklaw
cf5cfb8d63 "feat: add mobile app support with Dockerfile and dev/build scripts, update all projects to use psmisc for process management" 2025-06-16 01:46:15 +08:00
louiscklaw
a686dd55dd feat: update REQ0187 requirements with TODO items for nav bar cleanup and status change implementation 2025-06-16 00:36:37 +08:00
louiscklaw
8d52be9b96 "fix: update party order details route variable naming and test case ID" 2025-06-16 00:35:17 +08:00
louiscklaw
db16b2d5dd party-order delete ok, 2025-06-16 00:07:22 +08:00
louiscklaw
7370316ea0 party-order list ok, 2025-06-16 00:01:36 +08:00
louiscklaw
77f7211317 feat: add REQ0189 frontend party-order list and details pages with mock data and API integration 2025-06-15 23:08:14 +08:00
louiscklaw
dfc9873815 feat: add REQ0187 frontend party-order CRUD functionality and backend API endpoints 2025-06-15 22:27:44 +08:00
louiscklaw
53b112e488 "docs: update FAQ with questions about modifying .tsx.draft files and replacement starting point" 2025-06-15 22:27:06 +08:00
407 changed files with 16306 additions and 704 deletions

View File

@@ -0,0 +1,23 @@
---
tags: frontend, party-order
---
# REQ0185 frontend party-order
frontend page to handle party-order (CRUD)
edit page T.B.A.
## TODO
- remove detail in left nav bar
- implement `changeStatus`
## sources
T.B.A.
## branch
develop/frontend/party-order/trunk
develop/requirements/REQ0187

View File

@@ -0,0 +1,21 @@
---
tags: frontend, party-user
---
# REQ0188 frontend party-user
frontend page to handle party-user (CRUD)
edit page T.B.A.
## TODO
## sources
T.B.A.
## branch
develop/requirements/REQ0188
develop/frontend/party-user/trunk
develop/frontend/party-user-auth/trunk

View File

@@ -0,0 +1,49 @@
---
tags: mobile, payment
---
# REQ0189 party payment flow
frontend page to handle party-user pay join event
## User flow
```mermaid
graph TD
a["user trigger paid request"]
b["redirect user to payment gateway"]
c["payment success, redirect user to payment success page"]
d["payment failed, show user"]
e["redirect user back to event_detail page"]
a --> b --payment ok --> c --> e
b --payment failed --> d
d --> e
```
## Test
- assume user already login
| steps | description |
| ----- | --------------------------------------------------- |
| 1 | user enter event detail page |
| 2 | user press join button |
| 3 | app redirect to payment gateway page |
| 4 | user choose pay |
| 5 | payment success, redirect back to event_detail page |
| end | test done |
## TODO
T.B.A.
## sources
T.B.A.
## branch
develop/requirements/REQ0189
develop/mobile/DummyPayPage/trunk

View File

@@ -0,0 +1,19 @@
---
tags: mobile, carousell, restaurant-cms
---
# REQ0188 import page from old projects
import demo page from old projects
edit page T.B.A.
## TODO
## sources
T.B.A.
## branch
develop/requirements/REQ0190

View File

@@ -6,6 +6,7 @@ RUN npm install -g pnpm
RUN apt-get update -y
RUN apt-get install -y openssl
RUN apt-get install -qqy psmisc
# Set working directory
WORKDIR /app

View File

@@ -1,3 +1,5 @@
// src/cms_backend/eslint.config.mjs
//
import globals from 'globals';
import eslintJs from '@eslint/js';
import eslintTs from 'typescript-eslint';
@@ -69,10 +71,7 @@ const importRules = () => ({
*/
const unusedImportsRules = () => ({
'unused-imports/no-unused-imports': 1,
'unused-imports/no-unused-vars': [
0,
{ vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' },
],
'unused-imports/no-unused-vars': [0, { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' }],
});
/**
@@ -93,15 +92,17 @@ const sortImportsRules = () => {
return {
'perfectionist/sort-named-imports': [1, { type: 'line-length', order: 'asc' }],
'perfectionist/sort-named-exports': [1, { type: 'line-length', order: 'asc' }],
'perfectionist/sort-exports': [
1,
{
order: 'asc',
type: 'line-length',
groupKind: 'values-first',
},
],
// disable sorting of export, i manage the export ordering
// 'perfectionist/sort-named-exports': [1, { type: 'line-length', order: 'asc' }],
// 'perfectionist/sort-exports': [
// 1,
// {
// order: 'asc',
// type: 'line-length',
// groupKind: 'values-first',
// },
// ],
'perfectionist/sort-imports': [
2,
{

View File

@@ -16,11 +16,11 @@ model Helloworld {
}
model Account {
id String @id @default(cuid())
userId String @map("user_id")
id String @id @default(cuid())
userId String @map("user_id")
type String
provider String
providerAccountId String @map("provider_account_id")
providerAccountId String @map("provider_account_id")
refresh_token String?
access_token String?
expires_at Int?
@@ -30,18 +30,21 @@ model Account {
session_state String?
oauth_token_secret String?
oauth_token String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
PartyUser PartyUser? @relation(fields: [partyUserId], references: [id])
partyUserId String?
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique @map("session_token")
userId String @map("user_id")
id String @id @default(cuid())
sessionToken String @unique @map("session_token")
userId String @map("user_id")
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
PartyUser PartyUser? @relation(fields: [partyUserId], references: [id])
partyUserId String?
}
model User {
@@ -1230,3 +1233,63 @@ model AccessLog {
@@index([timestamp])
@@index([userId])
}
model PartyOrderItem {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//
taxes Float
status String
shipping Float
discount Float
subtotal Float
orderNumber String
totalAmount Float
totalQuantity Float
history Json
payment Json
customer Json
delivery Json
items Json[]
shippingAddress Json
// OrderProductItem OrderProductItem[]
// OrderHistory OrderHistory[]
// OrderDelivery OrderDelivery[]
// OrderCustomer OrderCustomer[]
// OrderPayment OrderPayment[]
// OrderShippingAddress OrderShippingAddress[]
}
model PartyUser {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
//
username String? @unique
password String?
//
name String?
email String @unique
emailVerified DateTime?
avatarUrl String?
bucketImage String?
admin Boolean @default(false)
accounts Account[]
sessions Session[]
info Json?
phoneNumber String @default("")
company String @default("")
status String @default("pending")
role String @default("")
isVerified Boolean @default(false)
//
country String @default("")
state String @default("")
city String @default("")
address String @default("")
zipCode String @default("")
//
rank String @default("user")
sex String @default("")
}

View File

@@ -31,6 +31,9 @@ import { EventReviewSeed } from './seeds/eventReview';
import { appLogSeed } from './seeds/AppLog';
import { accessLogSeed } from './seeds/AccessLog';
import { userMetaSeed } from './seeds/userMeta';
//
import { partyOrderItemSeed } from './seeds/partyOrderItem';
import { partyUserSeed } from './seeds/partyUser';
//
// import { Blog } from './seeds/blog';
@@ -59,6 +62,9 @@ import { userMetaSeed } from './seeds/userMeta';
//
await appLogSeed;
await accessLogSeed;
//
await partyOrderItemSeed;
await partyUserSeed;
// await Blog;
// await Mail;

View File

@@ -0,0 +1,94 @@
import { PrismaClient } from '@prisma/client';
import { _mock } from './_mock';
const prisma = new PrismaClient();
const ITEMS = Array.from({ length: 3 }, (_, index) => ({
id: _mock.id(index),
sku: `16H9UR${index}`,
quantity: index + 1,
name: _mock.productName(index),
coverUrl: _mock.image.product(index),
price: _mock.number.price(index),
}));
async function partyOrderItem() {
await prisma.partyOrderItem.deleteMany({});
for (let index = 1; index < 20 + 1; index++) {
const shipping = 10;
const discount = 10;
const taxes = 10;
const items = (index % 2 && ITEMS.slice(0, 1)) || (index % 3 && ITEMS.slice(1, 3)) || ITEMS;
const totalQuantity = items.reduce((accumulator, item) => accumulator + item.quantity, 0);
const subtotal = items.reduce((accumulator, item) => accumulator + item.price * item.quantity, 0);
const totalAmount = subtotal - shipping - discount + taxes;
const customer = {
id: _mock.id(index),
name: _mock.fullName(index),
email: _mock.email(index),
avatarUrl: _mock.image.avatar(index),
ipAddress: '192.158.1.38',
};
const delivery = { shipBy: 'DHL', speedy: 'Standard', trackingNumber: 'SPX037739199373' };
const history = {
orderTime: _mock.time(1),
paymentTime: _mock.time(2),
deliveryTime: _mock.time(3),
completionTime: _mock.time(4),
timeline: [
{ title: 'Delivery successful', time: _mock.time(1) },
{ title: 'Transporting to [2]', time: _mock.time(2) },
{ title: 'Transporting to [1]', time: _mock.time(3) },
{ title: 'The shipping unit has picked up the goods', time: _mock.time(4) },
{ title: 'Order has been created', time: _mock.time(5) },
],
};
const temp = await prisma.partyOrderItem.upsert({
where: { id: index.toString() },
update: {},
create: {
id: _mock.id(index),
orderNumber: `#601${index}`,
taxes,
items,
history,
subtotal: items.reduce((accumulator, item) => accumulator + item.price * item.quantity, 0),
shipping,
discount,
customer,
delivery,
totalAmount,
totalQuantity,
shippingAddress: {
fullAddress: '19034 Verna Unions Apt. 164 - Honolulu, RI / 87535',
phoneNumber: '365-374-4961',
},
payment: {
//
cardType: 'mastercard',
cardNumber: '4111 1111 1111 1111',
},
status: (index % 2 && 'completed') || (index % 3 && 'pending') || (index % 4 && 'cancelled') || 'refunded',
},
});
}
console.log('seed partyOrderItemSeed done');
}
const partyOrderItemSeed = partyOrderItem()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
export { partyOrderItemSeed };

View File

@@ -0,0 +1,135 @@
/**
* Party User seed data generator
* Creates initial user accounts for development and testing
* Includes:
* - Fixed demo accounts (alice, demo)
* - Randomly generated test accounts with CJK locale data
*/
import { faker as enFaker } from '@faker-js/faker/locale/en_US';
import { faker as zhFaker } from '@faker-js/faker/locale/zh_CN';
import { faker as jaFaker } from '@faker-js/faker/locale/ja';
import { faker as koFaker } from '@faker-js/faker/locale/ko';
import { faker as twFaker } from '@faker-js/faker/locale/zh_TW';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const ROLE = [
`CEO`,
`CTO`,
`Project Coordinator`,
`Team Leader`,
`Software Developer`,
`Marketing Strategist`,
`Data Analyst`,
`Product Owner`,
`Graphic Designer`,
`Operations Manager`,
`Customer Support Specialist`,
`Sales Manager`,
`HR Recruiter`,
`Business Consultant`,
`Financial Planner`,
`Network Engineer`,
`Content Creator`,
`Quality Assurance Tester`,
`Public Relations Officer`,
`IT Administrator`,
`Compliance Officer`,
`Event Planner`,
`Legal Counsel`,
`Training Coordinator`,
];
const STATUS = ['active', 'pending', 'banned'];
async function partyUser() {
const alice = await prisma.partyUser.upsert({
where: { email: 'alice@prisma.io' },
update: {},
create: {
email: 'alice@prisma.io',
name: 'Alice',
username: 'pualice',
password: 'Aa12345678',
emailVerified: new Date(),
phoneNumber: '+85291234567',
company: 'helloworld company',
status: STATUS[0],
role: ROLE[0],
isVerified: true,
sex: 'F',
},
});
await prisma.partyUser.upsert({
where: { email: 'demo@minimals.cc' },
update: {},
create: {
email: 'demo@minimals.cc',
password: '@2Minimal',
//
username: 'pudemo',
name: 'Demo',
emailVerified: new Date(),
phoneNumber: '+85291234568',
company: 'helloworld company',
status: STATUS[1],
role: ROLE[1],
isVerified: true,
avatarUrl: 'https://images.unsplash.com/photo-1619970096024-c7b438a3b82a',
rank: 'user',
sex: 'M',
},
});
for (let i = 0; i < 5; i++) {
const CJK_LOCALES = {
en: enFaker,
zh: zhFaker,
ja: jaFaker,
ko: koFaker,
tw: twFaker,
};
function getRandomCJKFaker() {
const locales = Object.keys(CJK_LOCALES);
const randomKey = locales[Math.floor(Math.random() * locales.length)] as keyof typeof CJK_LOCALES;
return CJK_LOCALES[randomKey];
}
const randomFaker = getRandomCJKFaker();
await prisma.partyUser.upsert({
where: { email: `party_user${i}@prisma.io` },
update: {},
create: {
email: `party_user${i}@prisma.io`,
name: `Party Dummy ${i}`,
username: `pu${i.toString()}`,
password: 'Aa12345678',
emailVerified: new Date(),
phoneNumber: `+8529123456${i.toString()}`,
company: randomFaker.company.name(),
role: ROLE[Math.floor(Math.random() * ROLE.length)],
status: STATUS[Math.floor(Math.random() * STATUS.length)],
isVerified: true,
sex: i % 2 ? 'F' : 'M',
},
});
}
console.log('seed partyUser done');
}
const partyUserSeed = partyUser()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
export { partyUserSeed };

View File

@@ -7,9 +7,13 @@ clear
while true; do
yarn db:studio &
npx nodemon --ext ts,tsx,prisma --exec "yarn db:push && yarn seed && yarn dev"
npx nodemon --ext ts,tsx,prisma --exec "yarn dev"
# npx nodemon --ext ts,tsx,prisma --exec "yarn db:push && yarn seed && yarn dev"
# yarn dev
killall node
killall yarn
echo "restarting..."
sleep 1
done

View File

@@ -4,8 +4,6 @@ yarn --dev
clear
while true; do
npx nodemon --ext prisma --exec "yarn db:push && yarn seed"
echo "restarting..."
sleep 1
done
yarn db:push && yarn seed
echo "done"

View File

@@ -2,6 +2,8 @@
set -x
killall node
killall yarn
rm -rf ./**/*Zone.Identifier
# yarn db:push

View File

@@ -0,0 +1,14 @@
Hi,
i copied from
`03_source/cms_backend/src/app/api/party-event`
to
`03_source/cms_backend/src/app/api/party-order`
with knowledge in `schema.prisma` file, and take a look into the sibling files in the same directory.
i want you to update `03_source/cms_backend/src/app/api/party-order` content to handle `party-order` (the purchase order of the party)
`party-order.service.ts` is already prepared and you can use it
please maintain same format and level of detail when you edit.
thanks.

View File

@@ -1,4 +1,13 @@
import type { User } from '@prisma/client';
// src/app/api/auth/me/route.ts
//
// PURPOSE:
// - T.B.A.
//
// RULES:
// - T.B.A.
//
import type { PartyUser, User } from '@prisma/client';
import type { NextRequest } from 'next/server';
import { headers } from 'next/headers';
@@ -11,9 +20,11 @@ import { getUserById } from 'src/app/services/user.service';
import { createAccessLog } from 'src/app/services/access-log.service';
import { flattenNextjsRequest } from '../sign-in/flattenNextjsRequest';
import { getPartyUserById } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
// NOTE: keep this comment to let prisma running on nextjs
// export const runtime = 'edge';
/**
@@ -29,6 +40,7 @@ const INVALID_AUTH_TOKEN = 'Invalid authorization token';
const USER_ID_NOT_FOUND = 'userId not found';
const USER_TOKEN_OK = 'user token check ok';
const AUTHORIZATION_TOKEN_MISSING_OR_INVALID = 'Authorization token missing or invalid';
const USER_BANNED = 'user banned';
export async function GET(req: NextRequest) {
const debug = { 'req.headers': flattenNextjsRequest(req) };
@@ -43,12 +55,17 @@ export async function GET(req: NextRequest) {
const accessToken = `${authorization}`.split(' ')[1];
const data = await verify(accessToken, JWT_SECRET);
console.log(data.userId);
if (data.userId) {
// TODO: remove me
// const currentUser = _users.find((user) => user.id === data.userId);
const currentUser: User | null = await getUserById(data.userId);
const { userId } = data;
let currentUser: User | PartyUser | null = null;
currentUser = await getPartyUserById(userId);
if (!currentUser) {
currentUser = await getUserById(userId);
}
if (!currentUser) {
createAccessLog('', USER_TOKEN_CHECK_FAILED, debug);

View File

@@ -1,11 +1,26 @@
###
# username and password ok
GET http://localhost:7272/api/auth/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWJnbnUyengwMDBjaHEzaGZ3dmtjejlvIiwiaWF0IjoxNzQ4OTY0ODkyLCJleHAiOjE3NTAxNzQ0OTJ9.lo04laCxtm0IVeYaETEV3hXKyDmXPEn7SyWtY2VR4dI
GET http://localhost:7272/api/auth/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWMwdWo4aXgwMDBqM2Y1eWhxc29xMW9wIiwiaWF0IjoxNzUwMjE5NTYyLCJleHAiOjE3NTE0MjkxNjJ9.8gKM2oMquccM_HDEfBAgtapCGf3M1eIp6SZ_knx7d1g
###
# username and password ok
POST http://localhost:7272/api/auth/sign-in
content-type: application/json
{
"email": "demo@minimals.cc",
"password": "@2Minimal"
}
###
# There is no user corresponding to the email address.
POST http://localhost:7272/api/auth/sign-in
content-type: application/json
@@ -15,7 +30,9 @@ content-type: application/json
}
###
# Wrong password
POST http://localhost:7272/api/auth/sign-in
content-type: application/json

View File

@@ -1,5 +1,7 @@
###
# username and password ok
POST http://localhost:7272/api/auth/sign-in
content-type: application/json
@@ -9,7 +11,9 @@ content-type: application/json
}
###
# There is no user corresponding to the email address.
POST http://localhost:7272/api/auth/sign-in
content-type: application/json
@@ -19,7 +23,9 @@ content-type: application/json
}
###
# Wrong password
POST http://localhost:7272/api/auth/sign-in
content-type: application/json

View File

@@ -0,0 +1,21 @@
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import { countTotalEvents } from 'src/app/services/eventItem.service';
// ----------------------------------------------------------------------
/** **************************************
* GET - Events, obsoleted
*************************************** */
export async function GET() {
try {
const numOfEvent = await countTotalEvents();
logger('[Event] list', numOfEvent);
return response({ numOfEvent }, STATUS.OK);
} catch (error) {
return handleError('Event - Get list', error);
}
}

View File

@@ -0,0 +1,3 @@
###
GET http://localhost:7272/api/event/helloworld

View File

@@ -0,0 +1,22 @@
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
// src/app/api/event/list/route.ts
import { countTotalEvents, listEvents } from 'src/app/services/eventItem.service';
// ----------------------------------------------------------------------
/** **************************************
* GET - Events, obsoleted
*************************************** */
export async function GET() {
try {
const numOfEvents = await countTotalEvents();
// logger('[Event] list', numOfEvents.length);
return response({ numOfEvents }, STATUS.OK);
} catch (error) {
return handleError('Event - Get list', error);
}
}

View File

@@ -0,0 +1,3 @@
###
GET http://localhost:7272/api/event/numOfEvent

View File

@@ -0,0 +1,58 @@
// src/app/api/product/createEvent/route.ts
//
// PURPOSE:
// create product to db
//
// RULES:
// T.B.A.
//
import type { NextRequest } from 'next/server';
import _ from 'lodash';
import { STATUS, response, handleError } from 'src/utils/response';
import { getEventItemById } from 'src/app/services/eventItem.service';
import { getPartyUserByEmail } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
/**
***************************************
* POST - Events
***************************************
*/
export async function POST(req: NextRequest) {
// logger('[Event] list', events.length);
const { data } = await req.json();
const { eventItemId, email } = data;
try {
const eventItem = await getEventItemById(eventItemId);
const partyUser = await getPartyUserByEmail(email);
if (partyUser) {
if (eventItem && eventItem?.joinMembers) {
const foundJoined = _.find(eventItem.joinMembers, { email });
if (foundJoined) {
console.log('user joined event already, skipping');
} else {
const { sex } = partyUser;
eventItem.joinMembers.push({ email, sex });
await prisma?.eventItem.update({
where: { id: eventItem.id },
data: { joinMembers: JSON.parse(JSON.stringify(eventItem.joinMembers)) },
});
}
}
}
return response({ result: 'joined' }, STATUS.OK);
} catch (error) {
console.log({ hello: 'world', data });
return handleError('Event - Create', error);
}
}

View File

@@ -0,0 +1,13 @@
###
# username and password ok
POST http://localhost:7272/api/event/partyUserJoinEvent
content-type: application/json
{
"data": {
"eventItemId": "e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01",
"email": "alice@prisma.io"
}
}

View File

@@ -0,0 +1,35 @@
<!-- AI: please maintain same format and level of detail when edit this file -->
# GUIDELINE
- Party Event API endpoint for managing party events
- Handles CRUD operations for party events
- Follows single file for single db table/collection pattern
## `route.ts`
Handles HTTP methods:
- `GET` - Retrieve party events
- `POST` - Create new party event
- `PUT` - Update existing party event
- `DELETE` - Remove party event
## `test.http`
Contains test requests for:
- Listing all party events
- Creating new party event
- Updating existing party event
- Deleting party event
## `../../services/party-event.service.ts`
Party Event CRUD operations:
`listPartyEvents` - List all party events with optional filters
`getPartyEvent` - Get single party event by ID
`createNewPartyEvent` - Create new party event record
`updatePartyEvent` - Update existing party event by ID
`deletePartyEvent` - Delete party event by ID

View File

@@ -0,0 +1,23 @@
//
//
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import { countTotalEvents } from 'src/app/services/eventItem.service';
// ----------------------------------------------------------------------
/** **************************************
* GET - Events, obsoleted
*************************************** */
export async function GET() {
try {
const numOfEvent = await countTotalEvents();
// logger('[Event] list', numOfEvent);
return response({ numOfEvent }, STATUS.OK);
} catch (error) {
return handleError('Event - Get list', error);
}
}

View File

@@ -0,0 +1,3 @@
###
GET http://localhost:7272/api/party-event/numOfEvent

View File

@@ -0,0 +1,35 @@
<!-- AI: please maintain same format and level of detail when edit this file -->
# GUIDELINE
- Party Order API endpoint for managing party orders
- Handles CRUD operations for party orders
- Follows single file for single db table/collection pattern
## `route.ts`
Handles HTTP methods:
- `GET` - Retrieve party orders
- `POST` - Create new party order
- `PUT` - Update existing party order
- `DELETE` - Remove party order
## `test.http`
Contains test requests for:
- Listing all party orders
- Creating new party order
- Updating existing party order
- Deleting party order
## `../../services/party-order.service.ts`
Party Order CRUD operations:
`listPartyOrders` - List all party orders with optional filters
`getPartyOrder` - Get single party order by ID
`createNewPartyOrder` - Create new party order record
`updatePartyOrder` - Update existing party order by ID
`deletePartyOrder` - Delete party order by ID

View File

@@ -0,0 +1,40 @@
// src/app/api/party-order/create/route.ts
//
// PURPOSE:
// Create new party order in db
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { isDev } from 'src/constants';
import { createOrder } from 'src/app/services/party-order.service';
// ----------------------------------------------------------------------
/** **************************************
* POST - Create PartyOrder
*************************************** */
export async function POST(req: NextRequest) {
const { partyOrderData } = await req.json();
try {
if (isDev) {
console.log({ partyOrderData });
}
const created = await createOrder(partyOrderData);
if (isDev) {
console.log('Order created successfully');
}
return response(created, STATUS.OK);
} catch (error) {
console.error('Error creating order:', { partyOrderData });
return handleError('PartyOrder - Create', error);
}
}

View File

@@ -0,0 +1,31 @@
###
POST http://localhost:7272/api/party-order/create
Content-Type: application/json
{
"partyOrderData": {
"taxes": 15.00,
"status": "pending",
"shipping": 10.00,
"discount": 20.00,
"subtotal": 290.00,
"orderNumber": "ORD_20231115001",
"totalAmount": 300.00,
"totalQuantity": 2.0,
"history": "{\"actions\":[{\"timestamp\":\"2023-11-15T10:00:00Z\",\"action\":\"order_created\"}]}",
"payment": "{\"method\":\"credit_card\",\"status\":\"unpaid\",\"transaction_id\":\"txn_123456\"}",
"customer": "{\"name\":\"John Doe\",\"email\":\"john.doe@example.com\",\"phone\":\"+1234567890\"}",
"delivery": "{\"method\":\"courier\",\"estimated_delivery\":\"2023-11-18\"}",
"items": [
{
"id": "ticket_001",
"name": "General Admission",
"quantity": 2,
"price": 150.00
}
],
"shippingAddress": "{\"street\":\"123 Main St\",\"city\":\"New York\",\"state\":\"NY\",\"zip\":\"10001\",\"country\":\"USA\"}"
}
}

View File

@@ -0,0 +1,22 @@
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { deleteOrder } from 'src/app/services/party-order.service';
/** **************************************
* PATCH - Delete PartyOrder
*************************************** */
export async function PATCH(req: NextRequest) {
try {
const { partyOrderId } = await req.json();
if (!partyOrderId) throw new Error('orderId cannot be null');
await deleteOrder(partyOrderId);
return response({ partyOrderId }, STATUS.OK);
} catch (error) {
return handleError('PartyOrder - Delete', error);
}
}

View File

@@ -0,0 +1,8 @@
###
PATCH http://localhost:7272/api/party-order/delete
Content-Type: application/json
{
"partyOrderId": "e99f09a7-dd88-49d5-b1c8-1daf80c2d7b02"
}

View File

@@ -0,0 +1,56 @@
// src/app/api/party-order/details/route.ts
//
// PURPOSE:
// Get party order from db by id
//
// RULES:
// Do not modify the format and level of details thanks.
import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import { L_INFO, L_ERROR } from 'src/constants';
import { getOrder } from 'src/app/services/party-order.service';
import { createAppLog } from 'src/app/services/app-log.service';
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
// ----------------------------------------------------------------------
/**
**************************************
* GET PartyOrder detail
***************************************
*/
export async function GET(req: NextRequest) {
const debug = { 'req.headers': flattenNextjsRequest(req) };
try {
const { searchParams } = req.nextUrl;
// RULES: partyOrderId must exist
const partyOrderId = searchParams.get('partyOrderId');
if (!partyOrderId) {
return response({ message: 'Order ID is required!' }, STATUS.BAD_REQUEST);
}
// NOTE: partyOrderId confirmed exist, run below
const partyOrder = await getOrder(partyOrderId);
if (!partyOrder) {
return response({ message: 'Order not found!' }, STATUS.NOT_FOUND);
}
logger('[PartyOrder] details', partyOrder.id);
createAppLog(L_INFO, 'Get order detail OK', debug);
return response({ partyOrder }, STATUS.OK);
} catch (error) {
createAppLog(L_ERROR, 'order detail error', debug);
return handleError('PartyOrder - Get details', error);
}
}

View File

@@ -0,0 +1,9 @@
###
# Get details for a specific party order
GET http://localhost:7272/api/party-order/details?partyOrderId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b13
###
# Alternative format with different ID
GET http://localhost:7272/api/party-order/details?id=ord_987654321

View File

@@ -0,0 +1,16 @@
import type { NextRequest, NextResponse } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
/**
***************************************
* GET - helloworld
***************************************
*/
export async function GET(req: NextRequest, res: NextResponse) {
try {
return response({ helloworld: 'party-order' }, STATUS.OK);
} catch (error) {
return handleError('Helloworld - Get all', error);
}
}

View File

@@ -0,0 +1,2 @@
###
GET http://localhost:7272/api/party-order/helloworld

View File

@@ -0,0 +1,38 @@
// src/app/api/party-order/list/route.ts
//
// PURPOSE:
// List all party orders from db
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { L_INFO, L_ERROR } from 'src/constants';
import { createAppLog } from 'src/app/services/app-log.service';
import { listPartyOrders } from 'src/app/services/party-order.service';
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
// ----------------------------------------------------------------------
/** **************************************
* GET - PartyOrders list
*************************************** */
export async function GET(req: NextRequest) {
const debug = { 'req.headers': flattenNextjsRequest(req) };
try {
const partyOrders = await listPartyOrders();
createAppLog(L_INFO, 'party-order list ok', {});
return response({ partyOrders }, STATUS.OK);
} catch (error) {
createAppLog(L_ERROR, 'party-order list error', debug);
return handleError('PartyOrder - Get list', error);
}
}

View File

@@ -0,0 +1,19 @@
###
# Basic list all orders
GET http://localhost:7272/api/party-order/list
###
# List orders by status
GET http://localhost:7272/api/party-order/list?status=completed
###
# List orders by user
GET http://localhost:7272/api/party-order/list?userId=usr_987654321
###
# List orders by payment status
GET http://localhost:7272/api/party-order/list?paymentStatus=paid

View File

@@ -0,0 +1,75 @@
import type { NextRequest, NextResponse } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { listPartyOrders, deleteOrder, updateOrder, createOrder } from 'src/app/services/party-order.service';
/**
**************************************
* GET - PartyOrder
***************************************
*/
export async function GET(req: NextRequest, res: NextResponse) {
try {
const orders = await listPartyOrders();
return response(orders, STATUS.OK);
} catch (error) {
return handleError('PartyOrder - Get list', error);
}
}
/**
***************************************
* POST - Create PartyOrder
***************************************
*/
export async function POST(req: NextRequest) {
const OPERATION = 'PartyOrder - Create';
const { data } = await req.json();
try {
const order = await createOrder(data);
return response(OPERATION, STATUS.OK);
} catch (error) {
return handleError(OPERATION, error);
}
}
/**
***************************************
* PUT - Update PartyOrder
***************************************
*/
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 result = await updateOrder(orderId, data);
return response(result, STATUS.OK);
} catch (error) {
return handleError('PartyOrder - Update', error);
}
}
/**
***************************************
* DELETE - Delete PartyOrder
***************************************
*/
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');
await deleteOrder(orderId);
return response({ success: true }, STATUS.OK);
} catch (error) {
return handleError('PartyOrder - Delete', error);
}
}

View File

@@ -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 type { IOrderItem } from '../update/route';
// import { searchEvents } from 'src/app/services/party-event.service';
// ----------------------------------------------------------------------
/** **************************************
* GET - Search PartyEvents
*************************************** */
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 results: IOrderItem[] = [];
// TODO: search party event not implemented
console.log('search party event not implemented');
// const results = await searchEvents(query);
logger('[PartyEvent] search-results', results?.length);
return response({ results }, STATUS.OK);
} catch (error) {
return handleError('PartyEvent - Get search', error);
}
}

View File

@@ -0,0 +1,24 @@
###
# Search party events by title
GET http://localhost:7272/api/party-event/search?query=Music
###
# Search party events by location
GET http://localhost:7272/api/party-event/search?query=Central+Park
###
# Search party events by tag
GET http://localhost:7272/api/party-event/search?query=Festival
###
# Combined search with multiple parameters
GET http://localhost:7272/api/party-event/search?query=Summer&location=Hong+Kong&category=Music
###
# No results expected
GET http://localhost:7272/api/party-event/search?query=zzzzzz

View File

@@ -0,0 +1,51 @@
// src/app/api/party-order/update/route.ts
//
// PURPOSE:
// Update party order in db by id
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { updateOrder } from 'src/app/services/party-order.service';
// ----------------------------------------------------------------------
/** **************************************
* PUT - Update PartyOrder
*************************************** */
export async function PUT(req: NextRequest) {
const { orderData } = await req.json();
const { id } = orderData;
if (!id) return response({ message: 'id not found' }, STATUS.ERROR);
try {
const result = await updateOrder(id, orderData);
return response({ result }, STATUS.OK);
} catch (error) {
console.error('Error updating order:', { orderData });
return handleError('PartyOrder - Update', error);
}
}
export type IOrderItem = {
id: string;
eventId: string;
userId: string;
status: string;
paymentStatus: string;
totalAmount: number;
items: {
id: string;
name: string;
quantity: number;
price: number;
}[];
createdAt: Date;
updatedAt: Date;
};

View File

@@ -0,0 +1,51 @@
###
PUT http://localhost:7272/api/party-order/update
Content-Type: application/json
{
"orderData": {
"id":"e99f09a7-dd88-49d5-b1c8-1daf80c2d7b02",
"taxes": 10.50,
"status": "completed",
"shipping": 5.00,
"discount": 20.00,
"subtotal": 290.00,
"orderNumber": "ORD-2023-001",
"totalAmount": 300.00,
"totalQuantity": 2,
"history": {
"payment": "2023-01-01",
"status_changes": [
"pending",
"paid"
]
},
"payment": {
"method": "credit_card",
"card_last4": "4321"
},
"customer": {
"name": "John Doe",
"email": "john@example.com"
},
"delivery": {
"method": "express",
"tracking_number": "TRK123456"
},
"items": [
{
"id": "ticket_001",
"name": "General Admission",
"quantity": 2,
"price": 150.00,
"status": "used"
}
],
"shippingAddress": {
"street": "123 Main St",
"city": "New York",
"zip": "10001"
}
}
}

View File

@@ -0,0 +1,76 @@
// src/app/api/party-user-auth/me/route.ts
//
// PURPOSE:
// - Handle authentication for party users via JWT
// - Verify and decode JWT tokens
// - Return current authenticated party user details
// - Log all access attempts (success/failure)
// - Validate token structure and user existence
//
// RULES:
// - Must validate Bearer token format before processing
// - All errors must be logged via access-log service
// - User existence must be verified after token validation
// - Sensitive data must be filtered from responses
// - Mock JWT_SECRET should be replaced in production
// - Debug info should be included in error logs
//
import type { NextRequest } from 'next/server';
import type { PartyUser } from '@prisma/client';
import { headers } from 'next/headers';
import { verify } from 'src/utils/jwt';
import { STATUS, response, handleError } from 'src/utils/response';
import { JWT_SECRET } from 'src/_mock/_auth';
import { createAccessLog } from 'src/app/services/access-log.service';
import { getPartyUserById } from 'src/app/services/party-user.service';
import { flattenNextjsRequest } from '../sign-in/flattenNextjsRequest';
// ----------------------------------------------------------------------
const ERR_USER_TOKEN_CHECK_FAILED = 'user token check failed';
const ERR_INVALID_AUTH_TOKEN = 'Invalid authorization token';
const ERR_USER_ID_NOT_FOUND = 'userId not found';
const ERR_AUTHORIZATION_TOKEN_MISSING_OR_INVALID = 'Authorization token missing or invalid';
const USER_TOKEN_OK = 'user token check ok';
export async function GET(req: NextRequest) {
const debug = { 'req.headers': flattenNextjsRequest(req) };
try {
const headersList = headers();
const authorization = headersList.get('authorization');
if (!authorization || !authorization.startsWith('Bearer ')) {
return response({ message: ERR_AUTHORIZATION_TOKEN_MISSING_OR_INVALID }, STATUS.UNAUTHORIZED);
}
const accessToken = `${authorization}`.split(' ')[1];
const data = await verify(accessToken, JWT_SECRET);
if (data.userId) {
// TODO: remove me
// const currentUser = _users.find((user) => user.id === data.userId);
// const currentUser: User | null = await getUserById(data.userId);
const currentUser: PartyUser | null = await getPartyUserById(data.userId);
if (!currentUser) {
createAccessLog('', ERR_USER_TOKEN_CHECK_FAILED, debug);
return response({ message: ERR_INVALID_AUTH_TOKEN }, STATUS.UNAUTHORIZED);
}
createAccessLog(currentUser.id, USER_TOKEN_OK, debug);
return response({ user: currentUser }, STATUS.OK);
} else {
return response({ message: ERR_USER_ID_NOT_FOUND }, STATUS.ERROR);
}
} catch (error) {
return handleError('[Auth] - Me', error);
}
}

View File

@@ -0,0 +1,38 @@
// Test cases for Party User Authentication endpoints
// Tests both successful and error scenarios
// Environment: http://localhost:7272
// Expected responses:
// - 200 OK with user data for valid tokens
// - 401 Unauthorized for invalid/missing tokens
// - 400 Bad Request for invalid credentials
###
# username and password ok
GET http://localhost:7272/api/party-user-auth/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWMwdWo4azIwMDBxM2Y1eTZlNXJzejRxIiwiaWF0IjoxNzUwMjEzOTkwLCJleHAiOjE3NTE0MjM1OTB9.MoKv3Nmrp_blE0jQ1rG1WyQ_TrJeF7kSe5xfHrF8b64
###
# There is no user corresponding to the email address.
POST http://localhost:7272/api/party-user-auth/sign-in
content-type: application/json
{
"email": "party_user0@prisma.io",
"password": "Aa12345678"
}
###
# Wrong password
POST http://localhost:7272/api/party-user-auth/sign-in
content-type: application/json
{
"email": "demo@minimals.cc",
"password": "@2Min111imal"
}

View File

@@ -0,0 +1,6 @@
export const ERR_USER_NOT_FOUND = 'There is no user corresponding to the email address.';
export const ERR_WRONG_PASSWORD = 'Wrong password';
export const LOG_USER_TRIED_LOGIN_WITH_EMAIL = `user tried login with email`;
export const LOG_USER_LOGGED_WITH_WRONG_PASSWORD = 'user logged with wrong password';
export const LOG_ACCESS_GRANTED = 'access granted';
export const LOG_ATTEMPTED_LOGIN_BUT_FAILED = 'attempted login but failed';

View File

@@ -0,0 +1,10 @@
import type { NextRequest } from 'next/server';
/**
* Flattens a Next.js request object into a plain object of headers
* @param {NextRequest} req - The Next.js request object
* @returns {Record<string, string>} An object containing all request headers
*/
export function flattenNextjsRequest(req: NextRequest) {
return Object.fromEntries(req.headers.entries());
}

View File

@@ -0,0 +1,60 @@
// src/app/api/party-user-auth/sign-in/route1.ts
//
import type { NextRequest } from 'next/server';
import { sign } from 'src/utils/jwt';
import { STATUS, response, handleError } from 'src/utils/response';
import { JWT_SECRET, JWT_EXPIRES_IN } from 'src/_mock/_auth';
import { createAccessLog } from 'src/app/services/access-log.service';
import prisma from '../../../lib/prisma';
import { flattenNextjsRequest } from './flattenNextjsRequest';
import {
LOG_USER_TRIED_LOGIN_WITH_EMAIL,
ERR_USER_NOT_FOUND,
LOG_USER_LOGGED_WITH_WRONG_PASSWORD,
ERR_WRONG_PASSWORD,
LOG_ACCESS_GRANTED,
LOG_ATTEMPTED_LOGIN_BUT_FAILED,
} from './constants';
// ----------------------------------------------------------------------
/**
* This API is used for demo purpose only
* You should use a real database
* You should hash the password before saving to database
* You should not save the password in the database
* You should not expose the JWT_SECRET in the client side
*/
export async function POST(req: NextRequest) {
const debug = { 'req.headers': flattenNextjsRequest(req) };
try {
const { email, password } = await req.json();
const currentUser = await prisma.partyUser.findFirst({ where: { email } });
if (!currentUser) {
await createAccessLog('', LOG_USER_TRIED_LOGIN_WITH_EMAIL, { email, debug });
return response({ message: ERR_USER_NOT_FOUND }, STATUS.UNAUTHORIZED);
}
if (currentUser?.password !== password) {
await createAccessLog(currentUser.id, LOG_USER_LOGGED_WITH_WRONG_PASSWORD, { debug });
return response({ message: ERR_WRONG_PASSWORD }, STATUS.UNAUTHORIZED);
}
const accessToken = await sign({ userId: currentUser?.id }, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
});
await createAccessLog(currentUser.id, LOG_ACCESS_GRANTED, { debug });
return response({ user: currentUser, accessToken }, STATUS.OK);
} catch (error) {
await createAccessLog('', LOG_ATTEMPTED_LOGIN_BUT_FAILED, { debug, error });
return handleError('Auth - Sign in', error);
}
}

View File

@@ -0,0 +1,42 @@
# REQ0188 frontend party-user
###
GET http://localhost:7272/api/auth/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWMwdWo4bGwwMDByM2Y1eXhob3JuMW1hIiwiaWF0IjoxNzUwMjE5NTgwLCJleHAiOjE3NTE0MjkxODB9.7BtuIKEvwDcHc5j9JYX0Eb1uB37kFH1Ksx4MTDTtEWQ
###
# username and password ok
POST http://localhost:7272/api/party-user-auth/sign-in
content-type: application/json
{
"email": "party_user0@prisma.io",
"password": "Aa12345678"
}
###
# There is no user corresponding to the email address.
POST http://localhost:7272/api/party-user-auth/sign-in
content-type: application/json
{
"email": "demo@minimals1.cc",
"password": "@2Minimal"
}
###
# Wrong password
POST http://localhost:7272/api/party-user-auth/sign-in
content-type: application/json
{
"email": "demo@minimals.cc",
"password": "@2Min111imal"
}

View File

@@ -0,0 +1,58 @@
import type { NextRequest } from 'next/server';
import { sign } from 'src/utils/jwt';
import { STATUS, response, handleError } from 'src/utils/response';
import { _users, JWT_SECRET, JWT_EXPIRES_IN } from 'src/_mock/_auth';
// ----------------------------------------------------------------------
/**
* This API is used for demo purpose only
* You should use a real database
* You should hash the password before saving to database
* You should not save the password in the database
* You should not expose the JWT_SECRET in the client side
*/
export const runtime = 'edge';
export async function POST(req: NextRequest) {
try {
const { email, password, firstName, lastName } = await req.json();
const userExists = _users.find((user) => user.email === email);
if (userExists) {
return response({ message: 'There already exists an account with the given email address.' }, STATUS.CONFLICT);
}
const newUser = {
id: _users[0].id,
displayName: `${firstName} ${lastName}`,
email,
password,
photoURL: '',
phoneNumber: '',
country: '',
address: '',
state: '',
city: '',
zipCode: '',
about: '',
role: 'user',
isPublic: true,
};
const accessToken = await sign({ userId: newUser.id }, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
});
// Push new user to database
_users.push(newUser);
return response({ user: newUser, accessToken }, STATUS.OK);
} catch (error) {
return handleError('Auth - Sign up', error);
}
}

View File

@@ -0,0 +1,60 @@
<!-- NOTES to AI: please maintain same format and level of detail when edit this file -->
# GUIDELINE
- Party-user Management API endpoint for managing party-user accounts
- Handles party-user CRUD operations and profile updates
- Follows Next.js API route conventions
## Endpoints
### `create/route.ts`
Creates new party-user account with required details
### `delete/route.ts`
Deletes party-user account by ID
### `details/route.ts`
Gets detailed party-user information
### `helloworld/route.ts`
Simple test endpoint returning "Hello World"
### `list/route.ts`
Lists all party-users with pagination support
### `search/route.ts`
Searches party-users by name or email
### `update/route.ts`
Updates party-user profile information
## Testing
Test files are available per endpoint in their respective directories:
- `create/test.http`
- `delete/test.http`
- `details/test.http`
- `helloworld/test.http`
- `list/test.http`
- `update/test.http`
## Related Services
`../../services/party-user.service.ts` (assumed) would handle:
`createUser` - Register new party-user account
`updateUser` - Update party-user profile
`deleteUser` - Remove party-user account
`listUsers` - Get paginated party-user list
`searchUsers` - Search party-users by criteria
`changeUserRole` - Modify party-user permissions
`uploadUserImage` - Handle profile picture uploads

View File

@@ -0,0 +1,34 @@
// src/app/api/party-user/create/route.ts
//
// PURPOSE:
// create new party user in db
//
// RULES:
// 1. Validates input data shape
// 2. Returns created party user data
//
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { createPartyUser } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
/**
***************************************
* POST - create PartyUser
***************************************
*/
export async function POST(req: NextRequest) {
const { partyUserData } = await req.json();
try {
const partyUser = await createPartyUser(partyUserData);
return response({ partyUser }, STATUS.OK);
} catch (error) {
return handleError('PartyUser - Create', error);
}
}

View File

@@ -0,0 +1,19 @@
###
POST http://localhost:7272/api/party-user/create
Content-Type: application/json
{
"partyUserData": {
"name": "Alice 123321",
"username": null,
"email": "alice@123111321.io",
"emailVerified": "2025-06-15T17:47:23.919Z",
"password": "Aa12345678",
"bucketImage": null,
"admin": false,
"info": null,
"phoneNumber": "+85291234567",
"avatarUrl": ""
}
}

View File

@@ -0,0 +1,35 @@
// src/app/api/party-user/delete/route.ts
//
// PURPOSE:
// delete party user from db by id
//
// RULES:
// 1. Requires valid party user ID
// 2. Returns deleted party user ID
// 3. Handles errors appropriately
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { deletePartyUser } from 'src/app/services/party-user.service';
/**
**************************************
* PATCH - Delete party user
***************************************
*/
export async function PATCH(req: NextRequest) {
try {
const { partyUserId } = await req.json();
if (!partyUserId) throw new Error('partyUserId cannot be null');
await deletePartyUser(partyUserId);
return response({ partyUserId }, STATUS.OK);
} catch (error) {
return handleError('PartyUser - Delete', error);
}
}

View File

@@ -0,0 +1,8 @@
###
PATCH http://localhost:7272/api/party-user/delete
Content-Type: application/json
{
"partyUserId": "cmbxz8t2b000oiigxib3o4jla"
}

View File

@@ -0,0 +1,43 @@
// src/app/api/party-user/details/route.ts
//
// PURPOSE:
// get party user details from db by id
//
// RULES:
// 1. Requires valid partyUserId parameter
// 2. Returns party user details if found
// 3. Handles not found case appropriately
// 4. Logs the operation
import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import { getPartyUser } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
/** **************************************
* GET PartyUser detail
*************************************** */
export async function GET(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
// RULES: userId must exist
const partyUserId = searchParams.get('partyUserId');
if (!partyUserId) return response({ message: 'partyUserId is required!' }, STATUS.BAD_REQUEST);
// NOTE: userId confirmed exist, run below
const partyUser = await getPartyUser(partyUserId);
if (!partyUser) return response({ message: 'User not found!' }, STATUS.NOT_FOUND);
logger('[User] details', partyUser.id);
return response({ partyUser }, STATUS.OK);
} catch (error) {
return handleError('PartyUser - Get details', error);
}
}

View File

@@ -0,0 +1,4 @@
###
GET http://localhost:7272/api/party-user/details?partyUserId=cmbxziat6000b715hmhrnwlx2

View File

@@ -0,0 +1,11 @@
import type { NextRequest, NextResponse } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
export async function GET(req: NextRequest, res: NextResponse) {
try {
return response({ helloworld: 'party-user' }, STATUS.OK);
} catch (error) {
return handleError('GET - helloworld', error);
}
}

View File

@@ -0,0 +1,3 @@
###
GET http://localhost:7272/api/party-user/helloworld

View File

@@ -0,0 +1,33 @@
// src/app/api/party-user/update/route.ts
//
// PURPOSE:
// update existing party user in db
//
// RULES:
// 1. Requires valid party user ID
// 2. Validates input data shape
//
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import { listPartyUsers } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
/**
***************************************
* GET - Products
***************************************
*/
export async function GET() {
try {
const partyUsers = await listPartyUsers();
logger('[User] list', partyUsers.length);
return response({ partyUsers }, STATUS.OK);
} catch (error) {
return handleError('Product - Get list', error);
}
}

View File

@@ -0,0 +1,2 @@
###
GET http://localhost:7272/api/party-user/list

View File

@@ -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 { _products } from 'src/_mock/_product';
// ----------------------------------------------------------------------
export const runtime = 'edge';
/** **************************************
* GET - Search products
*************************************** */
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 products = _products();
// Accept search by name or sku
const results = products.filter(({ name, sku }) => name.toLowerCase().includes(query) || sku?.toLowerCase().includes(query));
logger('[Product] search-results', results.length);
return response({ results }, STATUS.OK);
} catch (error) {
return handleError('Product - Get search', error);
}
}

View File

@@ -0,0 +1,88 @@
// src/app/api/party-user/update/route.ts
//
// PURPOSE:
// update existing party user in db
//
// RULES:
// 1. Requires valid party user ID
// 2. Validates input data shape
// 3. Returns updated party user data
// 4. Handles errors appropriately
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { updatePartyUser } from 'src/app/services/party-user.service';
// ----------------------------------------------------------------------
/** **************************************
* PUT - Update PartyUser
*************************************** */
export async function PUT(req: NextRequest) {
// logger('[Product] list', products.length);
const { partyUserData } = await req.json();
try {
await updatePartyUser(partyUserData.id, partyUserData);
return response({ partyUserData }, STATUS.OK);
} catch (error) {
return handleError('PartyUser - Update', error);
}
}
export type IProductItem = {
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: IProductReview[];
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 IProductReview = {
id: string;
name: string;
rating: number;
comment: string;
helpful: number;
avatarUrl: string;
postedAt: IDateValue;
isPurchased: boolean;
attachments?: string[];
};

View File

@@ -0,0 +1,22 @@
###
PUT http://localhost:7272/api/party-user/update
Content-Type: application/json
{
"partyUserData": {
"id": "cmc0cedkx000boln3viy77598",
"createdAt": "2025-06-15T17:47:24.547Z",
"updatedAt": "2025-06-15T17:47:24.547Z",
"name": "Alice 123321",
"username": null,
"email": "alice@prisma.io",
"emailVerified": "2025-06-15T17:47:23.919Z",
"password": "Aa12345678",
"image": null,
"bucketImage": null,
"admin": false,
"info": null,
"phoneNumber": "+85291234567"
}
}

View File

@@ -13,10 +13,7 @@ import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { isDev } from 'src/constants';
import prisma from '../../../lib/prisma';
import { createProduct } from 'src/app/services/product.service';
// import { createProduct } from 'src/app/services/product.service';
// ----------------------------------------------------------------------

View File

@@ -0,0 +1,81 @@
<!-- AI: please maintain same format and level of detail when edit this file -->
# GUIDELINE
- User Management API endpoint for managing user accounts
- Handles user CRUD operations, role management and profile updates
- Follows Next.js API route conventions
## `route.ts`
Main user route handler with HTTP methods:
- `GET` - Get current user details
- `POST` - Create new user account
- `PUT` - Update user profile
- `DELETE` - Delete user account
## Sub-routes
### `changeToAdmin/route.ts`
Changes user role to admin
### `changeToUser/route.ts`
Changes user role to regular user
### `checkAdmin/route.ts`
Verifies if user has admin privileges
### `createUser/route.ts`
Creates new user account with required details
### `deleteUser/route.ts`
Deletes user account by ID
### `details/route.ts`
Gets detailed user information
### `list/route.ts`
Lists all users with pagination support
### `saveUser/route.ts`
Saves user profile changes
### `search/route.ts`
Searches users by name or email
### `image/upload/route.ts`
Handles user profile picture uploads
## `test.http`
Contains test requests for:
- User authentication
- Role changes (admin/user)
- Profile updates
- Account creation/deletion
- User listing/searching
- Image uploads
## Related Services
`../../services/user.service.ts` (assumed) would handle:
`createUser` - Register new user account
`updateUser` - Update user profile
`deleteUser` - Remove user account
`listUsers` - Get paginated user list
`searchUsers` - Search users by criteria
`changeUserRole` - Modify user permissions
`uploadUserImage` - Handle profile picture uploads

View File

@@ -1,7 +1,13 @@
with knowledge in schema.prisma file,
please refer the below helloworld example `helloworld.service.ts`
and create `user.service.ts` to cover user record
Hi,
thanks
i copied from
`03_source/cms_backend/src/app/services/user.service.ts`
to
`03_source/cms_backend/src/app/services/party-user.service.ts`
`/home/logic/_wsl_workspace/001_github_ws/HKSingleParty-ws/HKSingleParty/03_source/cms_backend/src/app/services/helloworld.service.ts`
with knowledge in `schema.prisma` file, and reference to the sibling files in same folder
i want you to update `party-user.service.ts` content to handle `party user` (the user joining party)
please use the model `PartyUser` to handle it.
thanks.

View File

@@ -1,28 +1,23 @@
// src/app/services/AccessLog.service.ts
// src/app/services/access-log.service.ts
//
// PURPOSE:
// Service for handling AccessLog records
// - Core service for audit logging and access tracking
// - Records all authentication attempts and system access
// - Provides query capabilities for audit trails
// - Integrates with Prisma ORM for database operations
//
// RULES:
// - All methods return Promises
// - Input validation should be done at controller level
// - Errors should be propagated to caller
// - All methods return Promises for async operations
// - Input validation must be done at controller level
// - Errors should be propagated to caller with context
// - Audit records should never be modified after creation
// - Sensitive data should be hashed before logging
// - Metadata should be stored as JSON for flexibility
import type { AccessLog } from '@prisma/client';
import prisma from '../lib/prisma';
// type CreateAccessLog = {
// userId?: string;
// message?: string;
// metadata?: Record<string, any>;
// };
// type UpdateAccessLog = {
// status?: number;
// metadata?: object;
// };
async function listAccessLogs(): Promise<AccessLog[]> {
return prisma.accessLog.findMany({
orderBy: { timestamp: 'desc' },

View File

@@ -47,6 +47,21 @@ async function getEvent(eventId: string): Promise<EventItem | null> {
return prisma.eventItem.findFirst({ where: { id: eventId } });
}
async function getEventItemById(eventId: string): Promise<EventItem | null> {
return prisma.eventItem.findFirst({ where: { id: eventId } });
}
async function countTotalEvents(): Promise<number> {
try {
const result = await prisma.eventItem.findMany();
console.log({ result });
return result.length;
} catch (error) {
console.log(error);
return -1;
}
}
// async function createNewEvent(createForm: CreateEvent) {
// return prisma.event.create({ data: createForm });
// }
@@ -68,4 +83,6 @@ export {
// updateEvent,
// deleteEvent,
// createNewEvent,
getEventItemById,
countTotalEvents,
};

View File

@@ -0,0 +1,66 @@
// src/app/services/party-order.service.ts
//
// PURPOSE:
// - Service for handling PartyOrderItem Record
//
import type { PartyOrderItem } from '@prisma/client';
import prisma from '../lib/prisma';
type CreateOrder = {
status: string;
taxes: number;
shipping: number;
discount: number;
items: any[];
customer: any;
payment: any;
delivery: any;
shippingAddress: any;
billingAddress: any;
};
type UpdateOrder = {
status?: string;
taxes?: number;
shipping?: number;
discount?: number;
items?: any[];
customer?: any;
payment?: any;
delivery?: any;
shippingAddress?: any;
billingAddress?: any;
};
async function listPartyOrders(): Promise<PartyOrderItem[]> {
return prisma.partyOrderItem.findMany();
}
async function getPartyOrder(partyOrderId: string): Promise<PartyOrderItem | null> {
return prisma.partyOrderItem.findUnique({
where: { id: partyOrderId },
});
}
async function createOrder(orderData: any) {
return prisma.partyOrderItem.create({
data: orderData,
});
}
async function updateOrder(orderId: string, updateData: UpdateOrder) {
return prisma.partyOrderItem.update({
where: { id: orderId },
data: updateData,
});
}
async function deleteOrder(orderId: string) {
return prisma.partyOrderItem.delete({
where: { id: orderId },
});
}
export { getPartyOrder as getOrder, createOrder, updateOrder, deleteOrder, listPartyOrders, type CreateOrder, type UpdateOrder };

View File

@@ -0,0 +1,88 @@
// src/app/services/party-user.service.ts
//
// PURPOSE:
// - Handle Party User Record CRUD operations
// - Manage party member data and permissions
// - Interface between controllers and database
//
// RULES:
// - Follow Prisma best practices for database operations
// - Validate all party user data before processing
// - Enforce party-specific business rules
// - Maintain audit trail for sensitive operations
//
import type { PartyUser } 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 listPartyUsers(): Promise<PartyUser[]> {
return prisma.partyUser.findMany({});
}
async function getPartyUser(partyUserId: string): Promise<PartyUser | null> {
return prisma.partyUser.findUnique({
where: { id: partyUserId },
// include: { reviews: true },
});
}
async function getPartyUserByEmail(email: string): Promise<PartyUser | null> {
return prisma.partyUser.findUnique({
where: { email },
});
}
async function getPartyUserById(id: string): Promise<PartyUser | null> {
return prisma.partyUser.findFirst({ where: { id } });
}
async function createPartyUser(partyUserData: any): Promise<PartyUser> {
return prisma.partyUser.create({ data: partyUserData });
}
async function updatePartyUser(partyUserId: string, partyUserData: any): Promise<PartyUser | null> {
return prisma.partyUser.update({
where: { id: partyUserId },
data: partyUserData,
});
}
async function deletePartyUser(partyUserId: string): Promise<PartyUser | null> {
return prisma.partyUser.delete({
where: { id: partyUserId },
});
}
export {
getPartyUserById,
getPartyUser,
listPartyUsers,
createPartyUser,
updatePartyUser,
deletePartyUser,
//
getPartyUserByEmail,
//
type CreateUser,
type UpdateUser,
//
};

View File

@@ -6,6 +6,7 @@ set -ex
DOCKER_COMPOSE_FILES=" -f docker-compose.yml -f docker-compose.dev.yml"
# docker compose $DOCKER_COMPOSE_FILES build
# docker compose $DOCKER_COMPOSE_FILES push
docker compose $DOCKER_COMPOSE_FILES up -d
# cd ../api_server

View File

@@ -0,0 +1,40 @@
# Server url
# Local server configuration at: https://docs.minimals.cc/mock-server/
VITE_SERVER_URL=http://localhost:7272
# VITE_SERVER_URL=https://api-dev-minimal-v630.pages.dev
# Public resource directory
VITE_ASSETS_DIR=
# Mapbox
VITE_MAPBOX_API_KEY=
# Firebase
VITE_FIREBASE_API_KEY=
VITE_FIREBASE_AUTH_DOMAIN=
VITE_FIREBASE_PROJECT_ID=
VITE_FIREBASE_STORAGE_BUCKET=
VITE_FIREBASE_MESSAGING_SENDER_ID=
VITE_FIREBASE_APPID=
# Aws
VITE_AWS_AMPLIFY_USER_POOL_ID=
VITE_AWS_AMPLIFY_USER_POOL_WEB_CLIENT_ID=
VITE_AWS_AMPLIFY_REGION=
# Auth0
VITE_AUTH0_DOMAIN=
VITE_AUTH0_CLIENT_ID=
VITE_AUTH0_CALLBACK_URL=
# Supabase
VITE_SUPABASE_URL=https://prhsilyjzxbkufchywxt.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InByaHNpbHlqenhia3VmY2h5d3h0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzY2NjM5MTEsImV4cCI6MjA1MjIzOTkxMX0.UBvxEhKEMtsRuDWBSDTglfnupKf9fyPI9IvQBxS5F6U
# Google Calendar
GOOGLE_CLIENT_ID=463956183839-i7j5nt5rkpbm4npukg21vfnhav5vvgeh.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=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
GOOGLE_CALENDAR_API_KEY=AIzaSyDh4-SOuKTopXe45oDM9nQA7R4cNJ1So0c
VITE_GOOGLE_CLIENT_ID=463956183839-i7j5nt5rkpbm4npukg21vfnhav5vvgeh.apps.googleusercontent.com
VITE_GOOGLE_CALENDAR_API_KEY=AIzaSyDh4-SOuKTopXe45oDM9nQA7R4cNJ1So0c

View File

@@ -3,6 +3,8 @@ FROM node:20-slim
# Install pnpm globally
RUN npm install -g pnpm
RUN apt-get update
RUN apt-get install -qqy psmisc
# Set working directory
WORKDIR /app

View File

@@ -23,7 +23,7 @@
"re:build-npm": "npm run clean && npm install && npm run build",
"tsc:dev": "yarn dev & yarn tsc:watch",
"tsc:print": "npx tsc --showConfig",
"tsc:w": "npx nodemon --delay 1 --ext ts,tsx --exec \"yarn tsc\"",
"tsc:w": "npx nodemon --delay 1 --ext ts,tsx --exec \"yarn tsc || (sleep 10; touch src/app.tsx)\"",
"tsc:watch": "tsc --noEmit --watch",
"tsc": "tsc --noEmit"
},
@@ -147,4 +147,4 @@
"vite": "^6.2.3",
"vite-plugin-checker": "^0.9.1"
}
}
}

View File

@@ -10,6 +10,9 @@ while true; do
yarn dev --force --clearScreen
killall node
killall yarn
echo "restarting..."
sleep 1
done

View File

@@ -2,6 +2,8 @@
set -x
killall node
killall yarn
rm -rf ./**/*Zone.Identifier
set -ex

View File

@@ -1,14 +1,14 @@
export const PRODUCT_GENDER_OPTIONS = [
export const PARTY_EVENT_GENDER_OPTIONS = [
{ label: 'Men', value: 'Men' },
{ label: 'Women', value: 'Women' },
{ label: 'Kids', value: 'Kids' },
];
export const PRODUCT_CATEGORY_OPTIONS = ['Shose', 'Apparel', 'Accessories'];
export const PARTY_EVENT_CATEGORY_OPTIONS = ['Shose', 'Apparel', 'Accessories'];
export const PRODUCT_RATING_OPTIONS = ['up4Star', 'up3Star', 'up2Star', 'up1Star'];
export const PARTY_EVENT_RATING_OPTIONS = ['up4Star', 'up3Star', 'up2Star', 'up1Star'];
export const PRODUCT_COLOR_OPTIONS = [
export const PARTY_EVENT_COLOR_OPTIONS = [
'#FF4842',
'#1890FF',
'#FFC0CB',
@@ -19,7 +19,7 @@ export const PRODUCT_COLOR_OPTIONS = [
'#FFFFFF',
];
export const PRODUCT_COLOR_NAME_OPTIONS = [
export const PARTY_EVENT_COLOR_NAME_OPTIONS = [
{ value: '#FF4842', label: 'Red' },
{ value: '#1890FF', label: 'Blue' },
{ value: '#FFC0CB', label: 'Pink' },
@@ -30,7 +30,7 @@ export const PRODUCT_COLOR_NAME_OPTIONS = [
{ value: '#FFFFFF', label: 'White' },
];
export const PRODUCT_SIZE_OPTIONS = [
export const PARTY_EVENT_SIZE_OPTIONS = [
{ value: '7', label: '7' },
{ value: '8', label: '8' },
{ value: '8.5', label: '8.5' },
@@ -44,26 +44,26 @@ export const PRODUCT_SIZE_OPTIONS = [
{ value: '13', label: '13' },
];
export const PRODUCT_STOCK_OPTIONS = [
export const PARTY_EVENT_STOCK_OPTIONS = [
{ value: 'in stock', label: 'In stock' },
{ value: 'low stock', label: 'Low stock' },
{ value: 'out of stock', label: 'Out of stock' },
];
// not used due to i18n
export const PRODUCT_PUBLISH_OPTIONS = [
export const PARTY_EVENT_PUBLISH_OPTIONS = [
{ value: 'published', label: 'Published' },
{ value: 'draft', label: 'Draft' },
];
export const PRODUCT_SORT_OPTIONS = [
export const PARTY_EVENT_SORT_OPTIONS = [
{ value: 'featured', label: 'Featured' },
{ value: 'newest', label: 'Newest' },
{ value: 'priceDesc', label: 'Price: High - Low' },
{ value: 'priceAsc', label: 'Price: Low - High' },
];
export const PRODUCT_CATEGORY_GROUP_OPTIONS = [
export const PARTY_EVENT_CATEGORY_GROUP_OPTIONS = [
{ group: 'Clothing', classify: ['Shirts', 'T-shirts', 'Jeans', 'Leather', 'Accessories'] },
{ group: 'Tailored', classify: ['Suits', 'Blazers', 'Trousers', 'Waistcoats', 'Apparel'] },
{ group: 'Accessories', classify: ['Shoes', 'Backpacks and bags', 'Bracelets', 'Face masks'] },

View File

@@ -0,0 +1,89 @@
import { _mock } from './_mock';
// ----------------------------------------------------------------------
export const PARTY_ORDER_STATUS_OPTIONS = [
{ value: 'pending', label: 'Pending' },
{ value: 'completed', label: 'Completed' },
{ value: 'cancelled', label: 'Cancelled' },
{ value: 'refunded', label: 'Refunded' },
];
const ITEMS = Array.from({ length: 3 }, (_, index) => ({
id: _mock.id(index),
sku: `16H9UR${index}`,
quantity: index + 1,
name: _mock.productName(index),
coverUrl: _mock.image.product(index),
price: _mock.number.price(index),
}));
export const _party_orders = Array.from({ length: 20 }, (_, index) => {
const shipping = 10;
const discount = 10;
const taxes = 10;
const items = (index % 2 && ITEMS.slice(0, 1)) || (index % 3 && ITEMS.slice(1, 3)) || ITEMS;
const totalQuantity = items.reduce((accumulator, item) => accumulator + item.quantity, 0);
const subtotal = items.reduce((accumulator, item) => accumulator + item.price * item.quantity, 0);
const totalAmount = subtotal - shipping - discount + taxes;
const customer = {
id: _mock.id(index),
name: _mock.fullName(index),
email: _mock.email(index),
avatarUrl: _mock.image.avatar(index),
ipAddress: '192.158.1.38',
};
const delivery = { shipBy: 'DHL', speedy: 'Standard', trackingNumber: 'SPX037739199373' };
const history = {
orderTime: _mock.time(1),
paymentTime: _mock.time(2),
deliveryTime: _mock.time(3),
completionTime: _mock.time(4),
timeline: [
{ title: 'Delivery successful', time: _mock.time(1) },
{ title: 'Transporting to [2]', time: _mock.time(2) },
{ title: 'Transporting to [1]', time: _mock.time(3) },
{ title: 'The shipping unit has picked up the goods', time: _mock.time(4) },
{ title: 'Order has been created', time: _mock.time(5) },
],
};
return {
id: _mock.id(index),
orderNumber: `#601${index}`,
createdAt: _mock.time(index),
taxes,
items,
history,
subtotal,
shipping,
discount,
customer,
delivery,
totalAmount,
totalQuantity,
shippingAddress: {
fullAddress: '19034 Verna Unions Apt. 164 - Honolulu, RI / 87535',
phoneNumber: '365-374-4961',
},
payment: {
//
cardType: 'mastercard',
cardNumber: '**** **** **** 5678',
},
status:
(index % 2 && 'completed') ||
(index % 3 && 'pending') ||
(index % 4 && 'cancelled') ||
'refunded',
};
});

View File

@@ -23,3 +23,7 @@ export * from './_product';
export * from './_overview';
export * from './_calendar';
export * from './_party-event';
export * from './_party-order';

View File

@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { endpoints, fetcher } from 'src/lib/axios';
import { fetcher } from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import type { IPostItem } from 'src/types/blog';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';

View File

@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import axios, { endpoints, fetcher } from 'src/lib/axios';
import axios, { fetcher } from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import type { ICalendarEvent } from 'src/types/calendar';
import type { SWRConfiguration } from 'swr';
import useSWR, { mutate } from 'swr';

View File

@@ -1,6 +1,7 @@
import { keyBy } from 'es-toolkit';
import { useMemo } from 'react';
import axios, { endpoints, fetcher } from 'src/lib/axios';
import axios, { fetcher } from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import type { IChatConversation, IChatMessage, IChatParticipant } from 'src/types/chat';
import type { SWRConfiguration } from 'swr';
import useSWR, { mutate } from 'swr';

View File

@@ -1,6 +1,7 @@
// src/actions/invoice.ts
import { useMemo } from 'react';
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
import axiosInstance, { fetcher } from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import type { IInvoiceItem, SaveInvoiceData } from 'src/types/invoice';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';

View File

@@ -1,6 +1,7 @@
import type { UniqueIdentifier } from '@dnd-kit/core';
import { startTransition, useMemo } from 'react';
import axios, { endpoints, fetcher } from 'src/lib/axios';
import axios, { fetcher } from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import type { IKanban, IKanbanColumn, IKanbanTask } from 'src/types/kanban';
import type { SWRConfiguration } from 'swr';
import useSWR, { mutate } from 'swr';

View File

@@ -1,6 +1,7 @@
import { keyBy } from 'es-toolkit';
import { useMemo } from 'react';
import { endpoints, fetcher } from 'src/lib/axios';
import { fetcher } from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import type { IMail, IMailLabel } from 'src/types/mail';
import type { SWRConfiguration } from 'swr';
import useSWR from 'swr';

View File

@@ -1,6 +1,7 @@
// src/actions/order.ts
import { useMemo } from 'react';
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
import axiosInstance, { fetcher } from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import type { IOrderItem } from 'src/types/order';
import type { IProductItem } from 'src/types/product';
import type { SWRConfiguration } from 'swr';

View File

@@ -1,7 +1,8 @@
// src/actions/product.ts
// src/actions/party-event.ts
//
import { useMemo } from 'react';
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
import axiosInstance, { fetcher } from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import type { IPartyEventItem } from 'src/types/party-event';
import type { SWRConfiguration } from 'swr';
import useSWR, { mutate } from 'swr';
@@ -74,6 +75,7 @@ type SearchResultsData = {
results: IPartyEventItem[];
};
// TODO: update useSearchPartyEvents
export function useSearchProducts(query: string) {
const url = query ? [endpoints.product.search, { params: { query } }] : '';
@@ -135,7 +137,6 @@ export async function updatePartyEvent(partyEventData: Partial<IPartyEventItem>)
/**
* Work in local
*/
mutate(
endpoints.partyEvent.list,
(currentData: any) => {
@@ -163,7 +164,6 @@ export async function deletePartyEvent(partyEventId: string) {
/**
* Work in local
*/
mutate(
endpoints.partyEvent.list,
(currentData: any) => {

View File

@@ -0,0 +1,209 @@
// src/actions/party-order.ts
//
import { useMemo } from 'react';
import axiosInstance, { fetcher } from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import type { IPartyOrderItem } from 'src/types/party-order';
import type { SWRConfiguration } from 'swr';
import useSWR, { mutate } from 'swr';
// ----------------------------------------------------------------------
const swrOptions: SWRConfiguration = {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
};
// ----------------------------------------------------------------------
type PartyOrdersData = {
partyOrders: IPartyOrderItem[];
};
export function useGetPartyOrders() {
const url = endpoints.partyOrder.list;
const { data, isLoading, error, isValidating } = useSWR<PartyOrdersData>(
url,
fetcher,
swrOptions
);
const memoizedValue = useMemo(
() => ({
partyOrders: data?.partyOrders || [],
partyOrdersLoading: isLoading,
partyOrdersError: error,
partyOrdersValidating: isValidating,
partyOrdersEmpty: !isLoading && !isValidating && !data?.partyOrders.length,
}),
[data?.partyOrders, error, isLoading, isValidating]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type PartyOrderData = {
partyOrder: IPartyOrderItem;
};
export function useGetPartyOrder(partyOrderId: string) {
const url = partyOrderId ? [endpoints.partyOrder.details, { params: { partyOrderId } }] : '';
const { data, isLoading, error, isValidating } = useSWR<PartyOrderData>(url, fetcher, swrOptions);
const memoizedValue = useMemo(
() => ({
partyOrder: data?.partyOrder,
partyOrderLoading: isLoading,
partyOrderError: error,
partyOrderValidating: isValidating,
}),
[data?.partyOrder, error, isLoading, isValidating]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type SearchResultsData = {
results: IPartyOrderItem[];
};
export function useSearchProducts(query: string) {
const url = query ? [endpoints.product.search, { params: { query } }] : '';
const { data, isLoading, error, isValidating } = useSWR<SearchResultsData>(url, fetcher, {
...swrOptions,
keepPreviousData: true,
});
const memoizedValue = useMemo(
() => ({
searchResults: data?.results || [],
searchLoading: isLoading,
searchError: error,
searchValidating: isValidating,
searchEmpty: !isLoading && !isValidating && !data?.results.length,
}),
[data?.results, error, isLoading, isValidating]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
export async function createPartyOrder(partyOrderData: IPartyOrderItem) {
/**
* Work on server
*/
const data = { partyOrderData };
const {
data: { id },
} = await axiosInstance.post(endpoints.partyOrder.create, data);
/**
* Work in local
*/
mutate(
endpoints.partyOrder.list,
(currentData: any) => {
const currentPartyOrders: IPartyOrderItem[] = currentData?.partyOrders;
const partyOrders = [...currentPartyOrders, { ...partyOrderData, id }];
return { ...currentData, partyOrders };
},
false
);
}
// ----------------------------------------------------------------------
export async function updatePartyOrder(partyOrderData: Partial<IPartyOrderItem>) {
/**
* Work on server
*/
const data = { partyOrderData };
await axiosInstance.put(endpoints.partyOrder.update, data);
/**
* Work in local
*/
mutate(
endpoints.partyOrder.list,
(currentData: any) => {
const currentPartyOrders: IPartyOrderItem[] = currentData?.partyOrders;
const partyOrders = currentPartyOrders.map((partyOrder) =>
partyOrder.id === partyOrderData.id ? { ...partyOrder, ...partyOrderData } : partyOrder
);
return { ...currentData, partyOrders };
},
false
);
}
// ----------------------------------------------------------------------
export async function deletePartyOrder(partyOrderId: string) {
/**
* Work on server
*/
const data = { partyOrderId };
await axiosInstance.patch(endpoints.partyOrder.delete, data);
/**
* Work in local
*/
mutate(
endpoints.partyOrder.list,
(currentData: any) => {
const currentProducts: IPartyOrderItem[] = currentData?.partyOrders;
const partyOrders = currentProducts.filter((partyOrder) => partyOrder.id !== partyOrderId);
return { ...currentData, partyOrders };
},
false
);
}
// ----------------------------------------------------------------------
// TODO: implement partyOrder changeStatus with url below
// const url = endpoints.order.changeStatus(orderId);
export async function changeStatus(partyOrderData: any, dummy: any) {
return true;
// /**
// * Work on server
// */
// const data = { partyOrderData };
// await axiosInstance.put(endpoints.partyOrder.update, data);
// /**
// * Work in local
// */
// mutate(
// endpoints.partyOrder.list,
// (currentData: any) => {
// const currentPartyOrders: IPartyOrderItem[] = currentData?.partyOrders;
// const partyOrders = currentPartyOrders.map((partyOrder) =>
// partyOrder.id === partyOrderData.id ? { ...partyOrder, ...partyOrderData } : partyOrder
// );
// return { ...currentData, partyOrders };
// },
// false
// );
}

View File

@@ -0,0 +1,290 @@
// src/actions/party-user1.ts
//
import { useMemo } from 'react';
import axiosInstance, { fetcher } from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import type { IPartyUserItem } from 'src/types/party-user';
import type { IProductItem } from 'src/types/product';
import type { SWRConfiguration } from 'swr';
import useSWR, { mutate } from 'swr';
// ----------------------------------------------------------------------
const swrOptions: SWRConfiguration = {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
};
// ----------------------------------------------------------------------
type PartyUsersData = {
partyUsers: IPartyUserItem[];
};
/**
* Fetches list of party users with SWR caching
* @returns {Object} Contains:
* - partyUsers: Array of party user items
* - partyUsersLoading: Loading state
* - partyUsersError: Error object if any
* - partyUsersValidating: Validation state
* - partyUsersEmpty: Boolean if no users found
*/
export function useGetPartyUsers() {
const url = endpoints.partyUser.list;
const { data, isLoading, error, isValidating } = useSWR<PartyUsersData>(url, fetcher, swrOptions);
const memoizedValue = useMemo(
() => ({
partyUsers: data?.partyUsers || [],
partyUsersLoading: isLoading,
partyUsersError: error,
partyUsersValidating: isValidating,
partyUsersEmpty: !isLoading && !isValidating && !data?.partyUsers.length,
}),
[data?.partyUsers, error, isLoading, isValidating]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type PartyUserData = {
partyUser: IPartyUserItem;
};
export function useGetPartyUser(partyUserId: string) {
const { data, isLoading, error, isValidating } = useSWR<PartyUserData>(
endpoints.partyUser.detailsByPartyUserId(partyUserId),
fetcher,
swrOptions
);
const memoizedValue = useMemo(
() => ({
partyUser: data?.partyUser,
partyUserLoading: isLoading,
partyUserError: error,
partyUserValidating: isValidating,
}),
[data?.partyUser, error, isLoading, isValidating]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type SearchResultsData = {
results: IProductItem[];
};
/**
* Searches products by query with SWR caching
* @param {string} query - Search term
* @returns {Object} Contains:
* - searchResults: Array of matching products
* - searchLoading: Loading state
* - searchError: Error object if any
* - searchValidating: Validation state
* - searchEmpty: Boolean if no results found
*/
export function useSearchProducts(query: string) {
const url = query ? [endpoints.product.search, { params: { query } }] : '';
const { data, isLoading, error, isValidating } = useSWR<SearchResultsData>(url, fetcher, {
...swrOptions,
keepPreviousData: true,
});
const memoizedValue = useMemo(
() => ({
searchResults: data?.results || [],
searchLoading: isLoading,
searchError: error,
searchValidating: isValidating,
searchEmpty: !isLoading && !isValidating && !data?.results.length,
}),
[data?.results, error, isLoading, isValidating]
);
return memoizedValue;
}
// ----------------------------------------------------------------------
type SaveUserData = {
name: string;
city: string;
role: string;
email: string;
state: string;
status: string;
address: string;
country: string;
zipCode: string;
company: string;
avatarUrl: string;
phoneNumber: string;
isVerified: boolean;
//
username: string;
password: string;
};
/**
* Creates a new party user with optimistic UI updates
* @param {CreateUserData} partyUserData - Data for the new party user
* @returns {Promise<void>}
* @sideeffects
* - Makes POST request to create user on server
* - Updates local SWR cache optimistically
* - Triggers revalidation of party user list
*/
export async function createPartyUser(partyUserData: CreateUserData) {
/**
* Work on server
*/
const data = { partyUserData };
const {
data: { id },
} = await axiosInstance.post(endpoints.partyUser.create, data);
/**
* Work in local
*/
mutate(
endpoints.partyUser.list,
(currentData: any) => {
const currentPartyUsers: IPartyUserItem[] = currentData?.partyUsers;
const partyUsers = [...currentPartyUsers, { ...partyUserData, id }];
return { ...currentData, partyUsers };
},
false
);
}
// ----------------------------------------------------------------------
/**
* Updates party user data with optimistic UI updates
* @param {Partial<IPartyUserItem>} partyUserData - Partial user data containing at least the ID
* @returns {Promise<void>}
* @sideeffects
* - Makes PUT request to update user on server
* - Updates both list and detail views in local SWR cache
* - Preserves unchanged fields while updating modified ones
*/
export async function updatePartyUser(partyUserData: Partial<IPartyUserItem>) {
/**
* Work on server
*/
const data = { partyUserData };
await axiosInstance.put(endpoints.partyUser.update, data);
/**
* Work in local
*/
mutate(
endpoints.partyUser.list,
(currentData: any) => {
const currentPartyUsers: IPartyUserItem[] = currentData?.partyUsers;
const partyUsers = currentPartyUsers.map((partyUser) =>
partyUser.id === partyUserData.id ? { ...partyUser, ...partyUserData } : partyUser
);
return { ...currentData, partyUsers };
},
false
);
const partyUserId: string = partyUserData.id || '';
mutate(
endpoints.partyUser.detailsByPartyUserId(partyUserId),
(currentData: any) => {
const currentPartyUser: IPartyUserItem = currentData?.partyUser;
console.log({ currentPartyUser });
const partyUser = partyUserData;
return { ...currentData, partyUser };
},
false
);
}
/**
* Tests connection to product API endpoint
* @param {SaveUserData} saveUserData - User data object (currently unused)
* @returns {Promise<AxiosResponse>} Response from test endpoint
* @deprecated This function should be renamed to better reflect its purpose
* TODO: Rename to testProductApiConnection() since it tests product API connection
* TODO: Or implement actual image upload functionality if needed
*/
export async function uploadUserImage(saveUserData: SaveUserData) {
console.log('uploadUserImage ?');
// const url = userId ? [endpoints.user.details, { params: { userId } }] : '';
const res = await axiosInstance.get('http://localhost:7272/api/product/helloworld');
return res;
}
// ----------------------------------------------------------------------
type CreateUserData = {
name: string;
city: string;
role: string;
email: string;
state: string;
status: string;
address: string;
country: string;
zipCode: string;
company: string;
avatarUrl: string;
phoneNumber: string;
isVerified: boolean;
//
username: string;
password: string;
};
/**
* Deletes a party user with optimistic UI updates
* @param {string} partyUserId - ID of user to delete
* @returns {Promise<void>}
* @sideeffects
* - Makes PATCH request to mark user as deleted on server
* - Removes user from local SWR cache
* - Triggers revalidation of party user list
*/
export async function deletePartyUser(partyUserId: string) {
/**
* Work on server
*/
const data = { partyUserId };
await axiosInstance.patch(endpoints.partyUser.delete, data);
/**
* Work in local
*/
mutate(
endpoints.partyUser.list,
(currentData: any) => {
const currentPartyUsers: IPartyUserItem[] = currentData?.partyUsers;
const partyUsers = currentPartyUsers.filter((partyUser) => partyUser.id !== partyUserId);
return { ...currentData, partyUsers };
},
false
);
}

View File

@@ -1,7 +1,8 @@
// src/actions/product.ts
//
import { useMemo } from 'react';
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
import axiosInstance, { fetcher } from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import type { IProductItem } from 'src/types/product';
import type { SWRConfiguration } from 'swr';
import useSWR, { mutate } from 'swr';

View File

@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import axiosInstance, { endpoints, fetcher } from 'src/lib/axios';
import axiosInstance, { fetcher } from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import type { IProductItem } from 'src/types/product';
import { IUserItem } from 'src/types/user';
import type { SWRConfiguration } from 'swr';

View File

@@ -1,4 +1,5 @@
import axios, { endpoints } from 'src/lib/axios';
import axios from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import { JWT_STORAGE_KEY } from './constant';
import { setSession } from './utils';

View File

@@ -1,6 +1,7 @@
import { useSetState } from 'minimal-shared/hooks';
import { useCallback, useEffect, useMemo } from 'react';
import axios, { endpoints } from 'src/lib/axios';
import axios from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import type { AuthState } from '../../types';
import { AuthContext } from '../auth-context';
import { JWT_STORAGE_KEY } from './constant';

View File

@@ -0,0 +1,2 @@
export const ERR_ACCESS_TOKEN_NOT_FOUND = `Access token not found in response`;
export const ACCESS_TOKEN_NOT_FOUND_IN_RESPONSE = 'Access token not found in response';

View File

@@ -0,0 +1,85 @@
import axios from 'src/lib/axios';
import { endpoints } from 'src/lib/endpoints';
import { JWT_STORAGE_KEY } from './constant';
import { ACCESS_TOKEN_NOT_FOUND_IN_RESPONSE, ERR_ACCESS_TOKEN_NOT_FOUND } from './ERRORS';
import { setSession } from './utils';
// ----------------------------------------------------------------------
export type SignInParams = {
email: string;
password: string;
};
export type SignUpParams = {
email: string;
password: string;
firstName: string;
lastName: string;
};
/** **************************************
* Sign in
*************************************** */
export const signInWithPassword = async ({ email, password }: SignInParams): Promise<void> => {
try {
const params = { email, password };
const res = await axios.post(endpoints.partyUserAuth.signIn, params);
const { accessToken } = res.data;
if (!accessToken) {
throw new Error(ERR_ACCESS_TOKEN_NOT_FOUND);
}
setSession(accessToken);
} catch (error) {
console.error('Error during sign in:', error);
throw error;
}
};
/** **************************************
* Sign up
*************************************** */
export const signUp = async ({
email,
password,
firstName,
lastName,
}: SignUpParams): Promise<void> => {
const params = {
email,
password,
firstName,
lastName,
};
try {
const res = await axios.post(endpoints.auth.signUp, params);
const { accessToken } = res.data;
if (!accessToken) {
throw new Error(ACCESS_TOKEN_NOT_FOUND_IN_RESPONSE);
}
sessionStorage.setItem(JWT_STORAGE_KEY, accessToken);
} catch (error) {
console.error('Error during sign up:', error);
throw error;
}
};
/** **************************************
* Sign out
*************************************** */
export const signOut = async (): Promise<void> => {
try {
await setSession(null);
} catch (error) {
console.error('Error during sign out:', error);
throw error;
}
};

View File

@@ -0,0 +1 @@
export const JWT_STORAGE_KEY = 'jwt_access_token';

View File

@@ -0,0 +1,7 @@
export * from './utils';
export * from './action';
export * from './constant';
// export * from './auth-provider';

View File

@@ -0,0 +1,94 @@
import axios from 'src/lib/axios';
import { paths } from 'src/routes/paths';
import { JWT_STORAGE_KEY } from './constant';
// ----------------------------------------------------------------------
export function jwtDecode(token: string) {
try {
if (!token) return null;
const parts = token.split('.');
if (parts.length < 2) {
throw new Error('Invalid token!');
}
const base64Url = parts[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const decoded = JSON.parse(atob(base64));
return decoded;
} catch (error) {
console.error('Error decoding token:', error);
throw error;
}
}
// ----------------------------------------------------------------------
export function isValidToken(accessToken: string) {
if (!accessToken) {
return false;
}
try {
const decoded = jwtDecode(accessToken);
if (!decoded || !('exp' in decoded)) {
return false;
}
const currentTime = Date.now() / 1000;
return decoded.exp > currentTime;
} catch (error) {
console.error('Error during token validation:', error);
return false;
}
}
// ----------------------------------------------------------------------
export function tokenExpired(exp: number) {
const currentTime = Date.now();
const timeLeft = exp * 1000 - currentTime;
setTimeout(() => {
try {
alert('Token expired!');
sessionStorage.removeItem(JWT_STORAGE_KEY);
window.location.href = paths.auth.jwt.signIn;
} catch (error) {
console.error('Error during token expiration:', error);
throw error;
}
}, timeLeft);
}
// ----------------------------------------------------------------------
const INVALID_ACCESS_TOKEN = 'Invalid access token!';
export async function setSession(accessToken: string | null) {
try {
if (accessToken) {
sessionStorage.setItem(JWT_STORAGE_KEY, accessToken);
axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
const decodedToken = jwtDecode(accessToken); // ~3 days by minimals server
if (decodedToken && 'exp' in decodedToken) {
tokenExpired(decodedToken.exp);
} else {
throw new Error(INVALID_ACCESS_TOKEN);
}
} else {
sessionStorage.removeItem(JWT_STORAGE_KEY);
delete axios.defaults.headers.common.Authorization;
}
} catch (error) {
console.error('Error during set session:', error);
throw error;
}
}

View File

@@ -0,0 +1,3 @@
export * from './jwt-sign-in-view';
export * from './jwt-sign-up-view';

View File

@@ -0,0 +1,167 @@
import { zodResolver } from '@hookform/resolvers/zod';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import InputAdornment from '@mui/material/InputAdornment';
import Link from '@mui/material/Link';
import { useBoolean } from 'minimal-shared/hooks';
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { Field, Form } from 'src/components/hook-form';
import { Iconify } from 'src/components/iconify';
import { RouterLink } from 'src/routes/components';
import { useRouter } from 'src/routes/hooks';
import { paths } from 'src/routes/paths';
import { z as zod } from 'zod';
import { FormHead } from '../../components/form-head';
import { signInWithPassword } from '../../context/party-user-jwt';
import { useAuthContext } from '../../hooks';
import { getErrorMessage } from '../../utils';
// ----------------------------------------------------------------------
// ----------------------------------------------------------------------
export function JwtSignInView(): React.JSX.Element {
const { t } = useTranslation();
const router = useRouter();
const showPassword = useBoolean();
const { checkUserSession } = useAuthContext();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
type SignInSchemaType = zod.infer<typeof SignInSchema>;
const EMAIL_IS_REQUIRED = 'Email is required!';
const EMAIL_MUST_BE_A_VALID_EMAIL_ADDRESS = 'Email must be a valid email address!';
const PASSWORD_IS_REQUIRED = 'Password is required!';
const PASSWORD_MUST_BE_AT_LEAST_6_CHARACTERS = 'Password must be at least 6 characters!';
const SignInSchema = zod.object({
email: zod
.string()
.min(1, { message: EMAIL_IS_REQUIRED })
.email({ message: EMAIL_MUST_BE_A_VALID_EMAIL_ADDRESS }),
password: zod
.string()
.min(1, { message: PASSWORD_IS_REQUIRED })
.min(6, { message: PASSWORD_MUST_BE_AT_LEAST_6_CHARACTERS }),
});
const defaultValues: SignInSchemaType = {
email: 'party_user0@prisma.io',
password: 'Aa12345678',
};
const methods = useForm<SignInSchemaType>({
resolver: zodResolver(SignInSchema),
defaultValues,
});
const {
handleSubmit,
formState: { isSubmitting },
} = methods;
const onSubmit = handleSubmit(async (data) => {
try {
await signInWithPassword({ email: data.email, password: data.password });
await checkUserSession?.();
router.refresh();
} catch (error) {
console.error(error);
const feedbackMessage = getErrorMessage(error);
setErrorMessage(feedbackMessage);
}
});
const renderForm = () => (
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
<Box sx={{ gap: 1.5, display: 'flex', flexDirection: 'column' }}>
<Link
component={RouterLink}
href="#"
variant="body2"
color="inherit"
sx={{ alignSelf: 'flex-end' }}
>
Forgot password?
</Link>
<Field.Text
name="password"
label="Password"
placeholder="6+ characters"
type={showPassword.value ? 'text' : 'password'}
slotProps={{
inputLabel: { shrink: true },
input: {
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={showPassword.onToggle} edge="end">
<Iconify
icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'}
/>
</IconButton>
</InputAdornment>
),
},
}}
/>
</Box>
<Button
fullWidth
color="inherit"
size="large"
type="submit"
variant="contained"
loading={isSubmitting}
loadingIndicator="Sign in..."
>
{t('sign-in')}
</Button>
</Box>
);
return (
<>
<FormHead
title="Sign in to your account"
description={
<>
{`Dont have an account? `}
<Link component={RouterLink} href={paths.partyUserAuth.jwt.signUp} variant="subtitle2">
{t('get-started')}
</Link>
</>
}
sx={{ textAlign: { xs: 'center', md: 'left' } }}
/>
<Alert severity="info" sx={{ mb: 3 }}>
Use <strong>{defaultValues.email}</strong>
{' with password '}
<strong>{defaultValues.password}</strong>
</Alert>
{!!errorMessage && (
<Alert severity="error" sx={{ mb: 3 }}>
{errorMessage}
</Alert>
)}
<Form methods={methods} onSubmit={onSubmit}>
{renderForm()}
</Form>
</>
);
}

Some files were not shown because too many files have changed in this diff Show More