"feat: refactor API handlers to use services, add logging, and improve security checks"

This commit is contained in:
louiscklaw
2025-06-04 02:35:05 +08:00
parent ef0c0ab389
commit 99239c32a5
20 changed files with 303 additions and 42 deletions

View File

@@ -25,6 +25,7 @@ export async function POST(req: NextRequest) {
const { data } = await req.json();
try {
// TODO: obsolete createNewAppLog
const createResult = await createNewAppLog(data);
return response(createResult, STATUS.OK);

View File

@@ -1,13 +1,20 @@
import type { User } from '@prisma/client';
import type { NextRequest } from 'next/server';
import { headers } from 'next/headers';
import { verify } from 'src/utils/jwt';
import { STATUS, response, handleError } from 'src/utils/response';
import { _users, JWT_SECRET } from 'src/_mock/_auth';
import { JWT_SECRET } from 'src/_mock/_auth';
import { getUserById } from 'src/app/services/user.service';
import { createAccessLog } from 'src/app/services/AccessLog.service';
import { flattenNextjsRequest } from '../sign-in/flattenNextjsRequest';
// ----------------------------------------------------------------------
export const runtime = 'edge';
// export const runtime = 'edge';
/**
* This API is used for demo purpose only
@@ -17,25 +24,44 @@ export const runtime = 'edge';
* You should not expose the JWT_SECRET in the client side
*/
export async function GET() {
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 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) };
try {
const headersList = headers();
const authorization = headersList.get('authorization');
if (!authorization || !authorization.startsWith('Bearer ')) {
return response({ message: 'Authorization token missing or invalid' }, STATUS.UNAUTHORIZED);
return response({ message: AUTHORIZATION_TOKEN_MISSING_OR_INVALID }, STATUS.UNAUTHORIZED);
}
const accessToken = `${authorization}`.split(' ')[1];
const data = await verify(accessToken, JWT_SECRET);
console.log(data.userId);
const currentUser = _users.find((user) => user.id === 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);
if (!currentUser) {
return response({ message: 'Invalid authorization token' }, STATUS.UNAUTHORIZED);
if (!currentUser) {
createAccessLog('', USER_TOKEN_CHECK_FAILED, debug);
return response({ message: 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({ user: currentUser }, 200);
} catch (error) {
return handleError('[Auth] - Me', error);
}

View File

@@ -0,0 +1,25 @@
###
# username and password ok
GET http://localhost:7272/api/auth/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbWJnbnUyengwMDBjaHEzaGZ3dmtjejlvIiwiaWF0IjoxNzQ4OTY0ODkyLCJleHAiOjE3NTAxNzQ0OTJ9.lo04laCxtm0IVeYaETEV3hXKyDmXPEn7SyWtY2VR4dI
###
# There is no user corresponding to the email address.
POST http://localhost:7272/api/auth/sign-in
content-type: application/json
{
"email": "demo@minimals1.cc",
"password": "@2Minimal"
}
###
# Wrong password
POST http://localhost:7272/api/auth/sign-in
content-type: application/json
{
"email": "demo@minimals.cc",
"password": "@2Min111imal"
}

View File

@@ -0,0 +1,5 @@
import type { NextRequest } from 'next/server';
export function flattenNextjsRequest(req: NextRequest) {
return Object.fromEntries(req.headers.entries());
}

View File

@@ -7,6 +7,7 @@ import { JWT_SECRET, JWT_EXPIRES_IN } from 'src/_mock/_auth';
import { createAccessLog } from 'src/app/services/AccessLog.service';
import prisma from '../../../lib/prisma';
import { flattenNextjsRequest } from './flattenNextjsRequest';
// ----------------------------------------------------------------------
@@ -22,7 +23,7 @@ const ERR_USER_NOT_FOUND = 'There is no user corresponding to the email address.
const ERR_WRONG_PASSWORD = 'Wrong password';
export async function POST(req: NextRequest) {
const debug = { 'req.headers': Object.fromEntries(req.headers.entries()) };
const debug = { 'req.headers': flattenNextjsRequest(req) };
try {
const { email, password } = await req.json();

View File

@@ -0,0 +1,25 @@
# GUIDELINE
## Event / event
- this is a `event` api endpoint
- this is a demo to handle `event` record
- use single file for single db table/collection only
## `route.ts`
handle `GET`, `POST`, `PUT`, `DELETE`
## `test.http`
store test request
## `../../services/event.service.ts`
event schema CRUD handler
`listEvents` - list `event` record
`getEvent` - get `event` record by id
`createNewEvent` - create `event` record
`updateEvent` - update `event` record by id
`deleteEvent` - delete `event` record by id

View File

@@ -8,10 +8,11 @@
import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
import { L_INFO, L_ERROR } from 'src/constants';
import { getEvent } from 'src/app/services/eventItem.service';
import { createAppLog } from 'src/app/services/AppLog.service';
// ----------------------------------------------------------------------
@@ -19,29 +20,32 @@ import prisma from '../../../lib/prisma';
* GET Event detail
*************************************** */
export async function GET(req: NextRequest) {
try {
const { searchParams } = req.nextUrl;
const debug = { 'req.headers': Object.fromEntries(req.headers.entries()) };
// RULES: eventId must exist
const eventId = searchParams.get('eventId');
const { searchParams } = req.nextUrl;
// RULES: for the incoming request, the `eventId` must exist
const eventId = searchParams.get('eventId');
try {
if (!eventId) {
return response({ message: 'Event ID is required!' }, STATUS.BAD_REQUEST);
}
// NOTE: eventId confirmed exist, run below
const event = await prisma.eventItem.findFirst({
include: { reviews: true },
where: { id: eventId },
});
// NOTE: `eventId` confirmed exist, run below
const event = await getEvent(eventId);
console.log({ event });
// RULES: show error if not found
if (!event) {
return response({ message: 'Event not found!' }, STATUS.NOT_FOUND);
}
logger('[Event] details', event.id);
// logger('[Event] details', event.id);
await createAppLog(L_INFO, 'get event detail ok', { eventId });
return response({ event }, STATUS.OK);
} catch (error) {
await createAppLog(L_ERROR, 'error during getting event detail', { debug, eventId });
return handleError('Event - Get details', error);
}
}

View File

@@ -1,3 +1,5 @@
###
GET http://localhost:7272/api/event/details?eventId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01
###
GET http://localhost:7272/api/event/details

View File

@@ -1,17 +1,17 @@
// src/app/api/event/list/route.ts
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// src/app/api/event/list/route.ts
import { listEvents } from 'src/app/services/eventItem.service';
// ----------------------------------------------------------------------
/** **************************************
* GET - Events
* GET - Events, obsoleted
*************************************** */
export async function GET() {
try {
const events = await prisma.eventItem.findMany();
const events = await listEvents();
logger('[Event] list', events.length);

View File

@@ -10,7 +10,11 @@ import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import { L_INFO, L_ERROR } from 'src/constants';
import { createAppLog } from 'src/app/services/AppLog.service';
import prisma from '../../../lib/prisma';
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
// ----------------------------------------------------------------------
@@ -21,6 +25,8 @@ import prisma from '../../../lib/prisma';
*/
export async function GET(req: NextRequest) {
// Original user details functionality
const debug = { 'req.headers': flattenNextjsRequest(req) };
try {
const { searchParams } = req.nextUrl;
@@ -32,8 +38,12 @@ export async function GET(req: NextRequest) {
if (!helloworld) return response({ message: 'User not found!' }, STATUS.NOT_FOUND);
createAppLog(L_INFO, 'Get OK', debug);
return response({ helloworld }, STATUS.OK);
} catch (error) {
createAppLog(L_ERROR, 'Get error', debug);
return handleError('Product - Get details', error);
}
}

View File

@@ -0,0 +1,3 @@
# GUIDELINES
T.B.A.

View File

@@ -1,7 +1,7 @@
// src/app/api/product/details/route.ts
//
// PURPOSE:
// save product to db by id
// get product from db by id
//
// RULES:
// T.B.A.
@@ -11,14 +11,22 @@ import type { NextRequest } from 'next/server';
import { logger } from 'src/utils/logger';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
import { L_INFO, L_ERROR } from 'src/constants';
import { getProduct } from 'src/app/services/product.service';
import { createAppLog } from 'src/app/services/AppLog.service';
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
// ----------------------------------------------------------------------
/** **************************************
/**
**************************************
* GET Product detail
*************************************** */
***************************************
*/
export async function GET(req: NextRequest) {
const debug = { 'req.headers': flattenNextjsRequest(req) };
try {
const { searchParams } = req.nextUrl;
@@ -29,10 +37,7 @@ export async function GET(req: NextRequest) {
}
// NOTE: productId confirmed exist, run below
const product = await prisma.productItem.findFirst({
include: { reviews: true },
where: { id: productId },
});
const product = await getProduct(productId);
if (!product) {
return response({ message: 'Product not found!' }, STATUS.NOT_FOUND);
@@ -40,8 +45,12 @@ export async function GET(req: NextRequest) {
logger('[Product] details', product.id);
createAppLog(L_INFO, 'Get product detail OK', debug);
return response({ product }, STATUS.OK);
} catch (error) {
createAppLog(L_ERROR, 'product detail error', debug);
return handleError('Product - Get details', error);
}
}

View File

@@ -0,0 +1,3 @@
###
GET http://localhost:7272/api/product/details?productId=e99f09a7-dd88-49d5-b1c8-1daf80c2d7b01

View File

@@ -1,22 +1,41 @@
// src/app/api/product/list/route.ts
import { logger } from 'src/utils/logger';
//
// PURPOSE:
// save product to db by id
//
// RULES:
// T.B.A.
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
import { L_INFO, L_ERROR } from 'src/constants';
import { createAppLog } from 'src/app/services/AppLog.service';
import { listProducts } from 'src/app/services/product.service';
import { flattenNextjsRequest } from '../../auth/sign-in/flattenNextjsRequest';
// ----------------------------------------------------------------------
/** **************************************
* GET - Products
* GET - Products list
*************************************** */
export async function GET() {
try {
const products = await prisma.productItem.findMany();
export async function GET(req: NextRequest) {
const debug = { 'req.headers': flattenNextjsRequest(req) };
logger('[Product] list', products.length);
try {
// const products = await prisma.productItem.findMany();
const products = await listProducts();
// logger('[Product] list', products.length);
createAppLog(L_INFO, 'product list ok', {});
return response({ products }, STATUS.OK);
} catch (error) {
createAppLog(L_ERROR, 'product list error', debug);
return handleError('Product - Get list', error);
}
}

View File

@@ -0,0 +1,3 @@
###
GET http://localhost:7272/api/product/list

View File

@@ -0,0 +1,23 @@
# GUIDELINE
- this is a helloworld api endpoint
- this is a demo to handle helloworld record
- use single file for single db table/collection only
## `route.ts`
handle `GET`, `POST`, `PUT`, `DELETE`
## `test.http`
store test request
## `../../services/helloworld.service.ts`
helloworld schema CRUD handler
`listHelloworlds` - list helloworld record
`getHelloworld` - get helloworld record by id
`createNewHelloworld` - create helloworld record
`updateHelloworld` - update helloworld record by id
`deleteHelloworld` - delete helloworld record by id

View File

@@ -0,0 +1,39 @@
// src/app/api/helloworld/detail/route.ts
//
// PURPOSE:
// Get single helloworld record detail
//
// RULES:
// - For helloworld requests, return simple response
//
import type { NextRequest } from 'next/server';
import { STATUS, response, handleError } from 'src/utils/response';
import prisma from '../../../lib/prisma';
// ----------------------------------------------------------------------
/**
**************************************
* GET - Handle both helloworld and user details
**************************************
*/
export async function GET(req: NextRequest) {
// Original user details functionality
try {
const { searchParams } = req.nextUrl;
// RULES: helloworldId must exist
const helloworldId = searchParams.get('helloworldId');
if (!helloworldId) return response({ message: 'helloworldId is required!' }, STATUS.BAD_REQUEST);
const helloworld = await prisma.userItem.findFirst({ where: { id: helloworldId } });
if (!helloworld) return response({ message: 'User not found!' }, STATUS.NOT_FOUND);
return response({ helloworld }, STATUS.OK);
} catch (error) {
return handleError('Product - Get details', error);
}
}

View File

@@ -0,0 +1,4 @@
###
GET http://localhost:7272/api/helloworld/details?helloworldId=1165ce3a-29b8-4e1a-9148-1ae08d7e8e01

View File

@@ -0,0 +1,34 @@
import prisma from '@/lib/prisma';
import { NextResponse } from 'next/server';
// GET: 获取所有学生
export async function GET() {
try {
const students = await prisma.student.findMany();
return NextResponse.json(students);
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch students' }, { status: 500 });
}
}
// POST: 创建新学生
export async function POST(request: Request) {
try {
const { email, metadata } = await request.json();
if (!email) {
return NextResponse.json({ error: 'Email is required' }, { status: 400 });
}
const student = await prisma.student.create({
data: {
email,
metadata: metadata || {},
},
});
return NextResponse.json(student, { status: 201 });
} catch (error) {
return NextResponse.json({ error: 'Failed to create student' }, { status: 500 });
}
}

View File

@@ -0,0 +1,25 @@
###
GET http://localhost:7272/api/helloworld
###
GET http://localhost:7272/api/helloworld?helloworldId=1
###
POST http://localhost:7272/api/helloworld?helloworldId=1
content-type: application/json
{
"data":{"hello": "hell"}
}
###
PUT http://localhost:7272/api/helloworld?helloworldId=1
content-type: application/json
{
"data": {"hello": "hell"}
}
###
DELETE http://localhost:7272/api/helloworld?helloworldId=1