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 <[email protected]>
This commit is contained in:
Rotimi Best
2025-06-09 01:38:57 +01:00
committed by GitHub
co-authored by Iniubong Obonguko
parent 9eb7b01587
commit 82be557a42
80 changed files with 3291 additions and 2302 deletions
@@ -1,4 +1,4 @@
export function formatYoutubeVideo(url: string, errors: { video: string }) {
export function formatYoutubeVideo(url: string, errors: Record<string, string>) {
console.log(`url`, url);
const prefix = 'https://www.youtube.com/embed';
@@ -89,7 +89,7 @@ export const addMathPlugin = (tinymce: any) => {
if (srcMatch) {
// 2. Point the formula to our server
const customAPI = env.PUBLIC_SERVER_URL + '/katex';
const customAPI = env.PUBLIC_SERVER_URL + '/course/katex';
const url = srcMatch[1]?.replaceAll('https://latex.codecogs.com/svg.image', customAPI);
// 3. Fetch the HTML from the server
@@ -0,0 +1,19 @@
import axios from 'axios';
import { env } from '$env/dynamic/public';
import { getAccessToken } from '$lib/utils/functions/supabase';
export const apiClient = axios.create({
baseURL: `${env.PUBLIC_SERVER_URL}`,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
});
apiClient.interceptors.request.use(async (config) => {
const token = await getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
@@ -1,8 +1,3 @@
import { QUESTION_TYPE } from '$lib/components/Question/constants';
import { STATUS } from '$lib/utils/constants/course';
import { isUUID } from '$lib/utils/functions/isUUID';
import { supabase } from '$lib/utils/functions/supabase';
import { isOrgAdmin } from '$lib/utils/store/org';
import type {
Course,
Exercise,
@@ -15,12 +10,19 @@ import type {
ProfileCourseProgress
} from '$lib/utils/types';
import type { PostgrestError, PostgrestSingleResponse } from '@supabase/supabase-js';
import { QUESTION_TYPE } from '$lib/components/Question/constants';
import { STATUS } from '$lib/utils/constants/course';
import { apiClient } from '$lib/utils/services/api';
import { get } from 'svelte/store';
import { isOrgAdmin } from '$lib/utils/store/org';
import { isUUID } from '$lib/utils/functions/isUUID';
import { supabase } from '$lib/utils/functions/supabase';
export async function fetchCourses(profileId, orgId) {
if (!orgId || !profileId) return;
const match = {};
const match: { member_profile_id?: string } = {};
// Filter by profile_id if role isn't admin within organization
if (!get(isOrgAdmin)) {
match.member_profile_id = profileId;
@@ -257,15 +259,13 @@ export async function getMarks(courseId) {
if (!courseId) return;
// Gets courses for a particular organisation where the current logged in user is a groupmember
const { data: marks } = await supabase
.rpc('get_marks')
.eq('course_id', courseId);
const { data: marks } = await supabase.rpc('get_marks').eq('course_id', courseId);
return { marks };
}
export function fetchLesson(lessonId: Lesson['id']) {
return supabase
export async function fetchLesson(lessonId: Lesson['id']) {
const { data, error } = await supabase
.from('lesson')
.select(
`id,
@@ -282,6 +282,29 @@ export function fetchLesson(lessonId: Lesson['id']) {
)
.eq('id', lessonId)
.single();
if (data) {
const keys = data.videos.filter((video) => video.type === 'upload').map((video) => video.key);
if (!keys.length) {
return { data, error };
}
try {
const response = await apiClient.post('/course/presign/download', { keys });
data.videos = data.videos.map((video) => {
if (video.type === 'upload') {
video.link = response.data.urls[video.key];
}
return video;
});
} catch (error) {
console.error('Error retrieving videos:', error);
}
}
return { data, error };
}
export function fetchLesssonLanguageHistory(lessonId: string, locale: string, endRange: number) {
+3 -5
View File
@@ -279,10 +279,7 @@ export interface Group_attendance {
groupmember?: Groupmember;
}
export enum VideoType {
youtube,
muse
}
export type LessonVideoType = 'youtube' | 'generic' | 'upload';
export interface LessonPage {
id?: string | null;
@@ -296,8 +293,9 @@ export interface LessonPage {
note: string;
slide_url: string;
videos: Array<{
type: string;
type: LessonVideoType;
link: string;
key?: string;
metadata?: {
svid?: string;
};