Compare commits

...

5 Commits

19 changed files with 278 additions and 105 deletions

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

@@ -1,5 +1,14 @@
import type { User } from '@prisma/client';
// 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
//
import type { NextRequest } from 'next/server';
import type { PartyUser } from '@prisma/client';
import { headers } from 'next/headers';
@@ -7,15 +16,13 @@ import { verify } from 'src/utils/jwt';
import { STATUS, response, handleError } from 'src/utils/response';
import { JWT_SECRET } from 'src/_mock/_auth';
import { getUserById } from 'src/app/services/user.service';
import { createAccessLog } from 'src/app/services/access-log.service';
import { getPartyUserById } from 'src/app/services/party-user.service';
import { flattenNextjsRequest } from '../sign-in/flattenNextjsRequest';
// ----------------------------------------------------------------------
// export const runtime = 'edge';
/**
* This API is used for demo purpose only
* You should use a real database
@@ -24,11 +31,12 @@ import { flattenNextjsRequest } from '../sign-in/flattenNextjsRequest';
* You should not expose the JWT_SECRET in the client side
*/
const USER_TOKEN_CHECK_FAILED = 'user token check failed';
const INVALID_AUTH_TOKEN = 'Invalid authorization token';
const USER_ID_NOT_FOUND = 'userId not found';
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';
const AUTHORIZATION_TOKEN_MISSING_OR_INVALID = 'Authorization token missing or invalid';
export async function GET(req: NextRequest) {
const debug = { 'req.headers': flattenNextjsRequest(req) };
@@ -38,29 +46,29 @@ export async function GET(req: NextRequest) {
const authorization = headersList.get('authorization');
if (!authorization || !authorization.startsWith('Bearer ')) {
return response({ message: AUTHORIZATION_TOKEN_MISSING_OR_INVALID }, STATUS.UNAUTHORIZED);
return response({ message: ERR_AUTHORIZATION_TOKEN_MISSING_OR_INVALID }, STATUS.UNAUTHORIZED);
}
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 currentUser: User | null = await getUserById(data.userId);
const currentUser: PartyUser | null = await getPartyUserById(data.userId);
if (!currentUser) {
createAccessLog('', USER_TOKEN_CHECK_FAILED, debug);
createAccessLog('', ERR_USER_TOKEN_CHECK_FAILED, debug);
return response({ message: INVALID_AUTH_TOKEN }, STATUS.UNAUTHORIZED);
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: USER_ID_NOT_FOUND }, STATUS.ERROR);
return response({ message: ERR_USER_ID_NOT_FOUND }, STATUS.ERROR);
}
} catch (error) {
return handleError('[Auth] - Me', error);

View File

@@ -1,22 +1,35 @@
// 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/auth/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWJnbnUyengwMDBjaHEzaGZ3dmtjejlvIiwiaWF0IjoxNzQ4OTY0ODkyLCJleHAiOjE3NTAxNzQ0OTJ9.lo04laCxtm0IVeYaETEV3hXKyDmXPEn7SyWtY2VR4dI
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/auth/sign-in
POST http://localhost:7272/api/party-user-auth/sign-in
content-type: application/json
{
"email": "demo@minimals1.cc",
"email": "demo@minimals.cc",
"password": "@2Minimal"
}
###
# Wrong password
POST http://localhost:7272/api/auth/sign-in
POST http://localhost:7272/api/party-user-auth/sign-in
content-type: application/json
{

View File

@@ -1,14 +1,18 @@
// src/app/services/user.service.ts
// src/app/services/party-user.service.ts
//
// PURPOSE:
// - Handle User Record CRUD operations
// - 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 input data before processing
// - Validate all party user data before processing
// - Enforce party-specific business rules
// - Maintain audit trail for sensitive operations
//
import type { User, PartyUser } from '@prisma/client';
import type { PartyUser } from '@prisma/client';
import prisma from '../lib/prisma';
@@ -47,8 +51,8 @@ async function getPartyUserByEmail(email: string): Promise<PartyUser | null> {
});
}
async function getUserById(id: string): Promise<User | null> {
return prisma.user.findFirst({ where: { id } });
async function getPartyUserById(id: string): Promise<PartyUser | null> {
return prisma.partyUser.findFirst({ where: { id } });
}
async function createPartyUser(partyUserData: any): Promise<PartyUser> {
@@ -69,7 +73,7 @@ async function deletePartyUser(partyUserId: string): Promise<PartyUser | null> {
}
export {
getUserById,
getPartyUserById,
getPartyUser,
listPartyUsers,
createPartyUser,

View File

@@ -66,7 +66,8 @@ import PrivacyAgreement from './pages/PrivacyAgreement';
import EventDetail from './pages/EventDetail';
import MemberProfile from './pages/MemberProfile';
import OrderDetail from './pages/OrderDetail';
import DummyPayPage from './pages/DummyPayPage';
import DummyPayPage from './pages/DummyEventPayPage';
import DummyEventPayPage from './pages/DummyEventPayPage';
setupIonicReact();
@@ -142,6 +143,8 @@ const IonicApp: React.FC<IonicAppProps> = ({ darkMode, schedule, setIsLoggedIn,
<Route exact={true} path="/helloworld" component={Helloworld} />
<Route exact={true} path={PATHS.DUMMY_EVENT_PAY_PAGE} component={DummyEventPayPage} />
<Route
path="/logout"
render={() => {

View File

@@ -31,6 +31,8 @@ const PATHS = {
PARTY_USER_SIGN_IN: '/partyUserlogin',
PARTY_USER_SIGN_UP: '/partyUserSignUp',
DUMMY_EVENT_PAY_PAGE: '/DummyEventPayPage',
//
TABS_DEBUG: '/tabs/debug',

View File

@@ -11,6 +11,7 @@ const constants = {
// Used to construct all API request URLs
API_ENDPOINT,
SIGN_IN: `${API_ENDPOINT}/api/party-user-auth/sign-in`,
PARTY_USER_JOIN_EVENT: `${API_ENDPOINT}/api/event/partyUserJoinEvent`,
};
if (!constants.API_ENDPOINT) {

View File

@@ -0,0 +1,14 @@
// import { setIsLoggedInData } from '../dataApi';
import { ActionType } from '../../util/types';
export const setEventIdToJoin = (eventId: string) => {
// await setIsLoggedInData(loggedIn);
return {
type: 'set-dummy-event-id-to-join',
eventId,
} as const;
};
export type DummyActions = ActionType<typeof setEventIdToJoin>;
// | ActionType<typeof checkUserSession>

View File

@@ -0,0 +1,13 @@
import { DummyActions as DummyActions } from './dummy.actions';
import { DummyState as DummyState } from './dummy.state';
export function dummyReducer(state: DummyState, action: DummyActions): DummyState {
switch (action.type) {
case 'set-dummy-event-id-to-join':
console.log('reducer called');
return { ...state, eventIdToJoin: action.eventId };
default:
return { ...state };
}
}

View File

@@ -0,0 +1,3 @@
export interface DummyState {
eventIdToJoin?: string;
}

View File

@@ -211,3 +211,5 @@ export const mapCenter = (state: AppState) => {
export const getPartyUserUsername = (state: AppState) => state.user.username;
export const getPartyUserState = (state: AppState) => state.user;
export const getEventIdToJoin = (state: AppState) => state.dummy.eventIdToJoin;

View File

@@ -15,6 +15,7 @@ import { locationsReducer } from './locations/locations.reducer';
// Additional feature reducers
import { orderReducer } from './sessions/orders.reducer';
import { dummyReducer } from './dummy/dummy.reducer';
export const initialState: AppState = {
data: {
@@ -40,10 +41,14 @@ export const initialState: AppState = {
loading: false,
//
isSessionValid: false,
//
},
locations: {
locations: [],
},
dummy: {
eventIdToJoin: '',
},
};
export const reducers = combineReducers({
@@ -52,6 +57,7 @@ export const reducers = combineReducers({
locations: locationsReducer,
//
order: orderReducer,
dummy: dummyReducer,
});
export type AppState = ReturnType<typeof reducers>;

View File

@@ -0,0 +1,131 @@
// REQ0041/home_discover_event_tab
import {
IonPage,
IonHeader,
IonToolbar,
IonButtons,
IonButton,
IonIcon,
IonTitle,
IonContent,
useIonRouter,
IonToast,
} from '@ionic/react';
import { chevronBackOutline, menuOutline } from 'ionicons/icons';
import React, { useEffect, useRef, useState } from 'react';
import './style.scss';
import PATHS from '../../PATHS';
import axios from 'axios';
import { UserState } from '../../data/user/user.state';
import { connect } from '../../data/connect';
import * as selectors from '../../data/selectors';
import constants from '../../constants';
interface OwnProps {}
interface StateProps {
isLoggedin: boolean;
//
partyUserState: UserState;
//
joinEventId: string;
}
interface DispatchProps {}
interface PageProps extends OwnProps, StateProps, DispatchProps {}
const DummyPayPage: React.FC<PageProps> = ({
isLoggedin,
partyUserState,
//
joinEventId,
}) => {
const router = useIonRouter();
// if (!isLoggedin) return <NotLoggedIn />;
async function handlePayClick() {
try {
await axios.post(constants.PARTY_USER_JOIN_EVENT, {
data: {
eventItemId: joinEventId,
email: partyUserState.meta?.email,
},
});
router.goBack();
setShowJoinOKToast(true);
} catch (error) {
console.error(error);
}
}
function handleCancelClick() {
router.goBack();
}
const [showJoinOKToast, setShowJoinOKToast] = useState(false);
return (
<IonPage id="speaker-list">
<IonHeader translucent={true} className="ion-no-border">
<IonToolbar>
<IonButtons slot="start">
{/* <IonMenuButton /> */}
<IonButton shape="round">
<IonIcon slot="icon-only" icon={chevronBackOutline}></IonIcon>
</IonButton>
</IonButtons>
<IonTitle>Dummy pay event page</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen={true}>
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '1rem',
textAlign: 'center',
padding: '3rem',
}}
>
<div>This is a dummy page to emulate payment gateway work</div>
<div>
<div>pay for event</div>
<pre style={{ backgroundColor: 'RGB(0,0,0, 0.1)' }}>{JSON.stringify(joinEventId)}</pre>
</div>
<IonButton onClick={handlePayClick}>Pay</IonButton>
<IonButton onClick={handleCancelClick}>Cancel</IonButton>
</div>
<IonToast
isOpen={showJoinOKToast}
message="ok, event paid, thank you..."
duration={2000}
// onDidDismiss={() => setShowJoinOKToast(false)}
/>
</IonContent>
</IonPage>
);
};
export default connect<OwnProps, StateProps, DispatchProps>({
mapStateToProps: (state) => ({
isLoggedin: state.user.isLoggedin,
//
joinEventId: selectors.getEventIdToJoin(state),
//
partyUserState: selectors.getPartyUserState(state),
}),
component: DummyPayPage,
});

View File

@@ -1,37 +0,0 @@
// REQ0041/home_discover_event_tab
import {
IonPage,
IonHeader,
IonToolbar,
IonButtons,
IonButton,
IonIcon,
IonTitle,
IonContent,
} from '@ionic/react';
import { menuOutline } from 'ionicons/icons';
import React, { useEffect, useRef, useState } from 'react';
import './style.scss';
const DummyPayPage: React.FC = () => {
return (
<IonPage id="speaker-list">
<IonHeader translucent={true} className="ion-no-border">
<IonToolbar>
<IonButtons slot="end">
{/* <IonMenuButton /> */}
<IonButton shape="round" id="events-open-modal" expand="block">
<IonIcon slot="icon-only" icon={menuOutline}></IonIcon>
</IonButton>
</IonButtons>
<IonTitle>Discover Events</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen={true}>DummyPayPage</IonContent>
</IonPage>
);
};
export default DummyPayPage;

View File

@@ -13,48 +13,27 @@ import {
IonContent,
IonPage,
IonButtons,
IonMenuButton,
IonButton,
IonIcon,
IonDatetime,
IonSelectOption,
IonList,
IonItem,
IonLabel,
IonSelect,
IonPopover,
IonText,
IonFooter,
useIonRouter,
IonAvatar,
} from '@ionic/react';
import './style.scss';
import {
accessibility,
accessibilityOutline,
chevronBackOutline,
ellipsisHorizontal,
ellipsisVertical,
heart,
locationOutline,
locationSharp,
logoIonic,
man,
manOutline,
people,
peopleOutline,
timer,
timerOutline,
timerSharp,
wallet,
walletOutline,
walletSharp,
woman,
womanOutline,
} from 'ionicons/icons';
import AboutPopover from '../../components/AboutPopover';
import { format, parseISO } from 'date-fns';
import { TestContent } from './TestContent';
import { Helloworld } from '../../api/Helloworld';
import { getEventById } from '../../api/getEventById';
import { connect } from '../../data/connect';
@@ -62,6 +41,9 @@ import * as selectors from '../../data/selectors';
import { Event } from '../../models/Event';
import { RouteComponentProps } from 'react-router';
import AvatarRow from './AvatarRow';
import { setPartyUserMeta } from '../../data/user/user.actions';
import { setEventIdToJoin } from '../../data/dummy/dummy.actions';
import PATHS from '../../PATHS';
const leftShift: number = -25;
@@ -71,7 +53,10 @@ interface OwnProps extends RouteComponentProps {
interface StateProps {}
interface DispatchProps {}
interface DispatchProps {
setPartyUserMeta: typeof setPartyUserMeta;
setEventIdToJoin: typeof setEventIdToJoin;
}
interface PageProps extends OwnProps, StateProps, DispatchProps {}
@@ -90,7 +75,7 @@ const showJoinedMembers = (joinMembers: Record<string, any>[]) => {
);
};
const EventDetail: React.FC<PageProps> = ({ event_detail }) => {
const EventDetail: React.FC<PageProps> = ({ event_detail, setEventIdToJoin }) => {
const router = useIonRouter();
const [showPopover, setShowPopover] = useState(false);
@@ -136,6 +121,13 @@ const EventDetail: React.FC<PageProps> = ({ event_detail }) => {
router.goBack();
}
function handleJoinClick() {
if (event_detail && event_detail?.id) {
setEventIdToJoin(event_detail.id);
router.push(PATHS.DUMMY_EVENT_PAY_PAGE);
}
}
if (!event_detail) return <>loading</>;
return (
@@ -268,7 +260,7 @@ const EventDetail: React.FC<PageProps> = ({ event_detail }) => {
margin: '1rem',
}}
>
<IonButton expand="full" shape="round">
<IonButton expand="full" shape="round" onClick={handleJoinClick}>
Join
</IonButton>
</div>
@@ -286,8 +278,10 @@ const EventDetail: React.FC<PageProps> = ({ event_detail }) => {
};
export default connect({
mapDispatchToProps: {
setEventIdToJoin,
},
mapStateToProps: (state, ownProps) => {
console.log({ t1: selectors.getEvents(state) });
return {
event_detail: selectors.getEvent(state, ownProps),
};

View File

@@ -14,7 +14,7 @@ import { menuOutline } from 'ionicons/icons';
import React, { useEffect, useRef, useState } from 'react';
import './style.scss';
const Helloworld: React.FC = () => {
const Helloworld: React.FC = ({}) => {
return (
<IonPage id="speaker-list">
<IonHeader translucent={true} className="ion-no-border">

View File

@@ -29,3 +29,6 @@ A: No, AI should not. Most of the time the user passing the update job. Unless u
Q: What should I do when user ask to update git staged files ?
A: You only need to update the comment with same format and detail levels of the staged files. Do not change any other code. The sibling files in the same directory is a good reference.
Q: which command should i use for check git stage ?
A: `git status . ` to avoid hanging in the terminal

View File

@@ -22,5 +22,17 @@
"path": "98_AI_workspace"
}
],
"settings": {}
}
"settings": {
"editor.fontSize": 15
},
"extensions": {
"recommendations": [
"streetsidesoftware.code-spell-checker",
"onatm.open-in-new-window",
"Prisma.prisma",
"humao.rest-client",
"Gruntfuggly.todo-tree",
"esbenp.prettier-vscode"
]
}
}