Compare commits

...

43 Commits

Author SHA1 Message Date
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
177 changed files with 7068 additions and 304 deletions

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,20 @@
---
tags: mobile, payment
---
# REQ0189 party payment flow
frontend page to handle party-user pay join event
edit page T.B.A.
## TODO
## sources
T.B.A.
## branch
develop/requirements/REQ0189
develop/mobile/DummyPayPage/trunk

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 {
@@ -1257,3 +1260,36 @@ model PartyOrderItem {
// 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,7 +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';
@@ -60,8 +62,9 @@ import { partyOrderItemSeed } from './seeds/partyOrderItem';
//
await appLogSeed;
await accessLogSeed;
//
await partyOrderItemSeed;
await partyUserSeed;
// await Blog;
// await Mail;

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

@@ -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,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,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,13 +1,13 @@
Hi,
i copied from
`03_source/cms_backend/src/app/services/party-event.service.ts`
`03_source/cms_backend/src/app/services/user.service.ts`
to
`03_source/cms_backend/src/app/services/party-order.service.ts`
`03_source/cms_backend/src/app/services/party-user.service.ts`
with knowledge in `schema.prisma` file, and reference to the sibling files in same folder
i want you to update `party-order.service.ts` content to handle party order (the purchase order of the party)
please use the model `PartyOrderItem` to handle it.
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,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,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

@@ -1,7 +1,8 @@
// src/actions/party-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 { IPartyOrderItem } from 'src/types/party-order';
import type { SWRConfiguration } from 'swr';
import useSWR, { mutate } from 'swr';

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>
</>
);
}

View File

@@ -0,0 +1,166 @@
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 { useState } from 'react';
import { useForm } from 'react-hook-form';
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 { SignUpTerms } from '../../components/sign-up-terms';
import { signUp } from '../../context/jwt';
import { useAuthContext } from '../../hooks';
import { getErrorMessage } from '../../utils';
// ----------------------------------------------------------------------
export type SignUpSchemaType = zod.infer<typeof SignUpSchema>;
export const SignUpSchema = zod.object({
firstName: zod.string().min(1, { message: 'First name is required!' }),
lastName: zod.string().min(1, { message: 'Last name is required!' }),
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!' }),
});
// ----------------------------------------------------------------------
export function JwtSignUpView() {
const router = useRouter();
const showPassword = useBoolean();
const { checkUserSession } = useAuthContext();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const defaultValues: SignUpSchemaType = {
firstName: 'Hello',
lastName: 'Friend',
email: 'hello@gmail.com',
password: '@2Minimal',
};
const methods = useForm<SignUpSchemaType>({
resolver: zodResolver(SignUpSchema),
defaultValues,
});
const {
handleSubmit,
formState: { isSubmitting },
} = methods;
const onSubmit = handleSubmit(async (data) => {
try {
await signUp({
email: data.email,
password: data.password,
firstName: data.firstName,
lastName: data.lastName,
});
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' }}>
<Box
sx={{ display: 'flex', gap: { xs: 3, sm: 2 }, flexDirection: { xs: 'column', sm: 'row' } }}
>
<Field.Text
name="firstName"
label="First name"
slotProps={{ inputLabel: { shrink: true } }}
/>
<Field.Text
name="lastName"
label="Last name"
slotProps={{ inputLabel: { shrink: true } }}
/>
</Box>
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
<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>
),
},
}}
/>
<Button
fullWidth
color="inherit"
size="large"
type="submit"
variant="contained"
loading={isSubmitting}
loadingIndicator="Create account..."
>
Create account
</Button>
</Box>
);
return (
<>
<FormHead
title="Hi New party user, Get started absolutely free "
description={
<>
{`Already have an account? `}
<Link component={RouterLink} href={paths.auth.jwt.signIn} variant="subtitle2">
Get started
</Link>
</>
}
sx={{ textAlign: { xs: 'center', md: 'left' } }}
/>
{!!errorMessage && (
<Alert severity="error" sx={{ mb: 3 }}>
{errorMessage}
</Alert>
)}
<Form methods={methods} onSubmit={onSubmit}>
{renderForm()}
</Form>
<SignUpTerms />
</>
);
}

