Compare commits

..

7 Commits

Author SHA1 Message Date
Talal Sharabi
c01168976f Fix session cookie: use COOKIE_SECURE env var instead of NODE_ENV
The Secure flag was tied to NODE_ENV=production, which broke sessions
on HTTP staging servers. Using a dedicated COOKIE_SECURE env var
allows production-mode Next.js to run over HTTP for staging while
still enabling Secure cookies in production (HTTPS).

Made-with: Cursor
2026-03-16 11:48:28 +04:00
Talal Sharabi
e27d8231e9 Fix login redirect: push router immediately and skip SWR revalidation
After login, explicitly navigate to destination instead of relying
on useEffect. Also skip SWR revalidation to prevent a race where the
re-fetch overrides the mutated user data.

Made-with: Cursor
2026-03-15 20:02:40 +04:00
Talal Sharabi
1544215008 Fix login response: return user directly instead of {user: ...}
The SWR useUser hook expects the user object directly, but the login
API was wrapping it in {user: ...}. This caused user.isVerified to be
undefined after login, triggering the email verification gate.

Made-with: Cursor
2026-03-15 15:57:12 +04:00
Talal Sharabi
3e143526e8 Fix build errors: add missing stub exports and fix implicit any types
- Add getApprovalWorkflowsByEntities to approval.workflows.be.ts stub
- Add filterAllowedUsers to users.be.ts stub
- Make convertToUsers synchronous in groups.be.ts stub
- Remove unused import of deleted API route in entities/[id]/index.tsx
- Fix implicit any type errors in approval-workflows, entities, permissions pages

Made-with: Cursor
2026-03-14 17:17:56 +04:00
Talal Sharabi
e750317757 Remove remaining mongodb references
- Replace ObjectId import with string type in approval.workflow.ts
- Delete mongodb.d.ts type declaration file

Made-with: Cursor
2026-03-14 17:06:50 +04:00
Talal Sharabi
65554ed4ce Remove all @firebase/util imports, replace uuidv4 with crypto.randomUUID
- Create src/utils/uuid.ts utility using crypto.randomUUID()
- Replace all uuidv4 imports from @firebase/util across 11 component files
- Replace base64 import in paypal.ts with Buffer-based implementation

Made-with: Cursor
2026-03-14 17:00:27 +04:00
Talal Sharabi
3c580498d3 Fix ESLint error: rename 'module' variable to avoid @next/next/no-assign-module-variable
Made-with: Cursor
2026-03-14 16:54:06 +04:00
27 changed files with 48 additions and 32 deletions

View File

View File

@@ -0,0 +1,8 @@
# Google Cloud EnCoach Analysis
Open this Cursor transcript using the chat UUID:
- [Google Cloud EnCoach Analysis](9c3d1115-4845-4980-a0d5-33767025c260)
Notes:
- This is the transcript where `talal@encoach.com` was used for the Google Cloud EnCoach infrastructure analysis.

View File

