chore: move backend to hono + typescript (#558)
* feat: setup secure presigned uploads and downloads * clean up * update lock * Get presigned url finally working * 557: Migrate backend to hono + ts * chore: update Dockerfile and package.json for backend configuration * add dev command * remove unused packages * Enhance backend API and frontend components for course management - Updated CORS configuration in the backend to allow specific methods and headers. - Refactored lesson download endpoint to use a new route for PDF downloads. - Removed unused certificate download functionality from the lesson router. - Updated frontend components to reflect changes in API endpoints and improve video upload functionality. - Introduced new video upload component and adjusted video handling in lesson materials. - Cleaned up unnecessary imports and optimized component structure for better performance. * PR cleanups * Enhance email configuration by adding SMTP_SENDER environment variable - Updated docker-compose.yaml to include SMTP_SENDER. - Modified Dockerfile to accept SMTP_SENDER as an argument. - Updated environment schema in env.ts to define SMTP_SENDER as optional. - Adjusted mail service to use SMTP_SENDER for the email 'from' field. - Updated documentation to reflect the new SMTP_SENDER variable. * Remove debug log statement from sendEmail function --------- Co-authored-by: Iniubong Obonguko <iniubongobonguko2018@gmail.com>
This commit is contained in:
co-authored by
Iniubong Obonguko
parent
9eb7b01587
commit
82be557a42
@@ -0,0 +1,91 @@
|
||||
import { Context, Hono } from 'hono';
|
||||
import { ZCertificateDownload, ZCourseDownloadContent } from '$src/types/course';
|
||||
|
||||
import { generateCertificate } from '$src/utils/certificate';
|
||||
import { generateCoursePdf } from '$src/utils/course';
|
||||
import { katexRouter } from '$src/routes/course/katex';
|
||||
import { lessonRouter } from '$src/routes/course/lesson';
|
||||
import { presignRouter } from '$src/routes/course/presign';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const courseRouter = new Hono();
|
||||
|
||||
courseRouter.route('/katex', katexRouter);
|
||||
courseRouter.route('/lesson', lessonRouter);
|
||||
courseRouter.route('/presign', presignRouter);
|
||||
|
||||
courseRouter.post('/download/certificate', async (c: Context) => {
|
||||
try {
|
||||
const data = await c.req.json();
|
||||
|
||||
const validatedData = ZCertificateDownload.parse(data);
|
||||
|
||||
const result = await generateCertificate(validatedData);
|
||||
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
c.header(
|
||||
'Content-Disposition',
|
||||
`attachment; filename="certificate-${validatedData.courseName}.pdf"`
|
||||
);
|
||||
|
||||
return c.body(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(result);
|
||||
controller.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: error.errors
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
console.error('Error generating certificate:', error);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
details: error
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
courseRouter.post('/download/content', async (c: Context) => {
|
||||
try {
|
||||
const data = await c.req.json();
|
||||
const validatedData = ZCourseDownloadContent.parse(data);
|
||||
|
||||
const pdfBuffer = await generateCoursePdf(validatedData);
|
||||
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
|
||||
return c.body(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(pdfBuffer);
|
||||
controller.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error generating course PDF:', error);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
details: error
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Context, Hono } from 'hono';
|
||||
|
||||
import katex from 'katex';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const katexRouter = new Hono();
|
||||
|
||||
const katexSchema = z.object({
|
||||
latex: z.string().min(1),
|
||||
displayMode: z.boolean().optional()
|
||||
});
|
||||
|
||||
katexRouter.post('/', async (c: Context) => {
|
||||
try {
|
||||
const data = await c.req.json();
|
||||
const validatedData = katexSchema.parse(data);
|
||||
|
||||
const html = katex.renderToString(validatedData.latex, {
|
||||
displayMode: validatedData.displayMode ?? false,
|
||||
throwOnError: false
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
html
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: error.errors
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
console.error('Error rendering LaTeX:', error);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Context, Hono } from 'hono';
|
||||
import { ZCertificateDownload, ZLessonDownloadContent } from '$src/types/course/lesson';
|
||||
|
||||
import { generateLessonPdf } from '$src/utils/lesson';
|
||||
|
||||
export const lessonRouter = new Hono();
|
||||
|
||||
lessonRouter.post('/download/pdf', async (c: Context) => {
|
||||
try {
|
||||
const data = await c.req.json();
|
||||
const validatedData = ZLessonDownloadContent.parse(data);
|
||||
|
||||
const pdfBuffer = await generateLessonPdf(validatedData);
|
||||
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
return c.body(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(pdfBuffer);
|
||||
controller.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error generating lesson PDF:', error);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
details: error
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { ZCourseDownloadPresignedUrl, ZCoursePresignUrlUpload } from '$src/types/course';
|
||||
|
||||
import { CLOUDFLARE } from '$src/constants';
|
||||
import type { GetSignedUrlParameters } from '$src/utils/s3';
|
||||
import { Hono } from 'hono';
|
||||
import { authMiddleware } from '$src/middlewares/auth';
|
||||
import { generateFileKey } from '$src/utils/upload';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { s3Client } from '$src/utils/s3';
|
||||
|
||||
export const presignRouter = new Hono();
|
||||
|
||||
presignRouter.post('/upload', authMiddleware, async (c) => {
|
||||
const body = await c.req.json();
|
||||
const result = ZCoursePresignUrlUpload.parse(body);
|
||||
|
||||
const { fileName, fileType } = result;
|
||||
const fileKey = generateFileKey(fileName);
|
||||
|
||||
try {
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: CLOUDFLARE.R2.BUCKET,
|
||||
Key: fileKey,
|
||||
ContentType: fileType
|
||||
}) as GetSignedUrlParameters[1];
|
||||
|
||||
const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, {
|
||||
expiresIn: CLOUDFLARE.R2.PRESIGN_EXPIRATION_TIME
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
url: presignedUrl,
|
||||
fileKey,
|
||||
message: 'Pre-signed URL generated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating pre-signed URL:', error);
|
||||
|
||||
if (error instanceof Error && error.name === 'SignatureDoesNotMatch') {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
type: 'SIGNATURE_ERROR',
|
||||
message: 'Invalid signature for the request'
|
||||
},
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
type: 'INTERNAL_ERROR',
|
||||
message: 'Error generating pre-signed URL'
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
presignRouter.post('/download', authMiddleware, async (c) => {
|
||||
const body = await c.req.json();
|
||||
const result = ZCourseDownloadPresignedUrl.parse(body);
|
||||
|
||||
const { keys } = result;
|
||||
|
||||
try {
|
||||
const signedUrls: Record<string, string> = {};
|
||||
|
||||
await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: CLOUDFLARE.R2.BUCKET,
|
||||
Key: key
|
||||
}) as GetSignedUrlParameters[1];
|
||||
|
||||
const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, {
|
||||
expiresIn: CLOUDFLARE.R2.DOWNLOAD_EXPIRATION_TIME
|
||||
});
|
||||
|
||||
signedUrls[key] = presignedUrl;
|
||||
})
|
||||
);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
urls: signedUrls,
|
||||
message: 'Video URLs retrieved successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error Retrieving Video:', error);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
type: 'INTERNAL_ERROR',
|
||||
message: 'Error Retrieving Video'
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user