Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,14 @@ export class JwtCookieStrategy extends PassportStrategy(
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request: IRequestWithCookies) => {
return request?.cookies?.authentication;
const token = request?.cookies?.authentication;
console.log("🍪 JWT Cookie Strategy - extracting token from cookies:", {
cookiesExist: !!request?.cookies,
authCookieExists: !!token,
tokenLength: token ? token.length : 0,
tokenPrefix: token ? token.substring(0, 20) + "..." : "none"
});
return token;
},
]),
ignoreExpiration: false,
Expand All @@ -38,7 +45,9 @@ export class JwtCookieStrategy extends PassportStrategy(

// eslint-disable-next-line @typescript-eslint/no-unused-vars
validate(payload: IJwtPayload): UserSession {
return {
console.log("🔍 JWT Cookie Strategy - validating payload:", JSON.stringify(payload, null, 2));

const userSession = {
userId: payload.userID,
role: payload.role,
groupId: payload.groupID,
Expand All @@ -47,5 +56,8 @@ export class JwtCookieStrategy extends PassportStrategy(
returnUrl: payload.returnUrl,
launch_presentation_locale: payload.launch_presentation_locale,
};

console.log("✅ JWT Cookie Strategy - returning userSession:", JSON.stringify(userSession, null, 2));
return userSession;
}
}
1 change: 1 addition & 0 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ COPY --chown=node:node --from=sourcer $DIR/apps/api/dist ./dist
COPY --chown=node:node --from=sourcer $DIR/node_modules $DIR/node_modules
COPY --chown=node:node --from=sourcer $DIR/prisma ./prisma
COPY --chown=node:node --from=sourcer $DIR/apps/api/migrate.sh ./migrate.sh
RUN sed -i 's/\r$//' ./migrate.sh && chmod +x ./migrate.sh
COPY --chown=node:node --from=sourcer $DIR/apps/api/ensureDb.js ./ensureDb.js
COPY --chown=node:node --from=sourcer $DIR/apps/api/src/scripts ./scripts

Expand Down
16 changes: 16 additions & 0 deletions apps/api/src/api/admin/admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2064,6 +2064,9 @@ export class AdminService {
}

async removeAssignment(id: number): Promise<BaseAssignmentResponseDto> {
// Fix: Delete related records in correct order to avoid foreign key constraint violations

// Step 1: Delete question responses and attempt-related data
await this.prisma.questionResponse.deleteMany({
where: { assignmentAttempt: { assignmentId: id } },
});
Expand All @@ -2076,6 +2079,16 @@ export class AdminService {
where: { assignmentId: id },
});

// Step 2: Delete grading jobs (fixes GradingJob_assignmentId_fkey constraint)
await this.prisma.gradingJob.deleteMany({
where: { assignmentId: id },
});

// Step 3: Delete grading audits (fixes GradingAudit_questionId_fkey constraint)
await this.prisma.gradingAudit.deleteMany({
where: { question: { assignmentId: id } },
});

await this.prisma.assignmentGroup.deleteMany({
where: { assignmentId: id },
});
Expand All @@ -2100,10 +2113,12 @@ export class AdminService {
where: { assignmentId: id },
});

// Step 4: Delete questions (after grading audits are deleted)
await this.prisma.question.deleteMany({
where: { assignmentId: id },
});

// Step 5: Verify assignment exists before final deletion
const assignmentExists = await this.prisma.assignment.findUnique({
where: { id },
select: { id: true, name: true, type: true },
Expand All @@ -2113,6 +2128,7 @@ export class AdminService {
throw new NotFoundException(`Assignment with Id ${id} not found.`);
}

// Step 6: Finally delete the assignment
await this.prisma.assignment.delete({
where: { id },
});
Expand Down
1 change: 1 addition & 0 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ COPY --chown=node:node --from=sourcer $DIR/apps/web/package.json ./package.json
COPY --chown=node:node --from=sourcer $DIR/node_modules $DIR/node_modules
COPY --chown=node:node --from=sourcer $DIR/apps/web/public ./public
COPY --chown=node:node --from=sourcer $DIR/apps/web/entrypoint.sh ./entrypoint.sh
RUN sed -i 's/\r$//' ./entrypoint.sh && chmod +x ./entrypoint.sh
ENV NODE_OPTIONS="--require ./node_modules/@instana/collector/src/immediate"

ENTRYPOINT ["/usr/bin/dumb-init", "--"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,14 @@ const AuthorQuestionsPage: FC<Props> = ({
defaultQuestionRetries,
}) => {
const router = useRouter();
useBeforeUnload(
const { hide: hideBeforeUnload, show: showBeforeUnload } = useBeforeUnload(
"Are you sure you want to leave this page? You will lose any unsaved changes.",
);

// Enable beforeunload protection when component mounts
useEffect(() => {
showBeforeUnload();
}, [showBeforeUnload]);
const [focusedQuestionId, setFocusedQuestionId] = useAuthorStore((state) => [
state.focusedQuestionId,
state.setFocusedQuestionId,
Expand Down Expand Up @@ -1267,7 +1272,7 @@ const AuthorQuestionsPage: FC<Props> = ({
{/* next button */}
{questions.length > 0 && (
<div className="col-span-4 md:col-span-8 lg:col-span-12 md:col-start-3 md:col-end-11 lg:col-start-3 lg:col-end-11 row-start-3 flex flex-col justify-end mb-10">
<FooterNavigation nextStep="config" />
<FooterNavigation nextStep="config" onNext={hideBeforeUnload} />
</div>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ interface Props extends ComponentPropsWithoutRef<"nav"> {
assignmentId?: string;
nextStep?: string;
currentStepId?: number;
onNext?: () => void;
}

export const FooterNavigation: FC<Props> = ({
assignmentId,
nextStep = "config",
currentStepId = 1, // Assuming this is used on questions page by default
onNext,
}) => {
const router = useRouter();
const [activeAssignmentId, questions] = useAuthorStore((state) => [
Expand Down Expand Up @@ -147,6 +149,9 @@ export const FooterNavigation: FC<Props> = ({
}
return;
}

// Call onNext callback before navigation to cleanup beforeunload
onNext?.();
router.push(`/author/${activeAssignmentId}/${nextStep}`);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import {
import { useRouter } from "next/navigation";
import { useState } from "react";

export const FooterNavigation = () => {
interface Props {
onNext?: () => void;
}

export const FooterNavigation = ({ onNext }: Props = {}) => {
const router = useRouter();
const [activeAssignmentId] = useAuthorStore((state) => [
state.activeAssignmentId,
Expand All @@ -31,6 +35,8 @@ export const FooterNavigation = () => {
const isAssignmentConfigValid = validateAssignmentConfig();

if (isAssignmentConfigValid) {
// Call onNext callback before navigation to cleanup beforeunload
onNext?.();
router.push(`/author/${activeAssignmentId}/review`);
} else {
// Check if error is on current page
Expand Down
8 changes: 2 additions & 6 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,8 @@ export const metadata: Metadata = {
],
authors: [
{
name: "Skills Network",
url: "https://skills.network",
},
{
name: "Rami Maalouf",
url: "https://rami-maalouf.tech",
name: "INTELA Education Portal",
url: "https://itl-edu.devkln.xyz",
},
],
};
Expand Down
40 changes: 22 additions & 18 deletions apps/web/app/learner/(components)/Question/PresentationGrader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
import { getLiveRecordingFeedback } from "@/lib/talkToBackend";
import { useLearnerStore, useVideoRecorderStore } from "@/stores/learner";

const ffmpeg = new FFmpeg();
// Fix: Remove global FFmpeg initialization to prevent SSR errors
// FFmpeg will be initialized inside useVideoProcessor hook

/** ------------------------------------------------------------------
* HOOK #1: Manage camera stream, recording, and a manual timer
Expand Down Expand Up @@ -122,8 +123,23 @@ const useVideoRecorder = (onRecordingComplete: (blob: Blob) => void) => {
* ------------------------------------------------------------------ */
const useVideoProcessor = () => {
const [processing, setProcessing] = useState(false);
const [ffmpeg, setFfmpeg] = useState<any>(null);

// Fix: Initialize FFmpeg only on client-side to prevent SSR errors
useEffect(() => {
if (typeof window !== 'undefined') {
import('@ffmpeg/ffmpeg').then(({ FFmpeg }) => {
const ffmpegInstance = new FFmpeg();
setFfmpeg(ffmpegInstance);
});
}
}, []);

const extractAudio = async (videoBlob: Blob) => {
if (!ffmpeg) {
throw new Error('FFmpeg not initialized - client-side only');
}

try {
await ffmpeg.writeFile("input.webm", await fetchFile(videoBlob));

Expand Down Expand Up @@ -166,6 +182,10 @@ const useVideoProcessor = () => {
text: string;
segments: TranscriptSegment[];
}> => {
if (!ffmpeg) {
throw new Error('FFmpeg not initialized - client-side only');
}

setProcessing(true);
try {
if (!ffmpeg.loaded) {
Expand Down Expand Up @@ -537,23 +557,7 @@ export default function PresentationGrader({

const [currentRecordingTime, setCurrentRecordingTime] = useState(0);

useEffect(() => {
const loadFFmpeg = async () => {
if (!ffmpeg.loaded) {
await ffmpeg.load({
coreURL: await toBlobURL(
"/ffmpeg-core/ffmpeg-core.js",
"text/javascript",
),
wasmURL: await toBlobURL(
"/ffmpeg-core/ffmpeg-core.wasm",
"application/wasm",
),
});
}
};
void loadFFmpeg();
}, []);
// Fix: Remove duplicate FFmpeg loading - already handled in useVideoProcessor hook

useEffect(() => {
setCachedEvaluation(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client"; // Fix: Force client-side rendering to prevent FFmpeg SSR errors

import { transcribeAudio } from "@/app/Helpers/transcribeAudio";
import {
QuestionStore,
Expand Down Expand Up @@ -115,7 +117,8 @@ const extractAudio = async (ffmpeg: FFmpeg, videoBlob: Blob): Promise<Blob> => {
}
};

const ffmpeg = new FFmpeg();
// Fix: Remove global FFmpeg initialization to prevent SSR errors
// FFmpeg will be initialized inside component useEffect

interface VideoPresentationEditorProps {
question: QuestionStore;
Expand All @@ -134,9 +137,21 @@ const VideoPresentationEditor = ({

const targetTime = config?.targetTime ?? 0;

// Fix: Initialize FFmpeg only on client-side to prevent SSR errors
const [ffmpeg, setFfmpeg] = useState<any>(null);
const [ffmpegLoaded, setFfmpegLoaded] = useState(false);
const [processSlides, setProcessSlides] = useState(false);

// Fix: Client-side FFmpeg initialization
useEffect(() => {
if (typeof window !== 'undefined') {
import('@ffmpeg/ffmpeg').then(({ FFmpeg }) => {
const ffmpegInstance = new FFmpeg();
setFfmpeg(ffmpegInstance);
});
}
}, []);

const [videoFile, setVideoFile] = useState<File | null>(null);
const [videoURL, setVideoURL] = useState("");
const videoRef = useRef<HTMLVideoElement>(null);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default async function Home() {
const headerList = headers();
const cookie = headerList.get("cookie");
if (!cookie && process.env.NODE_ENV === "production") {
redirect("https://skills.network");
redirect("https://itl-edu.devkln.xyz");
}
const user = await getUser(cookie);

Expand Down
25 changes: 15 additions & 10 deletions apps/web/components/Loading.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
"use client";

import Lottie from "lottie-react";
import React from "react";
import React, { useEffect, useState } from "react";

interface LoadingProps {
animationData: object;
}
const Loading: React.FC<LoadingProps> = ({ animationData }) => {
if (typeof document === "undefined") {
return null;
}
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, []);

// Always render the container div for consistent SSR/CSR
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-75">
<Lottie
className="h-44 scale-150"
loop
autoplay
animationData={animationData}
/>
{isClient && (
<Lottie
className="h-44 scale-150"
loop
autoplay
animationData={animationData}
/>
)}
</div>
);
};
Expand Down
5 changes: 5 additions & 0 deletions apps/web/components/MarkDownEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ const MarkdownEditor: React.FC<Props> = ({
useEffect(() => {
let isMounted = true;
const initializeQuill = async () => {
// Fix: Add client-side check to prevent SSR errors with Quill Editor
if (
typeof window !== "undefined" &&
typeof document !== "undefined" &&
quillRef.current &&
!quillInstance
Expand All @@ -56,6 +58,9 @@ const MarkdownEditor: React.FC<Props> = ({

const QuillModule = await import("quill");
if (!isMounted) return;
// Fix: Also check quillRef.current after async operations to prevent race conditions
if (!quillRef.current) return;

const Quill = QuillModule.default;
const quill = new Quill(quillRef.current, {
theme: "snow",
Expand Down
6 changes: 5 additions & 1 deletion apps/web/components/MarkdownViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,14 @@ const MarkdownViewer: FC<Props> = (props) => {
setQuillInstance(null);
}

if (quillRef.current) {
// Fix: Add client-side check to prevent SSR errors with Quill Editor
if (quillRef.current && typeof window !== 'undefined') {
window.hljs = hljs;

void import("quill").then((QuillModule) => {
// Fix: Re-check quillRef.current after async import to prevent race conditions
if (!quillRef.current) return;

const Quill = QuillModule.default;

const quill = new Quill(quillRef.current, {
Expand Down
Loading