@@ -13,7 +13,7 @@ import validateBlanks from "../validateBlanks";
import { toast } from "react-toastify";
import setEditingAlert from "../../Shared/setEditingAlert";
import PromptEdit from "../../Shared/PromptEdit";
import { uuidv4 } from "@firebase/util";
import { uuidv4 } from "@/utils/uuid";
interface Word {
letter: string;

View File

@@ -11,7 +11,7 @@ import { toast } from "react-toastify";
import setEditingAlert from "../../Shared/setEditingAlert";
import { MdEdit, MdEditOff } from "react-icons/md";
import MCOption from "./MCOption";
import { uuidv4 } from "@firebase/util";
import { uuidv4 } from "@/utils/uuid";
const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }> = ({ exercise, sectionId }) => {

View File

@@ -18,7 +18,7 @@ import { toast } from 'react-toastify';
import { DragEndEvent } from '@dnd-kit/core';
import { handleMatchSentencesReorder } from '@/stores/examEditor/reorder/local';
import PromptEdit from '../Shared/PromptEdit';
import { uuidv4 } from '@firebase/util';
import { uuidv4 } from '@/utils/uuid';
const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: number }> = ({ exercise, sectionId }) => {
const { currentModule, dispatch } = useExamEditorStore();

View File

@@ -11,7 +11,7 @@ import { useCallback, useEffect, useState } from "react";
import { MdAdd } from "react-icons/md";
import Alert, { AlertItem } from "../../Shared/Alert";
import PromptEdit from "../../Shared/PromptEdit";
import { uuidv4 } from "@firebase/util";
import { uuidv4 } from "@/utils/uuid";
const UnderlineMultipleChoice: React.FC<{exercise: MultipleChoiceExercise, sectionId: number}> = ({

View File

@@ -18,7 +18,7 @@ import SortableQuestion from '../../Shared/SortableQuestion';
import setEditingAlert from '../../Shared/setEditingAlert';
import { handleMultipleChoiceReorder } from '@/stores/examEditor/reorder/local';
import PromptEdit from '../../Shared/PromptEdit';
import { uuidv4 } from '@firebase/util';
import { uuidv4 } from '@/utils/uuid';
interface MultipleChoiceProps {
exercise: MultipleChoiceExercise;

View File

@@ -16,7 +16,7 @@ import setEditingAlert from '../Shared/setEditingAlert';
import { DragEndEvent } from '@dnd-kit/core';
import { handleTrueFalseReorder } from '@/stores/examEditor/reorder/local';
import PromptEdit from '../Shared/PromptEdit';
import { uuidv4 } from '@firebase/util';
import { uuidv4 } from '@/utils/uuid';
const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> = ({ exercise, sectionId }) => {
const { currentModule, dispatch } = useExamEditorStore();

View File

@@ -22,7 +22,7 @@ import { validateEmptySolutions, validateQuestionText, validateWordCount } from
import { handleWriteBlanksReorder } from '@/stores/examEditor/reorder/local';
import { ParsedQuestion, parseText, reconstructText } from './parsing';
import PromptEdit from '../Shared/PromptEdit';
import { uuidv4 } from '@firebase/util';
import { uuidv4 } from '@/utils/uuid';
const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise; }> = ({ sectionId, exercise }) => {

View File

@@ -17,7 +17,7 @@ import { validateQuestions, validateEmptySolutions, validateWordCount } from "./
import Header from "../../Shared/Header";
import BlanksFormEditor from "./BlanksFormEditor";
import PromptEdit from "../Shared/PromptEdit";
import { uuidv4 } from "@firebase/util";
import { uuidv4 } from "@/utils/uuid";
const WriteBlanksForm: React.FC<{ sectionId: number; exercise: WriteBlanksExercise }> = ({ sectionId, exercise }) => {

View File

@@ -7,7 +7,7 @@ import ai_usage from "@/utils/ai.detection";
import { calculateBandScore, getGradingLabel } from "@/utils/score";
import moment from "moment";
import { Assignment } from "@/interfaces/results";
import { uuidv4 } from "@firebase/util";
import { uuidv4 } from "@/utils/uuid";
import { useRouter } from "next/router";
import { uniqBy } from "lodash";
import { sortByModule } from "@/utils/moduleUtils";

View File

@@ -1,9 +1,8 @@
import { ObjectId } from "mongodb";
import { Module } from ".";
import { Type, User, userTypeLabels, userTypeLabelsShort } from "./user";
export interface ApprovalWorkflow {
_id?: ObjectId,
_id?: string,
name: string,
entityId: string,
requester: User["id"],

View File

@@ -6,7 +6,7 @@ export const sessionOptions: IronSessionOptions = {
password: process.env.SECRET_COOKIE_PASSWORD as string,
cookieName: "eCrop/ielts",
cookieOptions: {
secure: process.env.NODE_ENV === "production",
secure: process.env.COOKIE_SECURE === "true",
},
};

5
src/mongodb.d.ts vendored
View File

@@ -1,5 +0,0 @@
import {MongoClient} from "mongodb";
declare global {
var _mongoClientPromise: Promise<MongoClient>;
}

View File

@@ -9,10 +9,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") return res.status(404).json({ok: false});
if (!req.session.token) return res.status(401).json({ok: false});
const module = req.query.module as string;
const examModule = req.query.module as string;
try {
const resp = await fetch(`${ODOO_URL}/api/exam/${module}`, {
const resp = await fetch(`${ODOO_URL}/api/exam/${examModule}`, {
method: "POST",
headers: {
Authorization: `Bearer ${req.session.token}`,

View File

@@ -38,7 +38,7 @@ async function login(req: NextApiRequest, res: NextApiResponse) {
req.session.token = data.token;
await req.session.save();
res.status(200).json({user});
res.status(200).json(user);
} catch (error) {
console.error("Login error:", error);
res.status(500).json({error: "Login failed"});

View File

@@ -76,8 +76,8 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const allAssigneeIds: string[] = [
...new Set(
workflows
.map(workflow => workflow.steps
.map(step => step.assignees)
.map((workflow: any) => workflow.steps
.map((step: any) => step.assignees)
.flat()
).flat()
)

View File

@@ -47,8 +47,6 @@ import {
BsX,
} from "react-icons/bs";
import { toast } from "react-toastify";
import entities from "../../api/entities";
const expirationDateColor = (date: Date) => {
const momentDate = moment(date);
const today = moment(new Date());
@@ -120,7 +118,7 @@ export const getServerSideProps = withIronSessionSsr(
]);
const usersWithRole = entityUsers.map((u) => {
const e = u?.entities?.find((e) => e.id === id);
const e = u?.entities?.find((e: any) => e.id === id);
return { ...u, role: findBy(entity.roles, "id", e?.role) };
});

View File

@@ -82,7 +82,8 @@ export default function Login({ destination = "/" }: { destination?: string }) {
toast.success("You have been logged in!", {
toastId: "login-successful",
});
mutateUser(response.data);
mutateUser(response.data, { revalidate: false });
router.push(destination);
})
.catch((e) => {
if (e.response.status === 401) {

View File

@@ -66,7 +66,7 @@ export const getServerSideProps = withIronSessionSsr(
// const permissions: Permission[] = await res.json();
// Pass data to the page via props
const usersData: BasicUser[] = permission.users.reduce(
(acc: BasicUser[], userId) => {
(acc: BasicUser[], userId: any) => {
const user = filteredUsers.find((u) => u.id === userId) as BasicUser;
if (!!user) acc.push(user);
return acc;

View File

@@ -10,7 +10,7 @@ import moment from "moment";
import { ToastContainer } from "react-toastify";
import clsx from "clsx";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import { uuidv4 } from "@firebase/util";
import { uuidv4 } from "@/utils/uuid";
import { usePDFDownload } from "@/hooks/usePDFDownload";
import useRecordStore from "@/stores/recordStore";
import StatsGridItem from "@/components/Medium/StatGridItem";

View File

@@ -14,7 +14,7 @@ import useTrainingContentStore from "@/stores/trainingContentStore";
import axios from "axios";
import { ITrainingContent } from "@/training/TrainingInterfaces";
import moment from "moment";
import { uuidv4 } from "@firebase/util";
import { uuidv4 } from "@/utils/uuid";
import TrainingScore from "@/training/TrainingScore";
import ModuleBadge from "@/components/ModuleBadge";
import RecordFilter from "@/components/Medium/RecordFilter";

View File

@@ -1,3 +1,11 @@
export async function getApprovalWorkflows(..._args: any[]): Promise<any[]> {
return [];
}
export async function getApprovalWorkflow(..._args: any[]): Promise<any> {
return null;
}
export async function getApprovalWorkflowsByEntities(..._args: any[]): Promise<any[]> {
return [];
}

View File

@@ -2,8 +2,8 @@ export async function getParticipantsGroups(..._args: any[]): Promise<any[]> {
return [];
}
export async function convertToUsers(..._args: any[]): Promise<any[]> {
return [];
export function convertToUsers(..._args: any[]): any {
return _args[0] || {};
}
export async function getGroupsForEntities(..._args: any[]): Promise<any[]> {

View File

@@ -1,5 +1,5 @@
import {TokenError, TokenSuccess} from "@/interfaces/paypal";
import {base64} from "@firebase/util";
const base64 = { encodeString: (s: string) => Buffer.from(s).toString("base64") };
import axios from "axios";
export const getAccessToken = async () => {

View File

@@ -37,3 +37,7 @@ export async function countUsersByTypes(..._args: any[]): Promise<number> {
export async function countAllowedUsers(..._args: any[]): Promise<number> {
return 0;
}
export function filterAllowedUsers(..._args: any[]): any[] {
return _args[0] || [];
}

3
src/utils/uuid.ts Normal file
View File

@@ -0,0 +1,3 @@
export function uuidv4(): string {
return crypto.randomUUID();
}