View File

@@ -111,6 +111,19 @@ export const navData: NavSectionProps['data'] = [
{ title: 'Details', path: paths.dashboard.partyOrder.demo.details },
],
},
{
title: 'party-user',
path: paths.dashboard.partyUser.root,
icon: ICONS.user,
children: [
{ title: 'Profile', path: paths.dashboard.partyUser.root },
{ title: 'Cards', path: paths.dashboard.partyUser.cards },
{ title: 'List', path: paths.dashboard.partyUser.list },
{ title: 'Create', path: paths.dashboard.partyUser.new },
{ title: 'Edit', path: paths.dashboard.partyUser.demo.edit },
{ title: 'Account', path: paths.dashboard.partyUser.account },
],
},
{
title: 'Product',
path: paths.dashboard.product.root,

View File

@@ -1,3 +1,18 @@
// src/lib/axios.ts
//
// PURPOSE:
// - Centralized Axios instance configuration
// - Global response error handling
// - Standardized API endpoint definitions
// - Reusable fetcher utility
//
// RULES:
// - All API calls must use this axiosInstance
// - Custom error handling in interceptor
// - Endpoints should be defined here for consistency
// - Fetcher should be used for simple GET requests
//
import type { AxiosRequestConfig } from 'axios';
import axios from 'axios';
import { CONFIG } from 'src/global-config';
@@ -27,83 +42,3 @@ export const fetcher = async (args: string | [string, AxiosRequestConfig]) => {
throw error;
}
};
// ----------------------------------------------------------------------
export const endpoints = {
chat: '/api/chat',
kanban: '/api/kanban',
calendar: '/api/calendar',
auth: {
me: '/api/auth/me',
signIn: '/api/auth/sign-in',
signUp: '/api/auth/sign-up',
},
mail: {
list: '/api/mail/list',
details: '/api/mail/details',
labels: '/api/mail/labels',
},
post: {
list: '/api/post/list',
details: '/api/post/details',
latest: '/api/post/latest',
search: '/api/post/search',
},
product: {
list: '/api/product/list',
details: '/api/product/details',
search: '/api/product/search',
save: '/api/product/saveProduct',
create: '/api/product/create',
update: '/api/product/update',
delete: '/api/product/delete',
},
user: {
list: '/api/user/list',
profile: '/api/user/profile',
update: '/api/user/update',
settings: '/api/user/settings',
details: '/api/user/details',
},
order: {
list: '/api/order/list',
profile: '/api/order/profile',
update: '/api/order/update',
settings: '/api/order/settings',
details: '/api/order/details',
changeStatus: (orderId: string) => `/api/order/changeStatus?orderId=${orderId}`,
},
invoice: {
list: '/api/invoice/list',
profile: '/api/invoice/profile',
update: '/api/invoice/update',
saveInvoice: (invoiceId: string) => `/api/invoice/saveInvoice?invoiceId=${invoiceId}`,
settings: '/api/invoice/settings',
details: '/api/invoice/details',
changeStatus: (invoiceId: string) => `/api/invoice/changeStatus?invoiceId=${invoiceId}`,
search: '/api/invoice/search',
},
//
//
//
partyEvent: {
list: '/api/party-event/list',
details: '/api/party-event/details',
search: '/api/party-event/search',
create: '/api/party-event/create',
update: '/api/party-event/update',
delete: '/api/party-event/delete',
},
partyOrder: {
create: '/api/party-order/create',
delete: '/api/party-order/delete',
list: '/api/party-order/list',
profile: '/api/party-order/profile',
update: '/api/party-order/update',
settings: '/api/party-order/settings',
details: '/api/party-order/details',
changeStatus: (partyOrderId: string) =>
`/api/party-order/changeStatus?partyOrderId=${partyOrderId}`,
},
};

View File

@@ -0,0 +1,96 @@
// ----------------------------------------------------------------------
export const endpoints = {
chat: '/api/chat',
kanban: '/api/kanban',
calendar: '/api/calendar',
auth: {
me: '/api/auth/me',
signIn: '/api/auth/sign-in',
signUp: '/api/auth/sign-up',
},
mail: {
list: '/api/mail/list',
details: '/api/mail/details',
labels: '/api/mail/labels',
},
post: {
list: '/api/post/list',
details: '/api/post/details',
latest: '/api/post/latest',
search: '/api/post/search',
},
product: {
list: '/api/product/list',
details: '/api/product/details',
search: '/api/product/search',
save: '/api/product/saveProduct',
create: '/api/product/create',
update: '/api/product/update',
delete: '/api/product/delete',
},
user: {
list: '/api/user/list',
profile: '/api/user/profile',
update: '/api/user/update',
settings: '/api/user/settings',
details: '/api/user/details',
},
order: {
list: '/api/order/list',
profile: '/api/order/profile',
update: '/api/order/update',
settings: '/api/order/settings',
details: '/api/order/details',
changeStatus: (orderId: string) => `/api/order/changeStatus?orderId=${orderId}`,
},
invoice: {
list: '/api/invoice/list',
profile: '/api/invoice/profile',
update: '/api/invoice/update',
saveInvoice: (invoiceId: string) => `/api/invoice/saveInvoice?invoiceId=${invoiceId}`,
settings: '/api/invoice/settings',
details: '/api/invoice/details',
changeStatus: (invoiceId: string) => `/api/invoice/changeStatus?invoiceId=${invoiceId}`,
search: '/api/invoice/search',
},
//
//
//
partyEvent: {
list: '/api/party-event/list',
details: '/api/party-event/details',
search: '/api/party-event/search',
create: '/api/party-event/create',
update: '/api/party-event/update',
delete: '/api/party-event/delete',
numOfEvent: '/api/party-event/numOfEvent',
},
partyOrder: {
create: '/api/party-order/create',
delete: '/api/party-order/delete',
list: '/api/party-order/list',
profile: '/api/party-order/profile',
update: '/api/party-order/update',
settings: '/api/party-order/settings',
details: '/api/party-order/details',
changeStatus: (partyOrderId: string) =>
`/api/party-order/changeStatus?partyOrderId=${partyOrderId}`,
},
partyUser: {
list: '/api/party-user/list',
details: '/api/party-user/details',
search: '/api/party-user/search',
create: '/api/party-user/create',
update: '/api/party-user/update',
delete: '/api/party-user/delete',
//
detailsByPartyUserId: (partyUserId: string) =>
`/api/party-user/details?partyUserId=${partyUserId}`,
},
partyUserAuth: {
me: '/api/party-user-auth/me',
signIn: '/api/party-user-auth/sign-in',
signUp: '/api/party-user-auth/sign-up',
},
};

View File

@@ -22,7 +22,7 @@ i18next
.init({
...i18nOptions(lng),
detection: { caches: ['localStorage'] },
debug: isDev,
debug: false,
});
// ----------------------------------------------------------------------

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { AccountBillingView } from 'src/sections/account/view';
// ----------------------------------------------------------------------
const metadata = { title: `Account billing settings | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AccountBillingView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { AccountChangePasswordView } from 'src/sections/account/view';
// ----------------------------------------------------------------------
const metadata = { title: `Account change password settings | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AccountChangePasswordView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { AccountGeneralView } from 'src/sections/account/view';
// ----------------------------------------------------------------------
const metadata = { title: `Account general settings | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AccountGeneralView />
</>
);
}

View File

@@ -0,0 +1,18 @@
import { CONFIG } from 'src/global-config';
import { AccountNotificationsView } from 'src/sections/account/view';
// ----------------------------------------------------------------------
const metadata = {
title: `Account notifications settings | Dashboard - ${CONFIG.appName}`,
};
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AccountNotificationsView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { AccountSocialsView } from 'src/sections/account/view';
// ----------------------------------------------------------------------
const metadata = { title: `Account socials settings | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AccountSocialsView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { UserCardsView } from 'src/sections/user/view';
// ----------------------------------------------------------------------
const metadata = { title: `User cards | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<UserCardsView />
</>
);
}

View File

@@ -0,0 +1,25 @@
// import { _userList } from 'src/_mock/_user';
import { useGetPartyUser } from 'src/actions/party-user';
import { CONFIG } from 'src/global-config';
import { useParams } from 'src/routes/hooks';
import { PartyUserEditView } from 'src/sections/party-user/view';
// ----------------------------------------------------------------------
const metadata = { title: `User edit | Dashboard - ${CONFIG.appName}` };
export default function Page() {
const { id = '' } = useParams();
// TODO: remove unused code
// const currentUser = _userList.find((user) => user.id === id);
const { partyUser: user } = useGetPartyUser(id);
return (
<>
<title>{metadata.title}</title>
<PartyUserEditView user={user} />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { PartyUserListView } from 'src/sections/party-user/view';
// ----------------------------------------------------------------------
const metadata = { title: `User list | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<PartyUserListView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { UserCreateView } from 'src/sections/party-user/view';
// ----------------------------------------------------------------------
const metadata = { title: `Create a new user | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<UserCreateView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { CONFIG } from 'src/global-config';
import { UserProfileView } from 'src/sections/user/view';
// ----------------------------------------------------------------------
const metadata = { title: `User profile | Dashboard - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<UserProfileView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { AmplifyResetPasswordView } from 'src/auth/view/amplify';
import { CONFIG } from 'src/global-config';
// ----------------------------------------------------------------------
const metadata = { title: `Reset password | Amplify - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AmplifyResetPasswordView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { AmplifySignInView } from 'src/auth/view/amplify';
import { CONFIG } from 'src/global-config';
// ----------------------------------------------------------------------
const metadata = { title: `Sign in | Amplify - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AmplifySignInView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { AmplifySignUpView } from 'src/auth/view/amplify';
import { CONFIG } from 'src/global-config';
// ----------------------------------------------------------------------
const metadata = { title: `Sign up | Amplify - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AmplifySignUpView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { AmplifyUpdatePasswordView } from 'src/auth/view/amplify';
import { CONFIG } from 'src/global-config';
// ----------------------------------------------------------------------
const metadata = { title: `Update password | Amplify - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AmplifyUpdatePasswordView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { AmplifyVerifyView } from 'src/auth/view/amplify';
import { CONFIG } from 'src/global-config';
// ----------------------------------------------------------------------
const metadata = { title: `Verify | Amplify - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<AmplifyVerifyView />
</>
);
}

View File

@@ -0,0 +1,7 @@
import { SplashScreen } from 'src/components/loading-screen';
// ----------------------------------------------------------------------
export default function CallbackPage() {
return <SplashScreen />;
}

View File

@@ -0,0 +1,16 @@
import { Auth0SignInView } from 'src/auth/view/auth0';
import { CONFIG } from 'src/global-config';
// ----------------------------------------------------------------------
const metadata = { title: `Sign in | Auth0 - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<Auth0SignInView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { FirebaseResetPasswordView } from 'src/auth/view/firebase';
import { CONFIG } from 'src/global-config';
// ----------------------------------------------------------------------
const metadata = { title: `Reset password | Firebase - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<FirebaseResetPasswordView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { FirebaseSignInView } from 'src/auth/view/firebase';
import { CONFIG } from 'src/global-config';
// ----------------------------------------------------------------------
const metadata = { title: `Sign in | Firebase - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<FirebaseSignInView />
</>
);
}

View File

@@ -0,0 +1,16 @@
import { FirebaseSignUpView } from 'src/auth/view/firebase';
import { CONFIG } from 'src/global-config';
// ----------------------------------------------------------------------
const metadata = { title: `Sign up | Firebase - ${CONFIG.appName}` };
export default function Page() {
return (
<>
<title>{metadata.title}</title>
<FirebaseSignUpView />
</>
);
}

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