Compare commits

..

21 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
Talal Sharabi
5fb82abafd Migrate frontend from MongoDB/Firebase to Odoo 19 backend
- Remove all MongoDB and Firebase dependencies
- Add Odoo proxy layer (src/lib/odoo.ts) for JWT-authenticated API forwarding
- Rewrite auth routes (login, logout, register, reset) to use Odoo endpoints
- Add catch-all API route ([...path].ts) to proxy remaining endpoints to Odoo
- Rewrite special-case routes for file uploads and binary responses
- Delete ~85 legacy API routes now handled by catch-all proxy
- Replace *.be.ts MongoDB utilities with stub implementations for SSR compat
- Update Dockerfile: remove MONGODB_URI, switch from yarn to npm
- Update session.ts to store Odoo JWT token

Made-with: Cursor
2026-03-14 16:46:15 +04:00
Tiago Ribeiro
2bfb94d01b Merged in develop (pull request #162)
Implemented limit 5 sessions per User
2025-03-05 08:22:28 +00:00
Francisco Lima
df84aaadf4 Merged in limit5SessionsUser (pull request #161)
Implemented limit 5 sessions per User

Approved-by: Tiago Ribeiro
2025-03-05 08:17:05 +00:00
José Lima
2789660e8a Implemented limit 5 sessions per User 2025-03-05 04:42:54 +00:00
Tiago Ribeiro
a78e6eb64f Merged in develop (pull request #160)
Develop
2025-03-04 23:24:59 +00:00
Francisco Lima
6c7d189957 Merged in fixStudentPerformanceFreeze (pull request #159)
FixStudentPerformanceFreeze

Approved-by: Tiago Ribeiro
2025-03-04 23:24:17 +00:00
José Lima
31f2a21a76 reverted unnecessary changes 2025-03-04 23:17:20 +00:00
José Lima
c49b1c8070 Fix student performance freeze and search users in create entities
TODO: pagination in student performance freeze
2025-03-04 23:12:26 +00:00
Tiago Ribeiro
d78654a30f Merged in develop (pull request #158)
Develop
2025-03-04 10:02:57 +00:00
João Correia
655e019bf6 Merged in approval-workflows (pull request #157)
add approved field to exam

Approved-by: Tiago Ribeiro
2025-03-04 01:44:04 +00:00
Tiago Ribeiro
d7a8f496c0 Merged develop into approval-workflows 2025-03-04 01:43:32 +00:00
Joao Correia
5e363e9951 Merge branch 'approval-workflows' of bitbucket.org:ecropdev/ielts-ui into approval-workflows 2025-03-04 00:34:17 +00:00
Joao Correia
3370f3c648 add approved field to exam 2025-03-04 00:33:09 +00:00
Tiago Ribeiro
53d6b0dd51 Merged in develop (pull request #153)
Prod Update - 12/02/2025
2025-02-12 09:13:08 +00:00
166 changed files with 12365 additions and 12420 deletions

View File

View File

@@ -7,9 +7,8 @@ FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* ./
RUN yarn --frozen-lockfile
COPY package.json package-lock.json ./
RUN npm ci --legacy-peer-deps
# Rebuild the source code only when needed
@@ -23,12 +22,7 @@ COPY . .
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
ENV MONGODB_URI "mongodb+srv://user:JKpFBymv0WLv3STj@encoach.lz18a.mongodb.net/?retryWrites=true&w=majority&appName=EnCoach"
RUN yarn build
# If using npm comment out above and use below instead
# RUN npm run build
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner

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.

10505
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,6 @@
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@firebase/util": "^1.9.7",
"@headlessui/react": "^2.1.2",
"@mdi/js": "^7.1.96",
"@mdi/react": "^1.6.1",
@@ -44,9 +43,6 @@
"eslint-config-next": "13.1.6",
"exceljs": "^4.4.0",
"express-handlebars": "^7.1.2",
"firebase": "9.19.1",
"firebase-admin": "^11.10.1",
"firebase-scrypt": "^2.2.0",
"formidable": "^3.5.0",
"formidable-serverless": "^1.1.1",
"framer-motion": "^9.0.2",
@@ -56,7 +52,6 @@
"lodash": "^4.17.21",
"moment": "^2.29.4",
"moment-timezone": "^0.5.44",
"mongodb": "^6.8.1",
"next": "^14.2.5",
"nodemailer": "^6.9.5",
"nodemailer-express-handlebars": "^6.1.0",
@@ -71,7 +66,6 @@
"react-datepicker": "^4.18.0",
"react-diff-viewer": "^3.1.1",
"react-dom": "18.2.0",
"react-firebase-hooks": "^5.1.1",
"react-icons": "^5.3.0",
"react-lineto": "^3.3.0",
"react-media-recorder": "1.6.5",
@@ -114,5 +108,6 @@
"husky": "^8.0.3",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.4"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

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

@@ -9,7 +9,7 @@ import {
useReactTable,
} from "@tanstack/react-table";
import clsx from "clsx";
import { useEffect, useState } from "react";
import { useState } from "react";
import { BsArrowDown, BsArrowUp } from "react-icons/bs";
import Button from "../Low/Button";

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,32 +1,10 @@
import {initializeApp} from "firebase/app";
import * as admin from "firebase-admin/app";
import {getStorage} from "firebase/storage";
import { base64 } from "@firebase/util";
const stagingServiceAccount = require("@/constants/staging.json");
const platformServiceAccount = require("@/constants/platform.json");
const firebaseConfig = {
apiKey: process.env.FIREBASE_PUBLIC_API_KEY || "",
authDomain: process.env.FIREBASE_AUTH_DOMAIN || "",
projectId: process.env.FIREBASE_PROJECT_ID || "",
storageBucket: process.env.FIREBASE_STORAGE_BUCKET || "",
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID || "",
appId: process.env.FIREBASE_APP_ID || "",
};
export const app = initializeApp(firebaseConfig, Math.random().toString());
export const adminApp = admin.initializeApp(
{
credential: admin.cert(process.env.ENVIRONMENT === "platform" ? platformServiceAccount : stagingServiceAccount),
},
Math.random().toString(),
);
export const storage = getStorage(app);
export const app = null as any;
export const adminApp = null as any;
export const storage = null as any;
export const firebaseAuthScryptParams = {
memCost: Number(process.env.FIREBASE_SCRYPT_MEM_COST),
rounds: Number(process.env.FIREBASE_SCRYPT_ROUNDS),
saltSeparator: process.env.FIREBASE_SCRYPT_B64_SALT_SEPARATOR!,
signerKey: process.env.FIREBASE_SCRYPT_B64_SIGNER_KEY!,
}
memCost: 0,
rounds: 0,
saltSeparator: "",
signerKey: "",
};

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

@@ -29,6 +29,7 @@ export interface ExamBase {
access: AccessType;
label?: string;
requiresApproval?: boolean;
approved?: boolean;
}
export interface ReadingExam extends ExamBase {
module: "reading";

View File

@@ -1,84 +0,0 @@
import { Module } from "@/interfaces";
import { getApprovalWorkflowByFormIntaker, createApprovalWorkflow } from "@/utils/approval.workflows.be";
import client from "@/lib/mongodb";
const db = client.db(process.env.MONGODB_DB);
/* export async function createApprovalWorkflowsOnExamCreation(examAuthor: string, examEntities: string[], examId: string, examModule: string) {
const results = await Promise.all(
examEntities.map(async (entity) => {
const configuredWorkflow = await getApprovalWorkflowByFormIntaker(entity, examAuthor);
if (!configuredWorkflow) {
return { entity, created: false };
}
configuredWorkflow.modules.push(examModule as Module);
configuredWorkflow.name = examId;
configuredWorkflow.examId = examId;
configuredWorkflow.entityId = entity;
configuredWorkflow.startDate = Date.now();
try {
await createApprovalWorkflow("active-workflows", configuredWorkflow);
return { entity, created: true };
} catch (error: any) {
return { entity, created: false };
}
})
);
const successCount = results.filter((r) => r.created).length;
const totalCount = examEntities.length;
return {
successCount,
totalCount,
};
} */
// TEMPORARY BEHAVIOUR! ONLY THE FIRST CONFIGURED WORKFLOW FOUND IS STARTED
export async function createApprovalWorkflowOnExamCreation(examAuthor: string, examEntities: string[], examId: string, examModule: string) {
let successCount = 0;
let totalCount = 0;
for (const entity of examEntities) {
const configuredWorkflow = await getApprovalWorkflowByFormIntaker(entity, examAuthor);
if (!configuredWorkflow) {
continue;
}
totalCount = 1; // a workflow was found
configuredWorkflow.modules.push(examModule as Module);
configuredWorkflow.name = examId;
configuredWorkflow.examId = examId;
configuredWorkflow.entityId = entity;
configuredWorkflow.startDate = Date.now();
configuredWorkflow.steps[0].completed = true;
configuredWorkflow.steps[0].completedBy = examAuthor;
configuredWorkflow.steps[0].completedDate = Date.now();
try {
await createApprovalWorkflow("active-workflows", configuredWorkflow);
successCount = 1;
break; // Stop after the first success
} catch (error: any) {
break;
}
}
// commented because they asked for every exam to stay confidential
/* if (totalCount === 0) { // current behaviour: if no workflow was found skip approval process
await db.collection(examModule).updateOne(
{ id: examId },
{ $set: { id: examId, access: "private" }},
{ upsert: true }
);
} */
return {
successCount,
totalCount,
};
}

View File

@@ -1,32 +0,0 @@
import {MongoClient} from "mongodb";
if (!process.env.MONGODB_URI) {
throw new Error('Invalid/Missing environment variable: "MONGODB_URI"');
}
const uri = process.env.MONGODB_URI || "";
const options = {
maxPoolSize: 10,
};
let client: MongoClient;
if (process.env.NODE_ENV === "development") {
// In development mode, use a global variable so that the value
// is preserved across module reloads caused by HMR (Hot Module Replacement).
let globalWithMongo = global as typeof globalThis & {
_mongoClient?: MongoClient;
};
if (!globalWithMongo._mongoClient) {
globalWithMongo._mongoClient = new MongoClient(uri, options);
}
client = globalWithMongo._mongoClient;
} else {
// In production mode, it's best to not use a global variable.
client = new MongoClient(uri, options);
}
// Export a module-scoped MongoClient. By doing this in a
// separate module, the client can be shared across functions.
export default client;

136
src/lib/odoo.ts Normal file
View File

@@ -0,0 +1,136 @@
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {IncomingMessage} from "http";
const ODOO_URL = process.env.ODOO_URL || "http://localhost:8069";
function buildOdooUrl(req: NextApiRequest, overridePath?: string): string {
const path = overridePath || req.url || "/";
const [pathname, qs] = path.split("?");
return `${ODOO_URL}${pathname}${qs ? `?${qs}` : ""}`;
}
function collectRawBody(req: IncomingMessage): Promise<Buffer> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => resolve(Buffer.concat(chunks)));
req.on("error", reject);
});
}
export function proxyToOdoo(overridePath?: string) {
return withIronSessionApiRoute(async (req: NextApiRequest, res: NextApiResponse) => {
const token = req.session.token;
if (!token) return res.status(401).json({error: "Not authenticated"});
const url = buildOdooUrl(req, overridePath);
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`,
};
let body: string | undefined;
if (req.method !== "GET" && req.method !== "HEAD") {
headers["Content-Type"] = req.headers["content-type"] || "application/json";
body = typeof req.body === "object" ? JSON.stringify(req.body) : req.body;
}
try {
const resp = await fetch(url, {
method: req.method,
headers,
body,
});
const contentType = resp.headers.get("content-type") || "";
res.status(resp.status);
if (contentType.includes("application/json")) {
const data = await resp.json();
return res.json(data);
}
const buffer = Buffer.from(await resp.arrayBuffer());
res.setHeader("Content-Type", contentType);
return res.send(buffer);
} catch (err) {
console.error("[odoo-proxy]", url, err);
return res.status(502).json({error: "Backend unavailable"});
}
}, sessionOptions);
}
export function proxyToOdooPublic(overridePath?: string) {
return withIronSessionApiRoute(async (req: NextApiRequest, res: NextApiResponse) => {
const url = buildOdooUrl(req, overridePath);
const headers: Record<string, string> = {};
if (req.session.token) {
headers.Authorization = `Bearer ${req.session.token}`;
}
let body: string | undefined;
if (req.method !== "GET" && req.method !== "HEAD") {
headers["Content-Type"] = req.headers["content-type"] || "application/json";
body = typeof req.body === "object" ? JSON.stringify(req.body) : req.body;
}
try {
const resp = await fetch(url, {method: req.method, headers, body});
const contentType = resp.headers.get("content-type") || "";
res.status(resp.status);
if (contentType.includes("application/json")) {
return res.json(await resp.json());
}
const buffer = Buffer.from(await resp.arrayBuffer());
res.setHeader("Content-Type", contentType);
return res.send(buffer);
} catch (err) {
console.error("[odoo-proxy-public]", url, err);
return res.status(502).json({error: "Backend unavailable"});
}
}, sessionOptions);
}
export function proxyStreamToOdoo(overridePath?: string) {
return withIronSessionApiRoute(async (req: NextApiRequest, res: NextApiResponse) => {
const token = req.session.token;
if (!token) return res.status(401).json({error: "Not authenticated"});
const url = buildOdooUrl(req, overridePath);
const rawBody = await collectRawBody(req);
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`,
};
if (req.headers["content-type"]) {
headers["Content-Type"] = req.headers["content-type"];
}
try {
const resp = await fetch(url, {
method: req.method || "POST",
headers,
body: rawBody.length > 0 ? rawBody : undefined,
});
const contentType = resp.headers.get("content-type") || "";
res.status(resp.status);
res.setHeader("Content-Type", contentType);
if (resp.headers.get("content-disposition")) {
res.setHeader("Content-Disposition", resp.headers.get("content-disposition")!);
}
const buffer = Buffer.from(await resp.arrayBuffer());
return res.send(buffer);
} catch (err) {
console.error("[odoo-stream-proxy]", url, err);
return res.status(502).json({error: "Backend unavailable"});
}
}, sessionOptions);
}
export {ODOO_URL};

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",
},
};
@@ -14,6 +14,7 @@ export const sessionOptions: IronSessionOptions = {
declare module "iron-session" {
interface IronSessionData {
user?: User | null;
token?: string;
envVariables?: {[key: string]: string};
}
}

5
src/mongodb.d.ts vendored
View File

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

View File

@@ -1,110 +1,158 @@
/* eslint-disable @next/next/no-img-element */
import {Stat, StudentUser, User} from "@/interfaces/user";
import {useState} from "react";
import {averageLevelCalculator} from "@/utils/score";
import {groupByExam} from "@/utils/stats";
import {createColumnHelper} from "@tanstack/react-table";
import { Stat, StudentUser, User } from "@/interfaces/user";
import { useState } from "react";
import { averageLevelCalculator } from "@/utils/score";
import { groupByExam } from "@/utils/stats";
import { createColumnHelper } from "@tanstack/react-table";
import Checkbox from "@/components/Low/Checkbox";
import Table from "@/components/High/Table";
type StudentPerformanceItem = StudentUser & {entitiesLabel: string; group: string};
type StudentPerformanceItem = StudentUser & {
entitiesLabel: string;
group: string;
userStats: Stat[];
};
const StudentPerformanceList = ({items = [], stats}: {items: StudentPerformanceItem[]; stats: Stat[]}) => {
const [isShowingAmount, setIsShowingAmount] = useState(false);
const StudentPerformanceList = ({
items = [],
}: {
items: StudentPerformanceItem[];
}) => {
const [isShowingAmount, setIsShowingAmount] = useState(false);
const columnHelper = createColumnHelper<StudentPerformanceItem>();
const columnHelper = createColumnHelper<StudentPerformanceItem>();
const columns = [
columnHelper.accessor("name", {
header: "Student Name",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("email", {
header: "E-mail",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("studentID", {
header: "ID",
cell: (info) => info.getValue() || "N/A",
}),
columnHelper.accessor("group", {
header: "Group",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("entitiesLabel", {
header: "Entities",
cell: (info) => info.getValue() || "N/A",
}),
columnHelper.accessor("levels.reading", {
header: "Reading",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "reading" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.listening", {
header: "Listening",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "listening" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.writing", {
header: "Writing",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "writing" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.speaking", {
header: "Speaking",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "speaking" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels.level", {
header: "Level",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${Object.keys(groupByExam(stats.filter((x) => x.module === "level" && x.user === info.row.original.id))).length} exams`,
}),
columnHelper.accessor("levels", {
id: "overall_level",
header: "Overall",
cell: (info) =>
!isShowingAmount
? averageLevelCalculator(
items,
stats.filter((x) => x.user === info.row.original.id),
).toFixed(1)
: `${Object.keys(groupByExam(stats.filter((x) => x.user === info.row.original.id))).length} exams`,
}),
];
const columns = [
columnHelper.accessor("name", {
header: "Student Name",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("email", {
header: "E-mail",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("studentID", {
header: "ID",
cell: (info) => info.getValue() || "N/A",
}),
columnHelper.accessor("group", {
header: "Group",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("entitiesLabel", {
header: "Entities",
cell: (info) => info.getValue() || "N/A",
}),
columnHelper.accessor("levels.reading", {
header: "Reading",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${
Object.keys(
groupByExam(
info.row.original.userStats.filter(
(x) => x.module === "reading"
)
)
).length
} exams`,
}),
columnHelper.accessor("levels.listening", {
header: "Listening",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${
Object.keys(
groupByExam(
info.row.original.userStats.filter(
(x) => x.module === "listening"
)
)
).length
} exams`,
}),
columnHelper.accessor("levels.writing", {
header: "Writing",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${
Object.keys(
groupByExam(
info.row.original.userStats.filter(
(x) => x.module === "writing"
)
)
).length
} exams`,
}),
columnHelper.accessor("levels.speaking", {
header: "Speaking",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${
Object.keys(
groupByExam(
info.row.original.userStats.filter(
(x) => x.module === "speaking"
)
)
).length
} exams`,
}),
columnHelper.accessor("levels.level", {
header: "Level",
cell: (info) =>
!isShowingAmount
? info.getValue() || 0
: `${
Object.keys(
groupByExam(
info.row.original.userStats.filter(
(x) => x.module === "level"
)
)
).length
} exams`,
}),
columnHelper.accessor("userStats", {
id: "overall_level",
header: "Overall",
cell: (info) =>
!isShowingAmount
? averageLevelCalculator(
info.row.original.focus,
info.getValue()
).toFixed(1)
: `${Object.keys(groupByExam(info.getValue())).length} exams`,
}),
];
return (
<div className="flex flex-col gap-4 w-full h-full">
<Checkbox isChecked={isShowingAmount} onChange={setIsShowingAmount}>
Show Utilization
</Checkbox>
<Table<StudentPerformanceItem>
data={items.sort(
(a, b) =>
averageLevelCalculator(
items,
stats.filter((x) => x.user === b.id),
) -
averageLevelCalculator(
items,
stats.filter((x) => x.user === a.id),
),
)}
columns={columns}
searchFields={[["name"], ["email"], ["studentID"], ["entitiesLabel"], ["group"]]}
/>
</div>
);
return (
<div className="flex flex-col gap-4 w-full h-full">
<Checkbox isChecked={isShowingAmount} onChange={setIsShowingAmount}>
Show Utilization
</Checkbox>
<Table<StudentPerformanceItem>
data={items.sort(
(a, b) =>
averageLevelCalculator(b.focus, b.userStats) -
averageLevelCalculator(a.focus, a.userStats)
)}
columns={columns}
searchFields={[
["name"],
["email"],
["studentID"],
["entitiesLabel"],
["group"],
]}
/>
</div>
);
};
export default StudentPerformanceList;

View File

@@ -0,0 +1,3 @@
import {proxyToOdoo} from "@/lib/odoo";
export default proxyToOdoo();

View File

@@ -1,32 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { ApprovalWorkflow } from "@/interfaces/approval.workflow";
import { sessionOptions } from "@/lib/session";
import { requestUser } from "@/utils/api";
import { updateApprovalWorkflow } from "@/utils/approval.workflows.be";
import { withIronSessionApiRoute } from "iron-session/next";
import { ObjectId } from "mongodb";
import type { NextApiRequest, NextApiResponse } from "next";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "PUT") return await put(req, res);
}
async function put(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res);
if (!user) return res.status(401).json({ ok: false });
if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) {
return res.status(403).json({ ok: false });
}
const { id } = req.query as { id?: string };
const approvalWorkflow: ApprovalWorkflow = req.body;
if (id && approvalWorkflow) {
approvalWorkflow._id = new ObjectId(id);
await updateApprovalWorkflow("active-workflows", approvalWorkflow);
return res.status(204).end();
}
}

View File

@@ -1,74 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { ApprovalWorkflow } from "@/interfaces/approval.workflow";
import { sessionOptions } from "@/lib/session";
import { requestUser } from "@/utils/api";
import { deleteApprovalWorkflow, getApprovalWorkflow, updateApprovalWorkflow } from "@/utils/approval.workflows.be";
import { getEntityWithRoles } from "@/utils/entities.be";
import { doesEntityAllow } from "@/utils/permissions";
import { withIronSessionApiRoute } from "iron-session/next";
import { ObjectId } from "mongodb";
import type { NextApiRequest, NextApiResponse } from "next";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "DELETE") return await del(req, res);
if (req.method === "PUT") return await put(req, res);
if (req.method === "GET") return await get(req, res);
}
async function del(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res);
if (!user) return res.status(401).json({ ok: false });
if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) {
return res.status(403).json({ ok: false });
}
const { id } = req.query as { id: string };
const workflow = await getApprovalWorkflow("active-workflows", id);
if (!workflow) return res.status(404).json({ ok: false });
const entity = await getEntityWithRoles(workflow.entityId);
if (!entity) return res.status(404).json({ ok: false });
if (!doesEntityAllow(user, entity, "delete_workflow") && !["admin", "developer"].includes(user.type)) {
return res.status(403).json({ ok: false });
}
return res.status(200).json(await deleteApprovalWorkflow("active-workflows", id));
}
async function put(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res);
if (!user) return res.status(401).json({ ok: false });
if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) {
return res.status(403).json({ ok: false });
}
const { id } = req.query as { id?: string };
const workflow: ApprovalWorkflow = req.body;
if (id && workflow) {
workflow._id = new ObjectId(id);
await updateApprovalWorkflow("active-workflows", workflow);
return res.status(204).end();
}
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res);
if (!user) return res.status(401).json({ ok: false });
if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) {
return res.status(403).json({ ok: false });
}
const { id } = req.query as { id?: string };
if (id) {
return res.status(200).json(await getApprovalWorkflow("active-workflows", id));
}
}

View File

@@ -1,37 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { ApprovalWorkflow } from "@/interfaces/approval.workflow";
import { Entity } from "@/interfaces/entity";
import { sessionOptions } from "@/lib/session";
import { requestUser } from "@/utils/api";
import { replaceApprovalWorkflowsByEntities } from "@/utils/approval.workflows.be";
import { withIronSessionApiRoute } from "iron-session/next";
import type { NextApiRequest, NextApiResponse } from "next";
export default withIronSessionApiRoute(handler, sessionOptions);
interface ReplaceApprovalWorkflowsRequest {
filteredWorkflows: ApprovalWorkflow[];
userEntitiesWithLabel: Entity[];
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return await post(req, res);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res);
if (!user) return res.status(401).json({ ok: false });
if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) {
return res.status(403).json({ ok: false });
}
const { filteredWorkflows, userEntitiesWithLabel } = req.body as ReplaceApprovalWorkflowsRequest;
const configuredWorkflows: ApprovalWorkflow[] = filteredWorkflows;
const entitiesIds: string[] = userEntitiesWithLabel.map((e) => e.id);
await replaceApprovalWorkflowsByEntities(configuredWorkflows, entitiesIds);
return res.status(204).end();
}

View File

@@ -1,32 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { sessionOptions } from "@/lib/session";
import { requestUser } from "@/utils/api";
import { getApprovalWorkflows } from "@/utils/approval.workflows.be";
import { withIronSessionApiRoute } from "iron-session/next";
import type { NextApiRequest, NextApiResponse } from "next";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await get(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res);
if (!user) return res.status(401).json({ ok: false });
if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) {
return res.status(403).json({ ok: false });
}
const entityIdsString = req.query.entityIds as string;
const entityIdsArray = entityIdsString.split(",");
if (!["admin", "developer"].includes(user.type)) {
// filtering workflows that have user as assignee in at least one of the steps
return res.status(200).json(await getApprovalWorkflows("active-workflows", entityIdsArray, undefined, user.id));
} else {
return res.status(200).json(await getApprovalWorkflows("active-workflows", entityIdsArray));
}
}

View File

@@ -1,463 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { app, storage } from "@/firebase";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
import { CorporateUser, MasterCorporateUser } from "@/interfaces/user";
import { User } from "@/interfaces/user";
import { Module } from "@/interfaces";
import moment from "moment-timezone";
import ExcelJS from "exceljs";
import { getStudentGroupsForUsersWithoutAdmin } from "@/utils/groups.be";
import { getSpecificUsers, getUser } from "@/utils/users.be";
import { getUserName } from "@/utils/users";
interface GroupScoreSummaryHelper {
score: [number, number];
label: string;
sessions: string[];
}
interface AssignmentData {
id: string;
assigner: string;
assignees: string[];
results: any;
exams: { module: Module }[];
startDate: string;
excel: {
path: string;
version: string;
};
name: string;
}
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
// if (req.method === "GET") return get(req, res);
if (req.method === "POST") return await post(req, res);
}
function logWorksheetData(worksheet: any) {
worksheet.eachRow((row: any, rowNumber: number) => {
console.log(`Row ${rowNumber}:`);
row.eachCell((cell: any, colNumber: number) => {
console.log(` Cell ${colNumber}: ${cell.value}`);
});
});
}
function commonExcel({
data,
userName,
users,
sectionName,
customTable,
customTableHeaders,
renderCustomTableData,
}: {
data: AssignmentData;
userName: string;
users: User[];
sectionName: string;
customTable: string[][];
customTableHeaders: string[];
renderCustomTableData: (data: any) => string[];
}) {
const allStats = data.results.flatMap((r: any) => r.stats);
const uniqueExercises = [...new Set(allStats.map((s: any) => s.exercise))];
const assigneesData = data.assignees
.map((assignee: string) => {
const userStats = allStats.filter((s: any) => s.user === assignee);
const dates = userStats.map((s: any) => moment(s.date));
const user = users.find((u) => u.id === assignee);
return {
userId: assignee,
// added some default values in case the user is not found
// could it be possible to have an assigned user deleted from the database?
user: user || {
name: "Unknown",
email: "Unknown",
demographicInformation: { passportId: "Unknown", gender: "Unknown" },
},
...userStats.reduce(
(acc: any, curr: any) => {
return {
...acc,
correct: acc.correct + curr.score.correct,
missing: acc.missing + curr.score.missing,
total: acc.total + curr.score.total,
};
},
{ correct: 0, missing: 0, total: 0 }
),
firstDate: moment.min(...dates),
lastDate: moment.max(...dates),
stats: userStats,
};
})
.sort((a, b) => b.correct - a.correct);
const results = assigneesData.map((r: any) => r.correct);
const highestScore = Math.max(...results);
const lowestScore = Math.min(...results);
const averageScore = results.reduce((a, b) => a + b, 0) / results.length;
const firstDate = moment.min(assigneesData.map((r: any) => r.firstDate));
const lastDate = moment.max(assigneesData.map((r: any) => r.lastDate));
const firstSectionData = [
{
label: sectionName,
value: userName,
},
{
label: "Report Download date :",
value: moment().format("DD/MM/YYYY"),
},
{ label: "Test Information :", value: data.name },
{
label: "Date of Test :",
value: moment(data.startDate).format("DD/MM/YYYY"),
},
{ label: "Number of Candidates :", value: data.assignees.length },
{ label: "Highest score :", value: highestScore },
{ label: "Lowest score :", value: lowestScore },
{ label: "Average score :", value: averageScore },
{ label: "", value: "" },
{
label: "Date and time of First submission :",
value: firstDate.format("DD/MM/YYYY"),
},
{
label: "Date and time of Last submission :",
value: lastDate.format("DD/MM/YYYY"),
},
];
// Create a new workbook and add a worksheet
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Report Data");
// Populate the worksheet with the data
firstSectionData.forEach(({ label, value }, index) => {
worksheet.getCell(`A${index + 1}`).value = label; // First column (labels)
worksheet.getCell(`B${index + 1}`).value = value; // Second column (values)
});
// added empty arrays to force row spacings
const customTableAndLine = [[], ...customTable, []];
customTableAndLine.forEach((row: string[], index) => {
worksheet.addRow(row);
});
// Define the static part of the headers (before "Test Sections")
const staticHeaders = [
"Sr N",
"Candidate ID",
"First and Last Name",
"Passport/ID",
"Email ID",
"Gender",
...customTableHeaders,
];
// Define additional headers after "Test Sections"
const additionalHeaders = ["Time Spent", "Date", "Score"];
// Calculate the dynamic columns based on the testSectionsArray
const testSectionHeaders = uniqueExercises.map(
(section, index) => `Part ${index + 1}`
);
const tableColumnHeadersFirstPart = [
...staticHeaders,
...uniqueExercises.map((a) => "Test Sections"),
];
// Add the main header row, merging static columns and "Test Sections"
const tableColumnHeaders = [
...tableColumnHeadersFirstPart,
...additionalHeaders,
];
worksheet.addRow(tableColumnHeaders);
// 1 headers rows
const startIndexTable =
firstSectionData.length + customTableAndLine.length + 1;
// // Merge "Test Sections" over dynamic number of columns
// const tableColumns = staticHeaders.length + numberOfTestSections;
// K10:M12 = 10,11,12,13
// horizontally group Test Sections
// if there are test section headers to even merge:
if (testSectionHeaders.length > 1) {
worksheet.mergeCells(
startIndexTable,
staticHeaders.length + 1,
startIndexTable,
tableColumnHeadersFirstPart.length
);
}
// Add the dynamic second and third header rows for test sections and sub-columns
worksheet.addRow([
...Array(staticHeaders.length).fill(""),
...testSectionHeaders,
"",
"",
"",
]);
worksheet.addRow([
...Array(staticHeaders.length).fill(""),
...uniqueExercises.map(() => "Grammar & Vocabulary"),
"",
"",
"",
]);
worksheet.addRow([
...Array(staticHeaders.length).fill(""),
...uniqueExercises.map(
(exercise) => allStats.find((s: any) => s.exercise === exercise).type
),
"",
"",
"",
]);
// vertically group based on the part, exercise and type
staticHeaders.forEach((header, index) => {
worksheet.mergeCells(
startIndexTable,
index + 1,
startIndexTable + 3,
index + 1
);
});
assigneesData.forEach((data, index) => {
worksheet.addRow([
index + 1,
data.userId,
data.user.name,
data.user.demographicInformation?.passportId,
data.user.email,
data.user.demographicInformation?.gender,
...renderCustomTableData(data),
...uniqueExercises.map((exercise) => {
const score = data.stats.find(
(s: any) => s.exercise === exercise && s.user === data.userId
).score;
return `${score.correct}/${score.total}`;
}),
`${Math.ceil(
data.stats.reduce((acc: number, curr: any) => acc + curr.timeSpent, 0) /
60
)} minutes`,
data.lastDate.format("DD/MM/YYYY HH:mm"),
data.correct,
]);
});
worksheet.addRow([""]);
worksheet.addRow([""]);
for (let i = 0; i < tableColumnHeaders.length; i++) {
worksheet.getColumn(i + 1).width = 30;
}
// Apply styles to the headers
[startIndexTable].forEach((rowNumber) => {
worksheet.getRow(rowNumber).eachCell((cell) => {
if (cell.value) {
cell.fill = {
type: "pattern",
pattern: "solid",
fgColor: { argb: "FFBFBFBF" }, // Grey color for headers
};
cell.font = { bold: true };
cell.alignment = { vertical: "middle", horizontal: "center" };
}
});
});
worksheet.addRow(["Printed by: Confidential Information"]);
worksheet.addRow(["info@encoach.com"]);
// Convert workbook to Buffer (Node.js) or Blob (Browser)
return workbook.xlsx.writeBuffer();
}
function corporateAssignment(
user: CorporateUser,
data: AssignmentData,
users: User[]
) {
return commonExcel({
data,
userName: user.name || "",
users,
sectionName: "Corporate Name :",
customTable: [],
customTableHeaders: [],
renderCustomTableData: () => [],
});
}
async function mastercorporateAssignment(
user: MasterCorporateUser,
data: AssignmentData,
users: User[]
) {
const userGroups = await getStudentGroupsForUsersWithoutAdmin(
user.id,
data.assignees
);
const adminUsers = [...new Set(userGroups.map((g) => g.admin))];
const userGroupsParticipants = userGroups.flatMap((g) => g.participants);
const adminsData = await getSpecificUsers(adminUsers);
const companiesData = adminsData.map((user) => {
const name = getUserName(user);
const users = userGroupsParticipants.filter((p) =>
data.assignees.includes(p)
);
const stats = data.results
.flatMap((r: any) => r.stats)
.filter((s: any) => users.includes(s.user));
const correct = stats.reduce(
(acc: number, s: any) => acc + s.score.correct,
0
);
const total = stats.reduce(
(acc: number, curr: any) => acc + curr.score.total,
0
);
return {
name,
correct,
total,
};
});
const customTable = [
...companiesData,
{
name: "Total",
correct: companiesData.reduce((acc, curr) => acc + curr.correct, 0),
total: companiesData.reduce((acc, curr) => acc + curr.total, 0),
},
].map((c) => [c.name, `${c.correct}/${c.total}`]);
const customTableHeaders = [
{ name: "Corporate", helper: (data: any) => data.user.corporateName },
];
return commonExcel({
data,
userName: user.name || "",
users: users.map((u) => {
const userGroup = userGroups.find((g) => g.participants.includes(u.id));
const admin = adminsData.find((a) => a.id === userGroup?.admin);
return {
...u,
corporateName: getUserName(admin),
};
}),
sectionName: "Master Corporate Name :",
customTable: [["Corporate Summary"], ...customTable],
customTableHeaders: customTableHeaders.map((h) => h.name),
renderCustomTableData: (data) =>
customTableHeaders.map((h) => h.helper(data)),
});
}
async function post(req: NextApiRequest, res: NextApiResponse) {
// verify if it's a logged user that is trying to export
if (req.session.user) {
const { id } = req.query as { id: string };
const assignment = await db.collection("assignments").findOne<AssignmentData>({ id: id });
if (!assignment) {
res.status(400).end();
return;
}
// if (
// data.excel &&
// data.excel.path &&
// data.excel.version === process.env.EXCEL_VERSION
// ) {
// // if it does, return the excel url
// const fileRef = ref(storage, data.excel.path);
// const url = await getDownloadURL(fileRef);
// res.status(200).end(url);
// return;
// }
const objectIds = assignment.assignees.map(id => id);
const users = await db.collection("users").find<User>({
id: { $in: objectIds }
}).toArray();
const user = await db.collection("users").findOne<User>({ id: assignment.assigner });
// we'll need the user in order to get the user data (name, email, focus, etc);
if (user && users) {
// generate the file ref for storage
const fileName = `${Date.now().toString()}.xlsx`;
const refName = `assignment_report/${fileName}`;
const fileRef = ref(storage, refName);
const getExcelFn = () => {
switch (user.type) {
case "teacher":
case "corporate":
return corporateAssignment(user as CorporateUser, assignment, users);
case "mastercorporate":
return mastercorporateAssignment(
user as MasterCorporateUser,
assignment,
users
);
default:
throw new Error("Invalid user type");
}
};
const buffer = await getExcelFn();
// upload the pdf to storage
await uploadBytes(fileRef, buffer, {
contentType:
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
// update the stats entries with the pdf url to prevent duplication
await db.collection("assignments").updateOne(
{ id: assignment.id },
{
$set: {
excel: {
path: refName,
version: process.env.EXCEL_VERSION,
}
}
}
);
const url = await getDownloadURL(fileRef);
res.status(200).end(url);
return;
}
}
res.status(401).json({ message: "Unauthorized" });
}

View File

@@ -1,424 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { storage } from "@/firebase";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import ReactPDF from "@react-pdf/renderer";
import GroupTestReport from "@/exams/pdf/group.test.report";
import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
import { Stat, CorporateUser } from "@/interfaces/user";
import { User, DemographicInformation } from "@/interfaces/user";
import { Module } from "@/interfaces";
import { ModuleScore, StudentData } from "@/interfaces/module.scores";
import { SkillExamDetails } from "@/exams/pdf/details/skill.exam";
import { LevelExamDetails } from "@/exams/pdf/details/level.exam";
import { calculateBandScore, getLevelScore } from "@/utils/score";
import { generateQRCode, getRadialProgressPNG, streamToBuffer } from "@/utils/pdf";
import { Group } from "@/interfaces/user";
import moment from "moment-timezone";
interface GroupScoreSummaryHelper {
score: [number, number];
label: string;
sessions: string[];
}
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
if (req.method === "POST") return post(req, res);
}
const getExamSummary = (score: number) => {
if (score > 0.8) {
return "Scoring between 81% and 100% on the English exam collectively demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading for this group of students. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. The group is encouraged to continue challenging themselves with advanced material in writing, speaking, listening, and reading to further refine their impressive command of the English language.";
}
if (score > 0.6) {
return "The group's average scores between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflect a commendable level of proficiency. There's evidence of a solid grasp of key concepts collectively, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for the entire group to further their mastery.";
}
if (score > 0.4) {
return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading indicates a moderate level of understanding for the group. While there's a commendable grasp of key concepts collectively, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. The group is encouraged to work together with consistent effort and targeted focus on weaker areas.";
}
if (score > 0.2) {
return "The group's average scores between 21% and 40% on the English exam, encompassing writing, speaking, listening, and reading, show some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills for the entire group. Strengthening writing, speaking, listening, and reading abilities collectively through consistent effort and focused group study will contribute to overall proficiency.";
}
return "The average performance of this group of students in English, covering writing, speaking, listening, and reading, indicates a collective need for improvement, with scores falling between 0% and 20%. Across all language domains, there's a significant gap in understanding key concepts. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial for the entire group. Implementing a shared, consistent study routine and seeking group support in each area can contribute to substantial progress.";
};
const getLevelSummary = (score: number) => {
if (score > 0.8) {
return "Scoring between 81% and 100% on the English exam collectively demonstrates an outstanding level of proficiency for this group of students, showcasing a mastery of key concepts related to vocabulary and grammar. There's evidence of not only a high level of skill but also a dedication to excellence. The group is encouraged to continue challenging themselves with advanced material in vocabulary and grammar to further refine their impressive command of the English language.";
}
if (score > 0.6) {
return "The group's average scores between 61% and 80% on the English exam reflect a commendable level of proficiency with solid grasp of key concepts related to vocabulary and grammar. Room for refinement and deeper exploration in these language skills remains, presenting an opportunity for the entire group to further their mastery. Consistent effort in honing nuanced aspects of vocabulary and grammar will contribute to even greater proficiency.";
}
if (score > 0.4) {
return "Scoring between 41% and 60% on the English exam indicates a moderate level of understanding for the group, with commendable grasp of key concepts related to vocabulary and grammar. Refining these fundamental language skills can lead to notable improvement. The group is encouraged to work together with consistent effort and targeted focus on enhancing their vocabulary and grammar.";
}
if (score > 0.2) {
return "The group's average scores between 21% and 40% on the English exam show some understanding of key concepts in vocabulary and grammar. However, there's room for improvement in these fundamental language skills for the entire group. Strengthening vocabulary and grammar collectively through consistent effort and focused group study will contribute to overall proficiency.";
}
return "The average performance of this group of students in English suggests a collective need for improvement, with scores falling between 0% and 20%. There's a significant gap in understanding key concepts related to vocabulary and grammar. Strengthening fundamental language skills, such as vocabulary and grammar, is crucial for the entire group. Implementing a shared, consistent study routine and seeking group support in these areas can contribute to substantial progress.";
};
const getPerformanceSummary = (module: Module, score: number) => {
if (module === "level") return getLevelSummary(score);
return getExamSummary(score);
};
const getScoreAndTotal = (stats: Stat[]) => {
return stats.reduce(
(acc, { score }) => {
return {
...acc,
correct: acc.correct + score.correct,
total: acc.total + score.total,
};
},
{ correct: 0, total: 0 },
);
};
const getLevelScoreForUserExams = (bandScore: number) => {
const [level] = getLevelScore(bandScore);
return level;
};
async function post(req: NextApiRequest, res: NextApiResponse) {
// verify if it's a logged user that is trying to export
if (req.session.user) {
const { id } = req.query as { id: string };
const data = await db.collection("assignments").findOne({ id: id }) as {
id: string;
assigner: string;
assignees: string[];
results: any;
exams: { module: Module }[];
startDate: string;
pdf: {
path: string,
version: string,
},
} | null;
if (!data) {
res.status(400).end();
return;
}
if (data.pdf && data.pdf.path && data.pdf.version === process.env.PDF_VERSION) {
// if it does, return the pdf url
const fileRef = ref(storage, data.pdf.path);
const url = await getDownloadURL(fileRef);
res.status(200).end(url);
return;
}
try {
const user = await db.collection("users").findOne<User>({ id: req.session.user.id });
// we'll need the user in order to get the user data (name, email, focus, etc);
if (user) {
// generate the QR code for the report
const qrcode = await generateQRCode((req.headers.origin || "") + req.url);
if (!qrcode) {
res.status(500).json({ ok: false });
return;
}
const flattenResults = data.results.reduce((accm: Stat[], entry: any) => {
const stats = entry.stats as Stat[];
return [...accm, ...stats];
}, []) as Stat[];
const users = await db.collection("users").find<User>({
id: { $in: data.assignees.map(id => id) }
}).toArray();
const flattenResultsWithGrade = flattenResults.map((e) => {
const focus = users.find((u) => u.id === e.user)?.focus || "academic";
const bandScore = calculateBandScore(e.score.correct, e.score.total, e.module, focus);
return { ...e, bandScore };
});
// in order to make sure we are using unique modules, generate the set based on them
const uniqueModules = [...new Set(flattenResults.map(item => item.module))] as Module[];
const moduleResults = uniqueModules.map((module) => {
const moduleResults = flattenResultsWithGrade.filter((e) => e.module === module);
const baseBandScore = moduleResults.reduce((accm, curr) => accm + curr.bandScore, 0) / moduleResults.length;
const bandScore = isNaN(baseBandScore) ? 0 : baseBandScore;
const { correct, total } = getScoreAndTotal(moduleResults);
const png = getRadialProgressPNG("azul", correct, total);
return {
bandScore,
png,
module: module[0].toUpperCase() + module.substring(1),
score: bandScore,
total,
code: module,
};
}) as ModuleScore[];
const { correct: overallCorrect, total: overallTotal } = getScoreAndTotal(flattenResults);
const baseOverallResult = overallCorrect / overallTotal;
const overallResult = isNaN(baseOverallResult) ? 0 : baseOverallResult;
const overallPNG = getRadialProgressPNG("laranja", overallCorrect, overallTotal);
// generate the overall detail report
const overallDetail = {
module: "Overall",
score: overallCorrect,
total: overallTotal,
png: overallPNG,
} as ModuleScore;
const testDetails = [overallDetail, ...moduleResults];
// generate the performance summary based on the overall result
const baseStat = data.exams[0];
const performanceSummary = getPerformanceSummary(
// from what I noticed, exams is either an array with the level module
// or X modules, either way
// as long as I verify the first entry I should be fine
baseStat.module,
overallResult,
);
const showLevel = baseStat.module === "level";
// level exams have a different report structure than the skill exams
const getCustomData = () => {
if (showLevel) {
return {
title: "GROUP ENGLISH LEVEL TEST RESULT REPORT ",
details: <LevelExamDetails detail={overallDetail} title="Group Average CEFR" />,
};
}
return {
title: "GROUP ENGLISH SKILLS TEST RESULT REPORT",
details: <SkillExamDetails testDetails={testDetails} />,
};
};
const { title, details } = getCustomData();
const numberOfStudents = data.assignees.length;
const getStudentsData = async (): Promise<StudentData[]> => {
return data.assignees.map((id) => {
const user = users.find((u) => u.id === id);
const exams = flattenResultsWithGrade.filter((e) => e.user === id);
const date =
exams.length === 0
? "N/A"
: new Date(exams[0].date).toLocaleDateString(undefined, {
year: "numeric",
month: "numeric",
day: "numeric",
});
const bandScore = exams.length === 0 ? 0 : exams.reduce((accm, curr) => accm + curr.bandScore, 0) / exams.length;
const { correct, total } = getScoreAndTotal(exams);
const result = exams.length === 0 ? "N/A" : `${correct}/${total}`;
const userDemographicInformation = user?.demographicInformation as DemographicInformation;
return {
id,
name: user?.name || "N/A",
email: user?.email || "N/A",
gender: user?.demographicInformation?.gender || "N/A",
date,
result,
level: showLevel ? getLevelScoreForUserExams(bandScore) : undefined,
bandScore,
passportId: userDemographicInformation?.passport_id || ""
};
});
};
const studentsData = await getStudentsData();
const getGroupScoreSummary = () => {
const resultHelper = studentsData.reduce((accm: GroupScoreSummaryHelper[], curr) => {
const { bandScore, id } = curr;
const flooredScore = Math.floor(bandScore);
const hasMatch = accm.find((a) => a.score.includes(flooredScore));
if (hasMatch) {
return accm.map((a) => {
if (a.score.includes(flooredScore)) {
return {
...a,
sessions: [...a.sessions, id],
};
}
return a;
});
}
return [
...accm,
{
score: [flooredScore, flooredScore + 0.5],
label: `${flooredScore} - ${flooredScore + 0.5}`,
sessions: [id],
},
];
}, []) as GroupScoreSummaryHelper[];
const result = resultHelper.map(({ score, label, sessions }) => {
const finalLabel = showLevel ? getLevelScore(score[0])[1] : label;
return {
label: finalLabel,
percent: Math.floor((sessions.length / numberOfStudents) * 100),
description: `No. Candidates ${sessions.length} of ${numberOfStudents}`,
};
});
return result;
};
const getInstitution = async () => {
try {
// due to database inconsistencies, I'll be overprotective here
const assignerUser = await db.collection("users").findOne<User>({ id: data.assigner });
// we'll need the user in order to get the user data (name, email, focus, etc);
if (assignerUser) {
if (assignerUser.type === "teacher") {
// also search for groups where this user belongs
const groups = await db.collection("groups")
.find<Group>({ participants: assignerUser.id })
.toArray();
if (groups.length > 0) {
const admins = await db.collection("users")
.find<CorporateUser>({ id: { $in: groups.map(g => g.admin).map(id => id) } })
.toArray();
const adminData = admins.find((a) => a.name);
if (adminData) {
return adminData.name;
}
}
}
return assignerUser.type
}
} catch (err) {
console.error(err);
}
return "";
};
const institution = await getInstitution();
const groupScoreSummary = getGroupScoreSummary();
const demographicInformation = user.demographicInformation as DemographicInformation;
const pdfStream = await ReactPDF.renderToStream(
<GroupTestReport
title={title}
date={moment(data.startDate)
.tz(user.demographicInformation?.timezone || "UTC")
.format("ll HH:mm:ss")}
name={user.name}
email={user.email}
id={user.id}
gender={demographicInformation?.gender}
summary={performanceSummary}
renderDetails={details}
logo={"public/logo_title.png"}
qrcode={qrcode}
numberOfStudents={numberOfStudents}
institution={institution}
studentsData={studentsData}
showLevel={showLevel}
summaryPNG={overallPNG}
summaryScore={`${Math.floor(overallResult * 100)}%`}
groupScoreSummary={groupScoreSummary}
passportId={demographicInformation?.passport_id || ""}
/>,
);
// generate the file ref for storage
const fileName = `${Date.now().toString()}.pdf`;
const refName = `assignment_report/${fileName}`;
const fileRef = ref(storage, refName);
// upload the pdf to storage
const pdfBuffer = await streamToBuffer(pdfStream);
const snapshot = await uploadBytes(fileRef, pdfBuffer, {
contentType: "application/pdf",
});
// update the stats entries with the pdf url to prevent duplication
await db.collection("assignments").updateOne(
{ id: data.id },
{
$set: {
pdf: {
path: refName,
version: process.env.PDF_VERSION,
}
}
}
);
const url = await getDownloadURL(fileRef);
res.status(200).end(url);
return;
}
res.status(401).json({ ok: false });
return;
} catch (err) {
console.error(err);
res.status(500).json({ ok: false });
return;
}
}
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (req.session.user) {
const { id } = req.query as { id: string };
const data = await db.collection("assignments").findOne({ id: id });
if (!data) {
res.status(400).end();
return;
}
if (data.assigner !== req.session.user.id) {
res.status(401).json({ ok: false });
return;
}
if (data.pdf?.path) {
const fileRef = ref(storage, data.pdf.path);
const url = await getDownloadURL(fileRef);
return res.redirect(url);
}
res.status(404).end();
return;
}
res.status(401).json({ ok: false });
return;
}

View File

@@ -1,36 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function post(req: NextApiRequest, res: NextApiResponse) {
// verify if it's a logged user that is trying to archive
if (req.session.user) {
const { id } = req.query as { id: string };
const docSnap = await db.collection("assignments").findOne({ id: id });
if (!docSnap) {
res.status(404).json({ ok: false });
return;
}
await db.collection("assignments").updateOne(
{ id: docSnap.id },
{ $set: { archived: true } }
);
res.status(200).json({ ok: true });
return;
}
res.status(401).json({ ok: false });
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
res.status(404).json({ ok: false });
}

View File

@@ -1,48 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import client from "@/lib/mongodb";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") return GET(req, res);
if (req.method === "PATCH") return PATCH(req, res);
if (req.method === "DELETE") return DELETE(req, res);
res.status(404).json({ok: false});
}
async function GET(req: NextApiRequest, res: NextApiResponse) {
const {id} = req.query;
const snapshot = await db.collection("assignments").findOne({id: id as string});
if (snapshot) {
res.status(200).json({...snapshot, id: snapshot.id});
}
}
async function DELETE(req: NextApiRequest, res: NextApiResponse) {
const {id} = req.query;
await db.collection("assignments").deleteOne({id});
res.status(200).json({ok: true});
}
async function PATCH(req: NextApiRequest, res: NextApiResponse) {
const {id} = req.query;
await db.collection("assignments").updateOne({id: id as string}, {$set: {...req.body}});
res.status(200).json({ok: true});
}

View File

@@ -1,37 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { ObjectId } from 'mongodb';
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function post(req: NextApiRequest, res: NextApiResponse) {
// verify if it's a logged user that is trying to archive
if (req.session.user) {
const { id } = req.query as { id: string };
const doc = await db.collection("assignments").findOne({ id: id });
if (!doc) {
res.status(404).json({ ok: false });
return;
}
await db.collection("assignments").updateOne(
{ id: id },
{ $set: { released: true } }
);
res.status(200).json({ ok: true });
return;
}
res.status(401).json({ ok: false });
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
res.status(404).json({ ok: false });
}

View File

@@ -1,44 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import moment from "moment";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function post(req: NextApiRequest, res: NextApiResponse) {
// verify if it's a logged user that is trying to archive
if (req.session.user) {
const { id } = req.query as { id: string };
const data = await db.collection("assignments").findOne({ id: id });
if (!data) {
res.status(404).json({ ok: false });
return;
}
if (moment().isAfter(moment(data.startDate))) {
res
.status(400)
.json({ ok: false, message: "Assignment can no longer " });
return;
}
await db.collection("assignments").updateOne(
{ id: id },
{ $set: { start: true } }
);
res.status(200).json({ ok: true });
return;
}
res.status(401).json({ ok: false });
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
res.status(404).json({ ok: false });
}

View File

@@ -1,36 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function post(req: NextApiRequest, res: NextApiResponse) {
// verify if it's a logged user that is trying to archive
if (req.session.user) {
const { id } = req.query as { id: string };
const docSnap = await db.collection("assignments").findOne({ id: id });
if (!docSnap) {
res.status(404).json({ ok: false });
return;
}
await db.collection("assignments").updateOne(
{ id: id },
{ $set: { archived: false } }
);
res.status(200).json({ ok: true });
return;
}
res.status(401).json({ ok: false });
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
res.status(404).json({ ok: false });
}

View File

@@ -1,29 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {uniqBy} from "lodash";
import {getAllAssignersByCorporate} from "@/utils/groups.be";
import {getAssignmentsByAssigners} from "@/utils/assignments.be";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") return GET(req, res);
res.status(404).json({ok: false});
}
async function GET(req: NextApiRequest, res: NextApiResponse) {
const {id} = req.query as {id: string};
const assigners = await getAllAssignersByCorporate(id, req.session.user!.type);
const assignments = await getAssignmentsByAssigners([...assigners, id]);
res.status(200).json(uniqBy(assignments, "id"));
}

View File

@@ -1,37 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {getAssignmentsByAssigner, getAssignmentsForCorporates} from "@/utils/assignments.be";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") return await GET(req, res);
res.status(404).json({ok: false});
}
async function GET(req: NextApiRequest, res: NextApiResponse) {
const {ids, startDate, endDate} = req.query as {
ids: string;
startDate?: string;
endDate?: string;
};
const startDateParsed = startDate ? new Date(startDate) : undefined;
const endDateParsed = endDate ? new Date(endDate) : undefined;
try {
const idsList = ids.split(",");
const assignments = await getAssignmentsForCorporates(req.session.user!.type, idsList, startDateParsed, endDateParsed);
res.status(200).json(assignments);
} catch (err: any) {
res.status(500).json({error: err.message});
}
}

View File

@@ -1,175 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import client from "@/lib/mongodb";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {uuidv4} from "@firebase/util";
import {Module} from "@/interfaces";
import {getExams} from "@/utils/exams.be";
import {Exam, InstructorGender, Variant} from "@/interfaces/exam";
import {capitalize, flatten, uniqBy} from "lodash";
import {User} from "@/interfaces/user";
import moment from "moment";
import {sendEmail} from "@/email";
import {release} from "os";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") return GET(req, res);
if (req.method === "POST") return POST(req, res);
res.status(404).json({ok: false});
}
async function GET(req: NextApiRequest, res: NextApiResponse) {
const docs = await db.collection("assignments").find({}).toArray();
res.status(200).json(docs);
}
interface ExamWithUser {
module: Module;
id: string;
assignee: string;
}
function getRandomIndex(arr: any[]): number {
const randomIndex = Math.floor(Math.random() * arr.length);
return randomIndex;
}
const generateExams = async (
generateMultiple: Boolean,
selectedModules: Module[],
assignees: string[],
userId: string,
variant?: Variant,
instructorGender?: InstructorGender,
): Promise<ExamWithUser[]> => {
if (generateMultiple) {
// for optimization purposes, it would be better to create a new endpoint that returned the answers for all users at once
const allExams = assignees.map(async (assignee) => {
const selectedModulePromises = selectedModules.map(async (module: Module) => {
try {
const exams: Exam[] = await getExams(db, module, "true", assignee, variant, instructorGender);
const exam = exams[getRandomIndex(exams)];
if (exam) {
return {module: exam.module, id: exam.id, assignee};
}
return null;
} catch (e) {
console.error(e);
return null;
}
}, []);
const newModules = await Promise.all(selectedModulePromises);
return newModules;
}, []);
const exams = flatten(await Promise.all(allExams)).filter((x) => x !== null) as ExamWithUser[];
return exams;
}
const selectedModulePromises = selectedModules.map(async (module: Module) => {
const exams: Exam[] = await getExams(db, module, "false", userId, variant, instructorGender);
const exam = exams[getRandomIndex(exams)];
if (exam) {
return {module: exam.module, id: exam.id};
}
return null;
});
const exams = await Promise.all(selectedModulePromises);
const examesFiltered = exams.filter((x) => x !== null) as ExamWithUser[];
return flatten(assignees.map((assignee) => examesFiltered.map((exam) => ({...exam, assignee}))));
};
async function POST(req: NextApiRequest, res: NextApiResponse) {
const {
examIDs,
selectedModules,
assignees,
// Generate multiple true would generate an unique exam for each user
// false would generate the same exam for all users
generateMultiple = false,
variant,
instructorGender,
...body
} = req.body as {
examIDs?: {id: string; module: Module}[];
selectedModules: Module[];
assignees: string[];
generateMultiple: Boolean;
name: string;
startDate: string;
endDate: string;
variant?: Variant;
instructorGender?: InstructorGender;
released: boolean;
};
const exams: ExamWithUser[] = !!examIDs
? examIDs.flatMap((e) => assignees.map((a) => ({...e, assignee: a})))
: await generateExams(generateMultiple, selectedModules, assignees, req.session.user!.id, variant, instructorGender);
if (exams.length === 0) {
res.status(400).json({ok: false, error: "No exams found for the selected modules"});
return;
}
const id = uuidv4();
await db.collection("assignments").insertOne({
id,
assigner: req.session.user?.id,
assignees,
results: [],
exams,
instructorGender,
...body,
});
res.status(200).json({ok: true, id});
for (const assigneeID of assignees) {
const assignee = await db.collection("users").findOne<User>({id: assigneeID});
if (!assignee) continue;
const name = body.name;
const teacher = req.session.user!;
const examModulesLabel = uniqBy(exams, (x) => x.module)
.map((x) => capitalize(x.module))
.join(", ");
const startDate = moment(body.startDate).format("DD/MM/YYYY - HH:mm");
const endDate = moment(body.endDate).format("DD/MM/YYYY - HH:mm");
await sendEmail(
"assignment",
{
user: {name: assignee.name},
assignment: {
name,
startDate,
endDate,
modules: examModulesLabel,
assigner: teacher.name,
id
},
environment: process.env.ENVIRONMENT,
},
[assignee.email],
"EnCoach - New Assignment!",
);
}
}

View File

@@ -1,70 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { FirebaseScrypt } from 'firebase-scrypt';
import { firebaseAuthScryptParams } from "@/firebase";
import crypto from 'crypto';
import axios from "axios";
import { getEntityWithRoles } from "@/utils/entities.be";
import { findBy } from "@/utils";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
return res.status(404).json({ ok: false });
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const maker = req.session.user;
if (!maker) {
return res.status(401).json({ ok: false, reason: "You must be logged in to make user!" });
}
const scrypt = new FirebaseScrypt(firebaseAuthScryptParams)
const users = req.body.users as {
email: string;
name: string;
type: string;
passport_id: string;
groupName?: string;
corporate?: string;
studentID?: string;
expiryDate?: string;
demographicInformation: {
country?: string;
passport_id?: string;
phone: string;
};
entity: { id: string, label: string }
entities: { id: string, role: string }[]
passwordHash: string | undefined;
passwordSalt: string | undefined;
}[];
const usersWithPasswordHashes = await Promise.all(users.map(async (user) => {
const currentUser = { ...user };
const salt = crypto.randomBytes(16).toString('base64');
const hash = await scrypt.hash(user.passport_id, salt);
const entity = await getEntityWithRoles(currentUser.entity!.id)
const defaultRole = findBy(entity?.roles || [], "isDefault", true)
currentUser.entities = [{ id: entity?.id || "", role: defaultRole?.id || "" }]
currentUser.email = currentUser.email.toLowerCase();
currentUser.passwordHash = hash;
currentUser.passwordSalt = salt;
return currentUser;
}));
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/user/import`, { makerID: maker.id, users: usersWithPasswordHashes }, {
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
});
return res.status(backendRequest.status).json(backendRequest.data)
}

View File

@@ -1,32 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import client from "@/lib/mongodb";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {uuidv4} from "@firebase/util";
const db = client.db(process.env.MONGODB_DB);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return GET(req, res);
if (req.method === "DELETE") return DELETE(req, res);
res.status(404).json({ok: false});
}
async function GET(req: NextApiRequest, res: NextApiResponse) {
const {id} = req.query;
const code = await db.collection("codes").findOne({ id: id as string });
res.status(200).json(code);
}
async function DELETE(req: NextApiRequest, res: NextApiResponse) {
const {id} = req.query;
const code = await db.collection("codes").findOne({ id: id as string });
if (!code) return res.status(404).json;
await db.collection("codes").deleteOne({ id: id as string });
res.status(200).json(code);
}

View File

@@ -1,28 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Code, } from "@/interfaces/user";
import { requestUser } from "@/utils/api";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
return res.status(404).json({ ok: false });
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user)
return res.status(401).json({ ok: false, reason: "You must be logged in!" })
const { entities } = req.query as { entities?: string[] };
if (entities)
return res.status(200).json(await db.collection("codes").find<Code>({ entity: { $in: Array.isArray(entities) ? entities : [entities] } }).toArray());
return res.status(200).json(await db.collection("codes").find<Code>({}).toArray());
}

View File

@@ -1,147 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Code, Group, Type } from "@/interfaces/user";
import { PERMISSIONS } from "@/constants/userPermissions";
import { prepareMailer, prepareMailOptions } from "@/email";
import { isAdmin } from "@/utils/users";
import { requestUser } from "@/utils/api";
import { doesEntityAllow } from "@/utils/permissions";
import { getEntity, getEntityWithRoles } from "@/utils/entities.be";
import { findBy } from "@/utils";
import { EntityWithRoles } from "@/interfaces/entity";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
if (req.method === "POST") return post(req, res);
if (req.method === "DELETE") return del(req, res);
return res.status(404).json({ ok: false });
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false, reason: "You must be logged in to generate a code!" });
return;
}
const { entity } = req.query as { entity?: string };
const snapshot = await db.collection("codes").find(entity ? { entity } : {}).toArray();
res.status(200).json(snapshot);
}
const generateAndSendCode = async (
code: string,
type: Type,
creator: string,
expiryDate: null | Date,
entity?: string,
info?: {
email: string; name: string; passport_id?: string
}) => {
if (!info) {
await db.collection("codes").insertOne({
code, type, creator, expiryDate, entity, creationDate: new Date().toISOString()
})
return true
}
const previousCode = await db.collection("codes").findOne<Code>({ email: info.email, entity })
const transport = prepareMailer();
const mailOptions = prepareMailOptions(
{
type,
code: previousCode ? previousCode.code : code,
environment: process.env.ENVIRONMENT,
},
[info.email.toLowerCase().trim()],
"EnCoach Registration",
"main",
);
try {
await transport.sendMail(mailOptions);
if (!previousCode) {
await db.collection("codes").insertOne({
code, type, creator, expiryDate, entity, name: info.name.trim(), email: info.email.trim().toLowerCase(),
...(info.passport_id ? { passport_id: info.passport_id.trim() } : {}),
creationDate: new Date().toISOString()
})
}
return true;
} catch (e) {
return false;
}
}
const countAvailableCodes = async (entity: EntityWithRoles) => {
const usedUp = await db.collection("codes").countDocuments({ entity: entity.id })
const total = entity.licenses
return total - usedUp
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false, reason: "You must be logged in to generate a code!" });
const { type, codes, infos, expiryDate, entity } = req.body as {
type: Type;
codes: string[];
infos?: { email: string; name: string; passport_id?: string, code: string }[];
expiryDate: null | Date;
entity?: string
};
if (!entity && !isAdmin(user))
return res.status(403).json({ ok: false, reason: "You must be an admin to generate a code without an entity!" });
const entityObj = entity ? await getEntityWithRoles(entity) : undefined
const isAllowed = entityObj ? doesEntityAllow(user, entityObj, 'create_code') : true
if (!isAllowed) return res.status(403).json({ ok: false, reason: "You do not have permissions to generate a code!" });
if (entityObj) {
const availableCodes = await countAvailableCodes(entityObj)
if (availableCodes < codes.length)
return res.status(400).json({
ok: false,
reason: `You only have ${availableCodes} codes available, while trying to create ${codes.length} codes`
})
}
const valid = []
for (const code of codes) {
const info = findBy(infos || [], 'code', code)
const isValid = await generateAndSendCode(code, type, user.id, expiryDate, entity, info)
valid.push(isValid)
}
return res.status(200).json({ ok: true, valid: valid.length });
}
async function del(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false, reason: "You must be logged in to generate a code!" });
return;
}
const codes = req.query.code as string[];
for (const code of codes) {
const snapshot = await db.collection("codes").findOne<Code>({ id: code as string });
if (!snapshot) continue;
await db.collection("codes").deleteOne({ id: snapshot.id });
}
res.status(200).json({ codes });
}

View File

@@ -1,78 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import client from "@/lib/mongodb";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {PERMISSIONS} from "@/constants/userPermissions";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
if (req.method === "DELETE") return del(req, res);
if (req.method === "PATCH") return patch(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {id} = req.query as {id: string};
const docSnap = await db.collection("discounts").findOne({id: id});
if (docSnap) {
res.status(200).json(docSnap);
} else {
res.status(404).json(undefined);
}
}
async function patch(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {id} = req.query as {id: string};
const docSnap = await db.collection("discounts").findOne({id: id});
if (docSnap) {
if (!["developer", "admin"].includes(req.session.user.type)) {
res.status(403).json({ok: false});
return;
}
await db.collection("discounts").updateOne({id: id}, {$set: {id: id, ...req.body}}, {upsert: true});
res.status(200).json({ok: true});
} else {
res.status(404).json({ok: false});
}
}
async function del(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {id} = req.query as {id: string};
const docSnap = await db.collection("discounts").findOne({id: id});
if (docSnap) {
if (!["developer", "admin"].includes(req.session.user.type)) {
res.status(403).json({ok: false});
return;
}
await db.collection("discounts").deleteOne({id: id});
res.status(200).json({ok: true});
} else {
res.status(404).json({ok: false});
}
}

View File

@@ -1,67 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Group } from "@/interfaces/user";
import { Discount, Package } from "@/interfaces/paypal";
import { v4 } from "uuid";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") await get(req, res);
if (req.method === "POST") await post(req, res);
if (req.method === "DELETE") return del(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const snapshot = await db.collection("discounts").find({}).toArray();
res.status(200).json(snapshot);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
if (!["developer", "admin"].includes(req.session.user!.type))
return res.status(403).json({
ok: false,
reason: "You do not have permission to create a new discount",
});
const body = req.body as Discount;
await db.collection("discounts").insertOne({ ...body });
res.status(200).json({ ok: true });
}
async function del(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res
.status(401)
.json({ ok: false, reason: "You must be logged in to generate a code!" });
return;
}
const discounts = req.query.discount as string[];
for (const discount of discounts) {
const snapshot = await db.collection("discounts").findOne({ id: discount as string });
if (!snapshot) continue;
await db.collection("discounts").deleteOne({ id: discount as string });
}
res.status(200).json({ discounts });
}

View File

@@ -1,94 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { deleteEntity, getEntity, getEntityWithRoles } from "@/utils/entities.be";
import client from "@/lib/mongodb";
import { Entity } from "@/interfaces/entity";
import { doesEntityAllow } from "@/utils/permissions";
import { getEntityUsers, getUser } from "@/utils/users.be";
import { requestUser } from "@/utils/api";
import { isAdmin } from "@/utils/users";
import { filterBy, mapBy } from "@/utils";
import { User } from "@/interfaces/user";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await get(req, res);
if (req.method === "PATCH") return await patch(req, res);
if (req.method === "DELETE") return await del(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const { id, showRoles } = req.query as { id: string; showRoles: string };
const entity = await (!!showRoles ? getEntityWithRoles : getEntity)(id);
res.status(200).json(entity);
}
async function del(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const { id } = req.query as { id: string };
const entity = await getEntityWithRoles(id)
if (!entity) return res.status(404).json({ ok: false })
if (!doesEntityAllow(user, entity, "delete_entity") && !["admin", "developer"].includes(user.type))
return res.status(403).json({ ok: false })
await deleteEntity(entity)
return res.status(200).json({ ok: true });
}
async function patch(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const { id } = req.query as { id: string };
if (!user.entities.map((x) => x.id).includes(id) && !isAdmin(user)) {
return res.status(403).json({ ok: false });
}
if (req.body.label) {
const entity = await db.collection<Entity>("entities").updateOne({ id }, { $set: { label: req.body.label } });
return res.status(200).json({ ok: entity.acknowledged });
}
if (req.body.licenses) {
const entity = await db.collection<Entity>("entities").updateOne({ id }, { $set: { licenses: req.body.licenses } });
return res.status(200).json({ ok: entity.acknowledged });
}
if (req.body.payment) {
const entity = await db.collection<Entity>("entities").updateOne({ id }, { $set: { payment: req.body.payment } });
return res.status(200).json({ ok: entity.acknowledged });
}
if (req.body.expiryDate !== undefined) {
const entity = await getEntity(id)
const result = await db.collection<Entity>("entities").updateOne({ id }, { $set: { expiryDate: req.body.expiryDate } });
const users = await getEntityUsers(id, 0, {
subscriptionExpirationDate: entity?.expiryDate,
$and: [
{ type: { $ne: "admin" } },
{ type: { $ne: "developer" } },
]
})
await db.collection<User>("users").updateMany({ id: { $in: mapBy(users, 'id') } }, { $set: { subscriptionExpirationDate: req.body.expiryDate } })
return res.status(200).json({ ok: result.acknowledged });
}
return res.status(200).json({ ok: true });
}

View File

@@ -1,65 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {countEntityUsers, getEntityUsers} from "@/utils/users.be";
import client from "@/lib/mongodb";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "get") return await get(req, res);
if (req.method === "PATCH") return await patch(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {id, onlyCount} = req.query as {id: string; onlyCount: string};
if (onlyCount) return res.status(200).json(await countEntityUsers(id));
const users = await getEntityUsers(id);
res.status(200).json(users);
}
async function patch(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {id} = req.query as {id: string};
const {add, members, role} = req.body as {add: boolean; members: string[]; role?: string};
if (add) {
await db.collection("users").updateMany(
{id: {$in: members}},
{
// @ts-expect-error
$push: {
entities: {id, role},
},
},
);
return res.status(204).end();
}
await db.collection("users").updateMany(
{id: {$in: members}},
{
// @ts-expect-error
$pull: {
entities: {id},
},
},
);
return res.status(204).end();
}

View File

@@ -1,29 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
import { Entity, WithEntities, WithEntity, WithLabeledEntities } from "@/interfaces/entity";
import { v4 } from "uuid";
import { mapBy } from "@/utils";
import { getEntitiesUsers, getUser, getUsers } from "@/utils/users.be";
import { Group, User } from "@/interfaces/user";
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
import { requestUser } from "@/utils/api";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await get(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const groups: WithEntity<Group>[] = ["admin", "developer"].includes(user.type)
? await getGroups()
: await getGroupsByEntities(mapBy(user.entities || [], 'id'))
res.status(200).json(groups);
}

View File

@@ -1,53 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { addUsersToEntity, addUserToEntity, createEntity, getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
import { Entity } from "@/interfaces/entity";
import { v4 } from "uuid";
import { requestUser } from "@/utils/api";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await get(req, res);
if (req.method === "POST") return await post(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const { showRoles } = req.query as { showRoles: string };
const getFn = showRoles ? getEntitiesWithRoles : getEntities;
if (["admin", "developer"].includes(user.type)) return res.status(200).json(await getFn());
res.status(200).json(await getFn(user.entities.map((x) => x.id)));
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
if (!["admin", "developer"].includes(user.type)) {
return res.status(403).json({ ok: false });
}
const entity: Entity = {
id: v4(),
label: req.body.label,
licenses: req.body.licenses
};
const members = req.body.members as string[] | undefined || []
console.log(members)
const roles = await createEntity(entity)
console.log(roles)
await addUserToEntity(user.id, entity.id, roles.admin.id)
if (members.length > 0) await addUsersToEntity(members, entity.id, roles.default.id)
return res.status(200).json(entity);
}

View File

@@ -1,63 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be";
import { Entity, EntityWithRoles, WithEntities, WithLabeledEntities } from "@/interfaces/entity";
import { v4 } from "uuid";
import { mapBy } from "@/utils";
import { getEntitiesUsers, getUser, getUsers } from "@/utils/users.be";
import { User } from "@/interfaces/user";
import { findAllowedEntities } from "@/utils/permissions";
import { RolePermission } from "@/resources/entityPermissions";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await get(req, res);
}
const labelUserEntity = (u: User, entities: EntityWithRoles[]) => ({
...u, entities: (u.entities || []).map((e) => {
const entity = entities.find((x) => x.id === e.id)
if (!entity) return e
const role = entity.roles.find((x) => x.id === e.role)
return { id: e.id, label: entity.label, role: e.role, roleLabel: role?.label }
})
})
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) return res.status(401).json({ ok: false });
const user = await getUser(req.session.user.id)
if (!user) return res.status(401).json({ ok: false });
const { type } = req.query as { type: string }
const entityIDs = mapBy(user.entities || [], 'id')
const entities = await getEntitiesWithRoles(entityIDs)
const isAdmin = ["admin", "developer"].includes(user.type)
const filter = !type ? undefined : { type }
const users = isAdmin
? await getUsers(filter)
: await getEntitiesUsers(mapBy(entities, 'id') as string[], filter)
const filteredUsers = users.map((u) => {
if (isAdmin) return labelUserEntity(u, entities)
if (!isAdmin && ["admin", "developer", "agent"].includes(user.type)) return undefined
const userEntities = mapBy(u.entities || [], 'id')
const sameEntities = entities.filter(e => userEntities.includes(e.id))
const permission = `view_${u.type}s` as RolePermission
const allowedEntities = findAllowedEntities(user, sameEntities, permission)
if (allowedEntities.length === 0) return undefined
return labelUserEntity(u, allowedEntities)
}).filter(x => !!x) as WithLabeledEntities<User>[]
res.status(200).json(filteredUsers);
}

View File

@@ -1,89 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { UserSolution } from "@/interfaces/exam";
import { speakingReverseMarking, writingReverseMarking } from "@/utils/score";
import { Stat } from "@/interfaces/user";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
try {
return await getSessionEvals(req, res);
} catch (error) {
console.error(error);
res.status(500).json({ ok: false });
}
}
function formatSolutionWithEval(userSolution: UserSolution | Stat, evaluation: any) {
if (userSolution.type === 'writing') {
return {
...userSolution,
solutions: [{
...userSolution.solutions[0],
evaluation: evaluation.result
}],
score: {
correct: writingReverseMarking[evaluation.result.overall],
total: 100,
missing: 0
},
isDisabled: false
};
}
if (userSolution.type === 'speaking' || userSolution.type === 'interactiveSpeaking') {
return {
...userSolution,
solutions: [{
...userSolution.solutions[0],
...(
userSolution.type === 'speaking'
? { fullPath: evaluation.result.fullPath }
: { answer: evaluation.result.answer }
),
evaluation: evaluation.result
}],
score: {
correct: speakingReverseMarking[evaluation.result.overall || 0] || 0,
total: 100,
missing: 0
},
isDisabled: false
};
}
return {
solution: userSolution,
evaluation
};
}
async function getSessionEvals(req: NextApiRequest, res: NextApiResponse) {
const { sessionId, userId, stats } = req.body;
const completedEvals = await db.collection("evaluation").find({
session_id: sessionId,
user: userId,
status: "completed"
}).toArray();
const evalsByExercise = new Map(
completedEvals.map(e => [e.exercise_id, e])
);
const statsWithEvals = stats
.filter((solution: UserSolution | Stat) => evalsByExercise.has(solution.exercise))
.map((solution: UserSolution | Stat) =>
formatSolutionWithEval(solution, evalsByExercise.get(solution.exercise)!)
);
res.status(200).json(statsWithEvals);
}

View File

@@ -1,133 +1,6 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios from "axios";
import formidable from "formidable-serverless";
import fs from "fs";
import FormData from 'form-data';
import client from "@/lib/mongodb";
import {proxyStreamToOdoo} from "@/lib/odoo";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const form = formidable({ keepExtensions: true });
await form.parse(req, async (err: any, fields: any, files: any) => {
if (err) {
console.error('Error parsing form:', err);
res.status(500).json({ ok: false, error: 'Failed to parse form data' });
return;
}
try {
const formData = new FormData();
if (!fields.userId || !fields.sessionId || !fields.exerciseId || !fields.task) {
throw new Error('Missing required fields');
}
formData.append('userId', fields.userId);
formData.append('sessionId', fields.sessionId);
formData.append('exerciseId', fields.exerciseId);
for (const fileKey of Object.keys(files)) {
const indexMatch = fileKey.match(/^audio_(\d+)$/);
if (!indexMatch) {
console.warn(`Skipping invalid file key: ${fileKey}`);
continue;
}
const index = indexMatch[1];
const questionKey = `question_${index}`;
const audioFile = files[fileKey];
if (!audioFile || !audioFile.path) {
throw new Error(`Invalid audio file for ${fileKey}`);
}
if (!fields[questionKey]) {
throw new Error(`Missing question for audio ${index}`);
}
try {
const buffer = fs.readFileSync(audioFile.path);
formData.append(`audio_${index}`, buffer, `audio_${index}.wav`);
formData.append(questionKey, fields[questionKey]);
fs.rmSync(audioFile.path);
} catch (fileError) {
console.error(`Error processing file ${fileKey}:`, fileError);
throw new Error(`Failed to process audio file ${index}`);
}
}
// Check if there is one eval for the current exercise
const previousEval = await db.collection("evaluation").findOne({
user: fields.userId,
session_id: fields.sessionId,
exercise_id: fields.exerciseId,
})
// If there is delete it
if (previousEval) {
await db.collection("evaluation").deleteOne({
user: fields.userId,
session_id: fields.sessionId,
exercise_id: fields.exerciseId,
})
}
// Insert the new eval for the backend to place it's result
await db.collection("evaluation").insertOne(
{
user: fields.userId,
session_id: fields.sessionId,
exercise_id: fields.exerciseId,
type: "speaking_interactive",
task: fields.task,
status: "pending"
}
);
await axios.post(
`${process.env.BACKEND_URL}/grade/speaking/${fields.task}`,
formData,
{
headers: {
...formData.getHeaders(),
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
}
);
res.status(200).json({ ok: true });
} catch (error) {
console.error('Error processing request:', error);
res.status(500).json({
ok: false,
error: 'Internal server error'
});
Object.keys(files).forEach(fileKey => {
const audioFile = files[fileKey];
if (audioFile && audioFile.path && fs.existsSync(audioFile.path)) {
try {
fs.rmSync(audioFile.path);
} catch (cleanupError) {
console.error(`Failed to clean up temp file ${audioFile.path}:`, cleanupError);
}
}
});
}
});
}
export default proxyStreamToOdoo("/api/evaluate/interactiveSpeaking");
export const config = {
api: {

View File

@@ -1,102 +1,6 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios from "axios";
import formidable from "formidable-serverless";
import fs from "fs";
import FormData from 'form-data';
import client from "@/lib/mongodb";
import {proxyStreamToOdoo} from "@/lib/odoo";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const form = formidable({ keepExtensions: true });
await form.parse(req, async (err: any, fields: any, files: any) => {
if (err) {
console.log(err);
res.status(500).json({ ok: false });
return;
}
try {
const formData = new FormData();
formData.append('userId', fields.userId);
formData.append('sessionId', fields.sessionId);
formData.append('exerciseId', fields.exerciseId);
formData.append('question_1', fields.question);
const audioFile = files.audio;
if (!audioFile || !audioFile.path) {
throw new Error('Audio file not found in request');
}
const buffer = fs.readFileSync(audioFile.path);
formData.append('audio_1', buffer, 'audio_1.wav');
fs.rmSync(audioFile.path);
// Check if there is one eval for the current exercise
const previousEval = await db.collection("evaluation").findOne({
user: fields.userId,
session_id: fields.sessionId,
exercise_id: fields.exerciseId,
})
// If there is delete it
if (previousEval) {
await db.collection("evaluation").deleteOne({
user: fields.userId,
session_id: fields.sessionId,
exercise_id: fields.exerciseId,
})
}
// Insert the new eval for the backend to place it's result
await db.collection("evaluation").insertOne(
{
user: fields.userId,
session_id: fields.sessionId,
exercise_id: fields.exerciseId,
type: "speaking",
task: 2,
status: "pending"
}
);
await axios.post(
`${process.env.BACKEND_URL}/grade/speaking/2`,
formData,
{
headers: {
...formData.getHeaders(),
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
}
);
res.status(200).json({ ok: true });
} catch (error) {
console.error('Error:', error);
if (files.audio?.path && fs.existsSync(files.audio.path)) {
try {
fs.rmSync(files.audio.path);
} catch (e) {
console.error('Failed to cleanup file:', e);
}
}
res.status(500).json({ ok: false });
}
});
}
export default proxyStreamToOdoo("/api/evaluate/speaking");
export const config = {
api: {

View File

@@ -1,110 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
}
type Query = {
op: string;
sessionId: string;
userId: string;
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
return res.status(401).json({ ok: false });
}
const { sessionId, userId, op } = req.query as Query;
switch (op) {
case 'pending':
return getPendingEvaluation(userId, sessionId, res);
case 'disabled':
return getSessionsWIthDisabledWithPending(userId, res);
default:
return res.status(400).json({
ok: false,
});
}
}
async function getPendingEvaluation(
userId: string,
sessionId: string,
res: NextApiResponse
) {
const singleEval = await db.collection("evaluation").findOne({
session_id: sessionId,
user: userId,
status: "pending",
});
return res.status(200).json({ hasPendingEvaluation: singleEval !== null });
}
async function getSessionsWIthDisabledWithPending(
userId: string,
res: NextApiResponse
) {
const sessions = await db.collection("stats")
.aggregate([
{
$match: {
user: userId,
disabled: true
}
},
{
$project: {
_id: 0,
session: 1
}
},
{
$lookup: {
from: "evaluation",
let: { sessionId: "$session" },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ["$session", "$$sessionId"] },
{ $eq: ["$user", userId] },
{ $eq: ["$status", "pending"] }
]
}
}
},
{
$project: {
_id: 1
}
}
],
as: "pendingEvals"
}
},
{
$match: {
"pendingEvals.0": { $exists: true }
}
},
{
$group: {
id: "$session"
}
}
]).toArray();
return res.status(200).json({
sessions: sessions.map(s => s.id)
});
}

View File

@@ -1,65 +1,3 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios from "axios";
import client from "@/lib/mongodb";
import {proxyToOdoo} from "@/lib/odoo";
const db = client.db(process.env.MONGODB_DB);
interface Body {
userId: string;
sessionId: string;
question: string;
answer: string;
exerciseId: string;
task: 1 | 2;
}
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const body = req.body as Body;
const taskNumber = body.task.toString() !== "1" && body.task.toString() !== "2" ? "1" : body.task.toString();
// Check if there is one eval for the current exercise
const previousEval = await db.collection("evaluation").findOne({
user: body.userId,
session_id: body.sessionId,
exercise_id: body.exerciseId,
})
// If there is delete it
if (previousEval) {
await db.collection("evaluation").deleteOne({
user: body.userId,
session_id: body.sessionId,
exercise_id: body.exerciseId,
})
}
// Insert the new eval for the backend to place it's result
await db.collection("evaluation").insertOne(
{
user: body.userId,
session_id: body.sessionId,
exercise_id: body.exerciseId,
type: "writing",
task: body.task,
status: "pending"
}
);
await axios.post(`${process.env.BACKEND_URL}/grade/writing/${taskNumber}`, body, {
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
});
res.status(200).json({ ok: true });
}
export default proxyToOdoo("/api/evaluate/writing");

View File

@@ -1,82 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import client from "@/lib/mongodb";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {PERMISSIONS} from "@/constants/userPermissions";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
if (req.method === "PATCH") return patch(req, res);
if (req.method === "DELETE") return del(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {module, id} = req.query as {module: string; id: string};
const docSnap = await db.collection(module).findOne({ id: id});
if (docSnap) {
res.status(200).json({
...docSnap,
module,
});
} else {
res.status(404).json(undefined);
}
}
async function patch(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {module, id} = req.query as {module: string; id: string};
const docSnap = await db.collection(module).findOne({ id: id});
if (docSnap) {
await db.collection(module).updateOne(
{ id: id},
{ $set: { id: id, ...req.body }},
{ upsert: true }
);
res.status(200).json({ok: true});
} else {
res.status(404).json({ok: false});
}
}
async function del(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {module, id} = req.query as {module: string; id: string};
const docSnap = await db.collection(module).findOne({ id: id});
if (docSnap) {
if (!PERMISSIONS.examManagement.delete.includes(req.session.user.type)) {
res.status(403).json({ok: false});
return;
}
await db.collection(module).deleteOne({ id: id });
res.status(200).json({ok: true});
} else {
res.status(404).json({ok: false});
}
}

View File

@@ -1,79 +0,0 @@
export const config = {
api: {
bodyParser: false
}
};
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios from "axios";
import formidable from 'formidable';
import FormData from 'form-data';
import { readFileSync } from 'fs';
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
return res.status(404).json({ ok: false });
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) return res.status(401).json({ ok: false });
try {
const form = formidable({
multiples: true,
});
const [_, files] = await form.parse(req);
const formData = new FormData();
if (files.exercises?.[0]) {
const file = files.exercises[0];
const buffer = readFileSync(file.filepath);
formData.append('exercises', buffer, {
filename: file.originalFilename!,
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
knownLength: buffer.length
});
}
if (files.solutions?.[0]) {
const file = files.solutions[0];
const buffer = readFileSync(file.filepath);
formData.append('solutions', buffer, {
filename: file.originalFilename!,
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
knownLength: buffer.length
});
}
const result = await axios.post(
`${process.env.BACKEND_URL}/${req.query.module}/import${req.query.module == "level" ? '/': ''}`,
formData,
{
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
...formData.getHeaders()
}
}
);
return res.status(200).json(result.data);
} catch (error) {
console.error(error);
return res.status(500).json({
error: 'Upload failed',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
}

View File

@@ -1,179 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { Module } from "@/interfaces";
import { Exam, ExamBase, InstructorGender, LevelExam, ListeningExam, ReadingExam, SpeakingExam, Variant } from "@/interfaces/exam";
import { createApprovalWorkflowOnExamCreation } from "@/lib/createWorkflowsOnExamCreation";
import client from "@/lib/mongodb";
import { sessionOptions } from "@/lib/session";
import { mapBy } from "@/utils";
import { requestUser } from "@/utils/api";
import { getApprovalWorkflowsByExamId, updateApprovalWorkflows } from "@/utils/approval.workflows.be";
import { generateExamDifferences } from "@/utils/exam.differences";
import { getExams } from "@/utils/exams.be";
import { isAdmin } from "@/utils/users";
import { uuidv4 } from "@firebase/util";
import { access } from "fs";
import { withIronSessionApiRoute } from "iron-session/next";
import type { NextApiRequest, NextApiResponse } from "next";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
// Temporary: Adding UUID here but later move to backend.
function addUUIDs(exam: ReadingExam | ListeningExam | LevelExam): ExamBase {
const arraysToUpdate = ["solutions", "words", "questions", "sentences", "options"];
exam.parts = exam.parts.map((part) => {
const updatedExercises = part.exercises.map((exercise: any) => {
arraysToUpdate.forEach((arrayName) => {
if (exercise[arrayName] && Array.isArray(exercise[arrayName])) {
exercise[arrayName] = exercise[arrayName].map((item: any) => (item.uuid ? item : { ...item, uuid: uuidv4() }));
}
});
return exercise;
});
return { ...part, exercises: updatedExercises };
});
return exam;
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await GET(req, res);
if (req.method === "POST") return await POST(req, res);
res.status(404).json({ ok: false });
}
async function GET(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const { module, avoidRepeated, variant, instructorGender } = req.query as {
module: Module;
avoidRepeated: string;
variant?: Variant;
instructorGender?: InstructorGender;
};
const exams: Exam[] = await getExams(db, module, avoidRepeated, req.session.user.id, variant, instructorGender);
res.status(200).json(exams);
}
async function POST(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res);
if (!user) return res.status(401).json({ ok: false });
const { module } = req.query as { module: string };
const session = client.startSession();
const entities = isAdmin(user) ? [] : mapBy(user.entities, "id");
try {
let exam = {
access: "public", // default access is public
...req.body,
module: module,
entities,
createdBy: user.id,
createdAt: new Date().toISOString(),
};
// Temporary: Adding UUID here but later move to backend.
exam = addUUIDs(exam);
let responseStatus: number;
let responseMessage: string;
await session.withTransaction(async () => {
const docSnap = await db.collection(module).findOne<ExamBase>({ id: req.body.id }, { session });
// Check whether the id of the exam matches another exam with different
// owners, throw exception if there is, else allow editing
const existingExamOwners = docSnap?.owners ?? [];
const newExamOwners = exam.owners ?? [];
const ownersSet = new Set(existingExamOwners);
if (docSnap !== null && (existingExamOwners.length !== newExamOwners.length || !newExamOwners.every((e: string) => ownersSet.has(e)))) {
throw new Error("Name already exists");
}
if (exam.requiresApproval === true) {
exam.access = "confidential";
}
await db.collection(module).updateOne(
{ id: req.body.id },
{ $set: { id: req.body.id, ...exam } },
{
upsert: true,
session,
}
);
// if it doesn't enter the next if condition it means the exam was updated and not created, so we can send this response.
responseStatus = 200;
responseMessage = `Successfully updated exam with ID: "${exam.id}"`;
// create workflow only if exam is being created for the first time
if (docSnap === null) {
try {
if (exam.requiresApproval === false) {
responseStatus = 200;
responseMessage = `Successfully created exam "${exam.id}" and skipped Approval Workflow due to user request.`;
} else if (isAdmin(user)) {
responseStatus = 200;
responseMessage = `Successfully created exam "${exam.id}" and skipped Approval Workflow due to admin rights.`;
} else {
const { successCount, totalCount } = await createApprovalWorkflowOnExamCreation(exam.createdBy, exam.entities, exam.id, module);
if (successCount === totalCount) {
responseStatus = 200;
responseMessage = `Successfully created exam "${exam.id}" and started its Approval Workflow.`;
} else if (successCount > 0) {
responseStatus = 207;
responseMessage = `Successfully created exam with ID: "${exam.id}" but was not able to start/find an Approval Workflow for all the author's entities.`;
} else {
responseStatus = 207;
responseMessage = `Successfully created exam with ID: "${exam.id}" but skipping approval process because no approval workflow was found configured for the exam author.`;
}
}
} catch (error) {
console.error("Workflow creation error:", error);
responseStatus = 207;
responseMessage = `Successfully created exam with ID: "${exam.id}" but something went wrong while creating the Approval Workflow(s).`;
}
} else {
// if exam was updated, log the updates
const approvalWorkflows = await getApprovalWorkflowsByExamId(exam.id);
if (approvalWorkflows) {
const differences = generateExamDifferences(docSnap as Exam, exam as Exam);
if (differences) {
approvalWorkflows.forEach((workflow) => {
const currentStepIndex = workflow.steps.findIndex((step) => !step.completed || step.rejected);
if (workflow.steps[currentStepIndex].examChanges === undefined) {
workflow.steps[currentStepIndex].examChanges = [...differences];
} else {
workflow.steps[currentStepIndex].examChanges!.push(...differences);
}
});
await updateApprovalWorkflows("active-workflows", approvalWorkflows);
}
}
}
res.status(responseStatus).json({
message: responseMessage,
});
});
} catch (error) {
console.error("Transaction failed: ", error);
res.status(500).json({ ok: false, error: (error as any).message });
} finally {
session.endSession();
}
}

View File

@@ -1,21 +1,3 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios from "axios";
import {proxyToOdoo} from "@/lib/odoo";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
return res.status(404).json({ ok: false });
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) return res.status(401).json({ ok: false });
const result = await axios.get(`${process.env.BACKEND_URL}/speaking/avatars`, {
headers: { Authorization: `Bearer ${process.env.BACKEND_JWT}` },
});
res.status(200).json(result.data);
}
export default proxyToOdoo("/api/exam/speaking/avatars");

View File

@@ -1,64 +1,70 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios from "axios";
import queryToURLSearchParams from "@/utils/query.to.url.params";
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {ODOO_URL} from "@/lib/odoo";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
if (req.method === "POST") return post(req, res);
if (!req.session.token) return res.status(401).json({ok: false});
return res.status(404).json({ ok: false });
}
const modules = Array.isArray(req.query.module) ? req.query.module : [req.query.module];
let endpoint = modules.join("/");
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) return res.status(401).json({ ok: false });
if (req.method === "GET") {
const url = new URL(`${ODOO_URL}/api/exam/${endpoint}`);
Object.entries(req.query).forEach(([k, v]) => {
if (k !== "module" && v) url.searchParams.set(k, String(v));
});
const queryParams = queryToURLSearchParams(req);
const endpoint = queryParams.getAll('module').join("/");
queryParams.delete('module');
const result = await axios.get(`${process.env.BACKEND_URL}/${endpoint}${Array.from(queryParams.entries()).length > 0 ? `?${queryParams.toString()}` : ""}`, {
headers: { Authorization: `Bearer ${process.env.BACKEND_JWT}` },
});
res.status(200).json(result.data);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) return res.status(401).json({ ok: false });
const queryParams = queryToURLSearchParams(req);
let endpoint = queryParams.getAll('module').join("/");
if (endpoint.startsWith("level")) {
endpoint = "level/"
} else if (endpoint.startsWith("listening")) {
endpoint = "listening/"
} else if (endpoint.startsWith("reading")) {
endpoint = "reading/"
try {
const resp = await fetch(url.toString(), {
headers: {Authorization: `Bearer ${req.session.token}`},
});
const data = await resp.json();
return res.status(resp.status).json(data);
} catch {
return res.status(502).json({error: "Backend unavailable"});
}
}
queryParams.delete('module');
const queryString = queryParams.toString();
if (req.method === "POST") {
if (endpoint.startsWith("level")) endpoint = "level/";
else if (endpoint.startsWith("listening")) endpoint = "listening/";
else if (endpoint.startsWith("reading")) endpoint = "reading/";
const hasFiles = req.headers['content-type']?.startsWith('multipart/form-data');
const hasFiles = req.headers["content-type"]?.startsWith("multipart/form-data");
const path = `/api/exam/${endpoint}${hasFiles ? "/attachment" : ""}`;
// https://github.com/vercel/next.js/discussions/36153#discussioncomment-3029675
// This just proxies the request
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(Buffer.from(chunk));
const body = Buffer.concat(chunks);
const { data } = await axios.post(
`${process.env.BACKEND_URL}/${endpoint}${hasFiles ? '/attachment' : ''}${queryString.length > 0 ? `?${queryString}` : ''}`, req, {
responseType: "stream",
headers: {
"Content-Type": req.headers["content-type"],
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
});
data.pipe(res);
try {
const headers: Record<string, string> = {
Authorization: `Bearer ${req.session.token}`,
};
if (req.headers["content-type"]) {
headers["Content-Type"] = req.headers["content-type"];
}
const resp = await fetch(`${ODOO_URL}${path}`, {
method: "POST",
headers,
body,
});
const contentType = resp.headers.get("content-type") || "";
res.status(resp.status);
res.setHeader("Content-Type", contentType);
const buf = Buffer.from(await resp.arrayBuffer());
return res.send(buf);
} catch {
return res.status(502).json({error: "Backend unavailable"});
}
}
return res.status(404).json({ok: false});
}
export const config = {

View File

@@ -1,59 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { flatten } from "lodash";
import { AccessType, Exam } from "@/interfaces/exam";
import { MODULE_ARRAY } from "@/utils/moduleUtils";
import { requestUser } from "../../../utils/api";
import { mapBy } from "../../../utils";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await GET(req, res);
res.status(404).json({ ok: false });
}
async function GET(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const user = await requestUser(req, res)
if (!user)
return res.status(401).json({ ok: false, reason: "You must be logged in!" })
const isAdmin = ["admin", "developer"].includes(user.type)
const { entities = [] } = req.query as { access?: AccessType, entities?: string[] | string };
let entitiesToFetch = Array.isArray(entities) ? entities : entities ? [entities] : []
if (!isAdmin) {
const userEntitiesIDs = mapBy(user.entities || [], 'id')
entitiesToFetch = entities ? entitiesToFetch.filter((entity): entity is string => entity ? userEntitiesIDs.includes(entity) : false) : userEntitiesIDs
if ((entitiesToFetch.length ?? 0) === 0) {
res.status(200).json([])
return
}
}
const moduleExamsPromises = MODULE_ARRAY.map(async (module) => {
const snapshot = await db.collection(module).find<Exam>({
isDiagnostic: false, ...(isAdmin && (entitiesToFetch.length ?? 0) === 0 ? {
} : {
entity: { $in: entitiesToFetch }
})
}).toArray();
return snapshot.map((doc) => ({
...doc,
module,
}));
});
const moduleExams = await Promise.all(moduleExamsPromises);
res.status(200).json(flatten(moduleExams));
}

View File

@@ -1,59 +1,41 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios from "axios";
import queryToURLSearchParams from "@/utils/query.to.url.params";
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {ODOO_URL} from "@/lib/odoo";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
if (req.method !== "POST") return res.status(404).json({ok: false});
if (!req.session.token) return res.status(401).json({ok: false});
return res.status(404).json({ ok: false });
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) return res.status(401).json({ ok: false });
const queryParams = queryToURLSearchParams(req);
let endpoint = queryParams.getAll('module').join("/");
if (endpoint === "listening") {
const response = await axios.post(
`${process.env.BACKEND_URL}/${endpoint}/media`,
req.body,
{
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
Accept: 'audio/mpeg'
},
responseType: 'arraybuffer',
}
);
res.writeHead(200, {
'Content-Type': 'audio/mpeg',
'Content-Length': response.data.length
});
res.end(response.data);
return;
}
if (endpoint === "speaking") {
const response = await axios.post(
`${process.env.BACKEND_URL}/${endpoint}/media`,
req.body,
{
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
}
}
);
res.status(200).json(response.data);
return;
}
return res.status(405).json({ "error": "Method not allowed."});
const modules = Array.isArray(req.query.module) ? req.query.module : [req.query.module];
const endpoint = modules[0];
try {
const resp = await fetch(`${ODOO_URL}/api/exam/${endpoint}/media`, {
method: "POST",
headers: {
Authorization: `Bearer ${req.session.token}`,
"Content-Type": req.headers["content-type"] || "application/json",
},
body: typeof req.body === "object" ? JSON.stringify(req.body) : req.body,
});
const contentType = resp.headers.get("content-type") || "";
if (contentType.includes("audio")) {
const buf = Buffer.from(await resp.arrayBuffer());
res.writeHead(200, {
"Content-Type": contentType,
"Content-Length": buf.length,
});
return res.end(buf);
}
const data = await resp.json();
return res.status(resp.status).json(data);
} catch {
return res.status(502).json({error: "Backend unavailable"});
}
}

View File

@@ -1,36 +1,31 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios from "axios";
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {ODOO_URL} from "@/lib/odoo";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
if (req.method !== "POST") return res.status(404).json({ok: false});
if (!req.session.token) return res.status(401).json({ok: false});
return res.status(404).json({ ok: false });
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) return res.status(401).json({ ok: false });
const response = await axios.post(
`${process.env.BACKEND_URL}/listening/instructions`,
req.body,
{
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
Accept: 'audio/mpeg'
},
responseType: 'arraybuffer',
}
);
res.writeHead(200, {
'Content-Type': 'audio/mpeg',
'Content-Length': response.data.length
});
res.end(response.data);
return;
try {
const resp = await fetch(`${ODOO_URL}/api/exam/listening/instructions`, {
method: "POST",
headers: {
Authorization: `Bearer ${req.session.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(req.body),
});
const buf = Buffer.from(await resp.arrayBuffer());
res.writeHead(200, {
"Content-Type": "audio/mpeg",
"Content-Length": buf.length,
});
res.end(buf);
} catch {
res.status(502).json({error: "Backend unavailable"});
}
}

View File

@@ -1,26 +1,3 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios from "axios";
export default withIronSessionApiRoute(handler, sessionOptions);
import {proxyToOdoo} from "@/lib/odoo";
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
return res.status(404).json({ ok: false });
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) return res.status(401).json({ ok: false });
const { videoId } = req.query;
const response = await axios.get(
`${process.env.BACKEND_URL}/speaking/media/${videoId}`,
{
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
}
}
);
return res.status(200).json(response.data);
}
export default proxyToOdoo();

View File

@@ -1,30 +1,29 @@
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import axios from "axios";
import queryToURLSearchParams from "@/utils/query.to.url.params";
import {ODOO_URL} from "@/lib/odoo";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
if (req.method !== "POST") return res.status(404).json({ok: false});
if (!req.session.token) return res.status(401).json({ok: false});
return res.status(404).json({ok: false});
const examModule = req.query.module as string;
try {
const resp = await fetch(`${ODOO_URL}/api/exam/${examModule}`, {
method: "POST",
headers: {
Authorization: `Bearer ${req.session.token}`,
"Content-Type": req.headers["content-type"] || "application/json",
},
body: typeof req.body === "object" ? JSON.stringify(req.body) : req.body,
});
const data = await resp.json();
return res.status(resp.status).json(data);
} catch {
return res.status(502).json({error: "Backend unavailable"});
}
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) return res.status(401).json({ok: false});
const queryParams = queryToURLSearchParams(req);
const endpoint = queryParams.getAll('module').join("/");
const result = await axios.post(`${process.env.BACKEND_URL}/${endpoint}`,
req.body,
{
headers: {Authorization: `Bearer ${process.env.BACKEND_JWT}`},
},
);
res.status(200).json(result.data);
}

View File

@@ -1,56 +1,3 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { app } from "@/firebase";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { CorporateUser, Group } from "@/interfaces/user";
import { Discount, Package } from "@/interfaces/paypal";
import { v4 } from "uuid";
import { checkAccess } from "@/utils/permissions";
import { CEFR_STEPS } from "@/resources/grading";
import { getCorporateUser } from "@/resources/user";
import { getUserCorporate } from "@/utils/groups.be";
import { Grading } from "@/interfaces";
import { getGroupsForUser } from "@/utils/groups.be";
import { uniq } from "lodash";
import { getSpecificUsers, getUser } from "@/utils/users.be";
import client from "@/lib/mongodb";
import { getGradingSystemByEntity } from "@/utils/grading.be";
import {proxyToOdoo} from "@/lib/odoo";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") await get(req, res);
if (req.method === "POST") await post(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const entity = req.query.entity as string
const gradingSystem = await getGradingSystemByEntity(entity);
return res.status(200).json(gradingSystem);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
if (!checkAccess(req.session.user, ["admin", "developer", "mastercorporate", "corporate"]))
return res.status(403).json({
ok: false,
reason: "You do not have permission to create a new grading system",
});
const body = req.body as Grading;
await db.collection("grading").updateOne({ entity: body.entity }, { $set: body }, { upsert: true });
res.status(200).json({ ok: true });
}
export default proxyToOdoo("/api/grading/multiple");

View File

@@ -1,48 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { app } from "@/firebase";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { CorporateUser, Group } from "@/interfaces/user";
import { Discount, Package } from "@/interfaces/paypal";
import { v4 } from "uuid";
import { checkAccess } from "@/utils/permissions";
import { CEFR_STEPS } from "@/resources/grading";
import { getCorporateUser } from "@/resources/user";
import { getUserCorporate } from "@/utils/groups.be";
import { Grading, Step } from "@/interfaces";
import { getGroupsForUser } from "@/utils/groups.be";
import { uniq } from "lodash";
import { getSpecificUsers, getUser } from "@/utils/users.be";
import client from "@/lib/mongodb";
import { getGradingSystemByEntity } from "@/utils/grading.be";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") await post(req, res);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
if (!checkAccess(req.session.user, ["admin", "developer", "mastercorporate", "corporate"]))
return res.status(403).json({
ok: false,
reason: "You do not have permission to create a new grading system",
});
const body = req.body as {
entities: string[]
steps: Step[];
};
await db.collection("grading").updateMany({ entity: { $in: body.entities } }, { $set: { steps: body.steps } }, { upsert: true });
res.status(200).json({ ok: true });
}

View File

@@ -1,90 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import client from "@/lib/mongodb";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Group} from "@/interfaces/user";
import {updateExpiryDateOnGroup} from "@/utils/groups.be";
import { requestUser } from "@/utils/api";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await get(req, res);
if (req.method === "DELETE") return await del(req, res);
if (req.method === "PATCH") return await patch(req, res);
res.status(404).json(undefined);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const {id} = req.query as {id: string};
const snapshot = await db.collection("groups").findOne({id: id});
if (snapshot) {
res.status(200).json({...snapshot});
} else {
res.status(404).json(undefined);
}
}
async function del(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const {id} = req.query as {id: string};
const group = await db.collection("groups").findOne<Group>({id: id});
if (!group) {
res.status(404);
return;
}
if (user.type === "admin" || user.type === "developer" || user.id === group.admin) {
await db.collection("groups").deleteOne({id: id});
res.status(200).json({ok: true});
return;
}
res.status(403).json({ok: false});
}
async function patch(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const {id} = req.query as {id: string};
const group = await db.collection("groups").findOne<Group>({id: id});
if (!group) {
res.status(404);
return;
}
if (
user.type === "admin" ||
user.type === "developer" ||
user.type === "mastercorporate" ||
user.type === "corporate" ||
user.id === group.admin
) {
if ("participants" in req.body && req.body.participants.length > 0) {
const newParticipants = (req.body.participants as string[]).filter((x) => !group.participants.includes(x));
await Promise.all(newParticipants.map(async (p) => await updateExpiryDateOnGroup(p, group.admin)));
}
await db.collection("groups").updateOne({id}, {$set: {id, ...req.body}}, {upsert: true});
res.status(200).json({ok: true});
return;
}
res.status(403).json({ok: false});
}

View File

@@ -1,115 +0,0 @@
import { sessionOptions } from '@/lib/session';
import { withIronSessionApiRoute } from 'iron-session/next';
import type { NextApiRequest, NextApiResponse } from 'next'
import client from "@/lib/mongodb";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { op } = req.query
if (req.method === 'GET') {
switch (op) {
default:
res.status(400).json({ error: 'Invalid operation!' })
}
}
else if (req.method === 'POST') {
switch (op) {
case 'existantGroupIds':
res.status(200).json(await existantGroupIds(req.body.names));
break;
case 'getIds':
res.status(200).json(await getIds(req.body));
break;
case 'deletePriorEntitiesGroups':
await deletePriorEntitiesGroups(req.body);
res.status(200).json({ ok: true });
break;
default:
res.status(400).json({ error: 'Invalid operation!' })
}
} else {
res.status(400).end(`Method ${req.method} Not Allowed`)
}
}
async function getIds(body: any): Promise<Record<string, string>> {
const { names, userEmails } = body;
const existingGroups: any[] = await db.collection('groups')
.find({ name: { $in: names } })
.project({ name: 1, id: 1, _id: 0 })
.toArray();
const users: any[] = await db.collection('users')
.find({ email: { $in: userEmails } })
.project({ email: 1, id: 1, _id: 0 })
.toArray();
return {
groups: existingGroups.reduce((acc, group) => {
acc[group.name] = group.id;
return acc;
}, {} as Record<string, string>),
users: users.reduce((acc, user) => {
acc[user.email] = user.id;
return acc;
}, {} as Record<string, string>)
};
}
async function deletePriorEntitiesGroups(body: any) {
const { ids, entity } = body;
if (!Array.isArray(ids) || ids.length === 0 || !entity) {
return;
}
const users = await db.collection('users')
.find({ id: { $in: ids } })
.project({ id: 1, entities: 1, _id: 0 })
.toArray();
// if the user doesn't have the target entity mark them for all groups deletion
const toDeleteUserIds = users
.filter(user => !user.entities?.some((e: any) => e.id === entity))
.map(user => user.id);
if (toDeleteUserIds.length === 0) {
return;
}
const affectedGroups = await db.collection('groups')
.find({ participants: { $in: toDeleteUserIds } })
.project({ id: 1, _id: 0 })
.toArray();
await db.collection('groups').updateMany(
{ participants: { $in: toDeleteUserIds } },
{ $pull: { participants: { $in: toDeleteUserIds } } } as any
);
// delete groups that were updated and have no participants
await db.collection('groups').deleteMany({
id: { $in: affectedGroups.map(g => g.id) },
participants: { $size: 0 }
});
}
async function existantGroupIds(names: string[]) {
const existingGroups = await db.collection('groups')
.find({
name: { $in: names }
})
.project({ id: 1, name: 1, _id: 0 })
.toArray();
return existingGroups.reduce((acc, group) => {
acc[group.name] = group.id;
return acc;
}, {} as Record<string, string>);
}

View File

@@ -1,51 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import client from "@/lib/mongodb";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Group} from "@/interfaces/user";
import {v4} from "uuid";
import {updateExpiryDateOnGroup, getGroupsForUser} from "@/utils/groups.be";
import {uniq, uniqBy} from "lodash";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") await get(req, res);
if (req.method === "POST") await post(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const {admin, participant} = req.query as {
admin: string;
participant: string;
};
const adminGroups = await getGroupsForUser(admin, participant);
const participants = uniq(adminGroups.flatMap((g) => g.participants));
const groups = await Promise.all(participants.map(async (c) => await getGroupsForUser(c, participant)));
return res.status(200).json([...adminGroups, ...uniqBy(groups.flat(), "id")]);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const body = req.body as Group;
await Promise.all(body.participants.map(async (p) => await updateExpiryDateOnGroup(p, body.admin)));
const id = v4();
await db.collection<Group>("groups").insertOne({
id,
name: body.name,
admin: body.admin,
participants: body.participants,
entity: body.entity,
});
res.status(200).json({ok: true, id});
}

View File

@@ -1,20 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { CorporateUser, User } from "@/interfaces/user";
import client from "@/lib/mongodb";
import { getLinkedUsers } from "@/utils/users.be";
import { uniqBy } from "lodash";
import type { NextApiRequest, NextApiResponse } from "next";
import { v4 } from "uuid";
import fs from 'fs'
import { findBy, mapBy } from "@/utils";
import { addUsersToEntity, getEntitiesWithRoles } from "@/utils/entities.be";
const db = client.db(process.env.MONGODB_DB);
type Data = {
name: string;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
res.status(200).json({ name: "John Doe" });
}

View File

@@ -1,73 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Invite } from "@/interfaces/invite";
import { requestUser } from "@/utils/api";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await get(req, res);
if (req.method === "DELETE") return await del(req, res);
if (req.method === "PATCH") return await patch(req, res);
res.status(404).json(undefined);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const { id } = req.query as { id: string };
const snapshot = await db.collection("invites").findOne({ id: id });
if (snapshot) {
res.status(200).json(snapshot);
} else {
res.status(404).json(undefined);
}
}
async function del(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const { id } = req.query as { id: string };
const snapshot = await db.collection("invites").findOne<Invite>({ id: id });
if(!snapshot){
res.status(404);
return;
}
if (user.type === "admin" || user.type === "developer") {
await db.collection("invites").deleteOne({ id: id });
res.status(200).json({ ok: true });
return;
}
res.status(403).json({ ok: false });
}
async function patch(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const { id } = req.query as { id: string };
if (user.type === "admin" || user.type === "developer") {
await db.collection("invites").updateOne(
{ id: id },
{ $set: {id: id, ...req.body} },
{ upsert: true }
);
return res.status(200).json({ ok: true });
}
res.status(403).json({ ok: false });
}

View File

@@ -1,67 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Invite } from "@/interfaces/invite";
import { CorporateUser, Group, User } from "@/interfaces/user";
import { v4 } from "uuid";
import { sendEmail } from "@/email";
import { updateExpiryDateOnGroup } from "@/utils/groups.be";
import { addUserToEntity, getEntity, getEntityWithRoles } from "@/utils/entities.be";
import { findBy } from "@/utils";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await get(req, res);
res.status(404).json(undefined);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const { id } = req.query as { id: string };
const invite = await db.collection("invites").findOne<Invite>({ id: id});
if (invite) {
if (invite.to !== req.session.user.id) return res.status(403).json({ ok: false });
await db.collection("invites").deleteOne({ id: id });
const invitedBy = await db.collection("users").findOne<User>({ id: invite.from});
if (!invitedBy) return res.status(404).json({ ok: false });
const inviteEntity = await getEntityWithRoles(invite.entity)
if (!inviteEntity) return res.status(404).json({ ok: false });
const defaultRole = findBy(inviteEntity.roles, 'isDefault', true)!
await addUserToEntity(invite.to, inviteEntity.id, defaultRole.id)
try {
await sendEmail(
"respondedInvite",
{
corporateName: invitedBy.name,
name: req.session.user.name,
decision: "accept",
environment: process.env.ENVIRONMENT,
},
[invitedBy.email],
`${req.session.user.name} has accepted your invite!`,
);
} catch (e) {
console.log(e);
}
res.status(200).json({ ok: true });
} else {
res.status(404).json(undefined);
}
}

View File

@@ -1,56 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import client from "@/lib/mongodb";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Invite} from "@/interfaces/invite";
import {User} from "@/interfaces/user";
import {sendEmail} from "@/email";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await get(req, res);
res.status(404).json(undefined);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {id} = req.query as {id: string};
const invite = await db.collection("invites").findOne<Invite>({ id: id});
if (invite) {
if (invite.to !== req.session.user.id) return res.status(403).json({ok: false});
await db.collection("invites").deleteOne({ id: id });
const invitedBy = await db.collection("users").findOne<User>({ id: invite.from });
if (!invitedBy) return res.status(404).json({ok: false});
try {
await sendEmail(
"respondedInvite",
{
corporateName: invitedBy.name,
name: req.session.user.name,
decision: "decline",
environment: process.env.ENVIRONMENT,
},
[invitedBy.email],
`${req.session.user.name} has declined your invite!`,
);
} catch (e) {
console.log(e);
}
res.status(200).json({ok: true});
} else {
res.status(404).json(undefined);
}
}

View File

@@ -1,69 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import {sendEmail} from "@/email";
import {Invite} from "@/interfaces/invite";
import {Ticket} from "@/interfaces/ticket";
import {User} from "@/interfaces/user";
import {sessionOptions} from "@/lib/session";
import client from "@/lib/mongodb";
import {withIronSessionApiRoute} from "iron-session/next";
import type {NextApiRequest, NextApiResponse} from "next";
import ShortUniqueId from "short-unique-id";
import { Entity } from "@/interfaces/entity";
import { getEntity } from "@/utils/entities.be";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") await get(req, res);
if (req.method === "POST") await post(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const snapshot = await db.collection("invites").find({}).toArray();
res.status(200).json(snapshot);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const body = req.body as Invite;
const existingInvites = await db.collection("invites").find<Invite>({}).toArray();
const invited = await db.collection("users").findOne<User>({ id: body.to});
if (!invited) return res.status(404).json({ok: false});
const entity = await getEntity(body.entity)
if (!entity) return res.status(404).json({ok: false});
try {
await sendEmail(
"receivedInvite",
{
name: invited.name,
entity: entity.label,
environment: process.env.ENVIRONMENT,
},
[invited.email],
"You have been invited to an entity!",
);
} catch (e) {
console.log(e);
}
if (existingInvites.filter((i) => i.to === body.to && i.from === body.from).length == 0) {
const shortUID = new ShortUniqueId();
await db.collection("invites").updateOne(
{ id: body.id || shortUID.randomUUID(8)},
{ $set: body },
{ upsert: true}
);
}
res.status(200).json({ok: true});
}

View File

@@ -1,34 +1,46 @@
import {NextApiRequest, NextApiResponse} from "next";
import {getAuth, signInWithEmailAndPassword} from "firebase/auth";
import {app} from "@/firebase";
import {sessionOptions} from "@/lib/session";
import {withIronSessionApiRoute} from "iron-session/next";
import {User} from "@/interfaces/user";
import client from "@/lib/mongodb";
const auth = getAuth(app);
import {ODOO_URL} from "@/lib/odoo";
export default withIronSessionApiRoute(login, sessionOptions);
async function login(req: NextApiRequest, res: NextApiResponse) {
const {email, password} = req.body as {email: string; password: string};
signInWithEmailAndPassword(auth, email.toLowerCase(), password)
.then(async (userCredentials) => {
const userId = userCredentials.user.uid;
const db = client.db(process.env.MONGODB_DB);
const user = await db.collection("users").findOne<User>({id: userId});
if (!user) return res.status(401).json({error: 401, message: "User does not exist!"});
req.session.user = {...user, id: userId};
await req.session.save();
res.status(200).json({user: {...user, id: userId}});
})
.catch((error) => {
console.log(error);
res.status(401).json({error});
try {
const resp = await fetch(`${ODOO_URL}/api/login`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({email: email.toLowerCase(), password}),
});
const data = await resp.json();
if (!resp.ok) {
return res.status(resp.status).json(data);
}
const statusMap: Record<string, string> = {
active: "active",
inactive: "disabled",
suspended: "disabled",
};
const user = {
...data.user,
id: String(data.user.id),
status: statusMap[data.user.status] || data.user.status || "active",
permissions: Array.isArray(data.user.permissions) ? data.user.permissions : [],
};
req.session.user = user;
req.session.token = data.token;
await req.session.save();
res.status(200).json(user);
} catch (error) {
console.error("Login error:", error);
res.status(500).json({error: "Login failed"});
}
}

View File

@@ -1,15 +1,10 @@
import {NextApiRequest, NextApiResponse} from "next";
import {getAuth, signOut} from "firebase/auth";
import {app} from "@/firebase";
import {sessionOptions} from "@/lib/session";
import {withIronSessionApiRoute} from "iron-session/next";
const auth = getAuth(app);
export default withIronSessionApiRoute(logout, sessionOptions);
async function logout(req: NextApiRequest, res: NextApiResponse) {
await auth.signOut();
req.session.destroy();
res.status(200).json(undefined);
}

View File

@@ -1,126 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { app } from "@/firebase";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { v4 } from "uuid";
import { CorporateUser, Group, Type, User } from "@/interfaces/user";
import { createUserWithEmailAndPassword, getAuth } from "firebase/auth";
import ShortUniqueId from "short-unique-id";
import { getGroup, getGroups, getUserCorporate, getUserGroups, getUserNamedGroup } from "@/utils/groups.be";
import { uniq } from "lodash";
import { getSpecificUsers, getUser } from "@/utils/users.be";
import client from "@/lib/mongodb";
import { getEntityWithRoles } from "@/utils/entities.be";
import { findBy } from "@/utils";
const DEFAULT_DESIRED_LEVELS = {
reading: 9,
listening: 9,
writing: 9,
speaking: 9,
};
const DEFAULT_LEVELS = {
reading: 0,
listening: 0,
writing: 0,
speaking: 0,
};
const auth = getAuth(app);
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
return res.status(404).json({ ok: false });
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const maker = req.session.user;
if (!maker) {
return res.status(401).json({ ok: false, reason: "You must be logged in to make user!" });
}
const { email, passport_id, password, type, groupID, entity, expiryDate, corporate } = req.body as {
email: string;
password?: string;
passport_id: string;
type: string;
entity: string;
groupID?: string;
corporate?: string;
expiryDate: null | Date;
};
// cleaning data
delete req.body.passport_id;
delete req.body.groupID;
delete req.body.expiryDate;
delete req.body.password;
delete req.body.corporate;
delete req.body.entity
await createUserWithEmailAndPassword(auth, email.toLowerCase(), !!password ? password : passport_id)
.then(async (userCredentials) => {
const userId = userCredentials.user.uid;
const entityWithRole = await getEntityWithRoles(entity)
const defaultRole = findBy(entityWithRole?.roles || [], "isDefault", true)
const user = {
...req.body,
bio: "",
id: userId,
type: type,
focus: "academic",
status: "active",
desiredLevels: DEFAULT_DESIRED_LEVELS,
profilePicture: "/defaultAvatar.png",
levels: DEFAULT_LEVELS,
isFirstLogin: false,
isVerified: true,
registrationDate: new Date(),
entities: [{ id: entity, role: defaultRole?.id || "" }],
subscriptionExpirationDate: expiryDate || null,
...((maker.type === "corporate" || maker.type === "mastercorporate") && type === "corporate"
? {
corporateInformation: {},
}
: {}),
};
const uid = new ShortUniqueId();
const code = uid.randomUUID(6);
await db.collection("users").insertOne(user);
await db.collection("codes").insertOne({
code,
creator: maker.id,
expiryDate,
type,
creationDate: new Date(),
userId,
email: email.toLowerCase(),
name: req.body.name,
...(!!passport_id ? { passport_id } : {}),
});
if (!!groupID) {
const group = await getGroup(groupID);
if (!!group) await db.collection("groups").updateOne({ id: group.id }, { $set: { participants: [...group.participants, userId] } });
}
console.log(`Returning - ${email}`);
return res.status(200).json({ ok: true });
})
.catch((error) => {
if (error.code.includes("email-already-in-use")) return res.status(403).json({ error, message: "E-mail is already in the platform." });
console.log(`Failing - ${email}`);
console.log(error);
return res.status(401).json({ error });
});
}

View File

@@ -1,83 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import client from "@/lib/mongodb";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {PERMISSIONS} from "@/constants/userPermissions";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
if (req.method === "DELETE") return del(req, res);
if (req.method === "PATCH") return patch(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {id} = req.query as {id: string};
const docSnap = await db.collection("packages").findOne({ id: id});
if (docSnap) {
res.status(200).json({
...docSnap,
module,
});
} else {
res.status(404).json(undefined);
}
}
async function patch(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {id} = req.query as {id: string};
const docSnap = await db.collection("packages").findOne({ id: id});
if (docSnap) {
if (!["developer", "admin"].includes(req.session.user.type)) {
res.status(403).json({ok: false});
return;
}
await db.collection("packages").updateOne(
{ id: id },
{ $set: req.body }
);
res.status(200).json({ok: true});
} else {
res.status(404).json({ok: false});
}
}
async function del(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {id} = req.query as {id: string};
const docSnap = await db.collection("packages").findOne({ id: id});
if (docSnap) {
if (!["developer", "admin"].includes(req.session.user.type)) {
res.status(403).json({ok: false});
return;
}
await db.collection("packages").deleteOne({ id: id });
res.status(200).json({ok: true});
} else {
res.status(404).json({ok: false});
}
}

View File

@@ -1,38 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import client from "@/lib/mongodb";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Group} from "@/interfaces/user";
import {Package} from "@/interfaces/paypal";
import {v4} from "uuid";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") await get(req, res);
if (req.method === "POST") await post(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const snapshot = await db.collection("packages").find({}).toArray();
res.status(200).json(snapshot);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (!["developer", "admin"].includes(req.session.user!.type))
return res.status(403).json({ok: false, reason: "You do not have permission to create a new package"});
const body = req.body as Package;
// Package already had an id but a new one was being set
// with v4() don't know if intentional or not, recreated the behaviour as was
await db.collection("packages").insertOne({...body, id: v4()})
res.status(200).json({ok: true});
}

View File

@@ -1,82 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {storage} from "@/firebase";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Group} from "@/interfaces/user";
import {Payment} from "@/interfaces/paypal";
import {deleteObject, ref} from "firebase/storage";
import client from "@/lib/mongodb";
import { requestUser } from "@/utils/api";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await get(req, res);
if (req.method === "DELETE") return await del(req, res);
if (req.method === "PATCH") return await patch(req, res);
res.status(404).json(undefined);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {id} = req.query as {id: string};
const payment = await db.collection("payments").findOne<Payment>({id});
if (!!payment) {
res.status(200).json(payment);
} else {
res.status(404).json(undefined);
}
}
async function del(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const {id} = req.query as {id: string};
const payment = await db.collection("payments").findOne<Payment>({id});
if (!payment) return res.status(404).json({ok: false});
if (user.type === "admin" || user.type === "developer") {
if (payment.commissionTransfer) await deleteObject(ref(storage, payment.commissionTransfer));
if (payment.corporateTransfer) await deleteObject(ref(storage, payment.corporateTransfer));
await db.collection("payments").deleteOne({id: payment.id});
res.status(200).json({ok: true});
return;
}
res.status(403).json({ok: false});
}
async function patch(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const {id} = req.query as {id: string};
const payment = await db.collection("payments").findOne<Payment>({id});
if (!payment) return res.status(404).json({ok: false});
if (user.type === "admin" || user.type === "developer") {
await db.collection("payments").updateOne({id: payment.id}, {$set: req.body});
if (req.body.isPaid) {
const corporateID = req.body.corporate;
await db.collection("users").updateOne({id: corporateID}, {$set: {status: "active"}});
}
return res.status(200).json({ok: true});
}
res.status(403).json({ok: false});
}

View File

@@ -1,62 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Payment} from "@/interfaces/paypal";
import {PaymentsStatus} from "@/interfaces/user.payments";
import client from "@/lib/mongodb";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await get(req, res);
res.status(404).json(undefined);
}
// user can fetch payments assigned to him as an agent
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const payments = await db
.collection("payments")
.find(["admin", "developer"].includes(req.session.user.type) ? {} : {[req.session.user.type]: req.session.user.id})
.toArray();
if (payments.length === 0) {
res.status(200).json({
pending: [],
done: [],
});
return;
}
const paidStatusEntries = payments.reduce(
(acc: PaymentsStatus, doc) => {
if (doc.isPaid) {
return {
...acc,
done: [...acc.done, doc.corporate],
};
}
return {
...acc,
pending: [...acc.pending, doc.corporate],
};
},
{
pending: [],
done: [],
},
);
res.status(200).json({
pending: [...new Set(paidStatusEntries.pending)],
done: [...new Set(paidStatusEntries.done)],
});
}

View File

@@ -1,201 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { storage } from "@/firebase";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { FilesStorage } from "@/interfaces/storage.files";
import { Payment } from "@/interfaces/paypal";
import fs from "fs";
import { ref, uploadBytes, deleteObject, getDownloadURL } from "firebase/storage";
import formidable from "formidable-serverless";
const db = client.db(process.env.MONGODB_DB);
const getPaymentField = (type: FilesStorage) => {
switch (type) {
case "commission":
return "commissionTransfer";
case "corporate":
return "corporateTransfer";
default:
return null;
}
};
const handleDelete = async (paymentId: string, paymentField: "commissionTransfer" | "corporateTransfer") => {
const paymentDoc = await db.collection("payments").findOne<Payment>({ id: paymentId })
if (paymentDoc) {
const { [paymentField]: paymentFieldPath } = paymentDoc;
// Create a reference to the file to delete
const documentRef = ref(storage, paymentFieldPath);
await deleteObject(documentRef);
await db.collection("payments").deleteOne({ id: paymentId });
await db.collection("payments").updateOne(
{ id: paymentId },
{
$unset: { [paymentField]: "" },
$set: { isPaid: false }
}
);
}
};
const handleUpload = async (req: NextApiRequest, paymentId: string, paymentField: "commissionTransfer" | "corporateTransfer") =>
new Promise((resolve, reject) => {
const form = formidable({ keepExtensions: true });
form.parse(req, async (err: any, fields: any, files: any) => {
if (err) {
reject(err);
return;
}
try {
const { file } = files;
const fileName = Date.now() + "-" + file.name;
const fileRef = ref(storage, fileName);
const binary = fs.readFileSync(file.path).buffer;
const snapshot = await uploadBytes(fileRef, binary);
fs.rmSync(file.path);
await db.collection("payments").updateOne(
{ id: paymentId },
{
$set: { [paymentField]: snapshot.ref.fullPath }
}
);
resolve(snapshot.ref.fullPath);
} catch (err) {
reject(err);
}
});
});
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
if (req.method === "GET") return await get(req, res);
if (req.method === "POST") return await post(req, res);
if (req.method === "DELETE") return await del(req, res);
if (req.method === "PATCH") return await patch(req, res);
res.status(404).json(undefined);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const { type, paymentId } = req.query as {
type: FilesStorage;
paymentId: string;
};
const paymentField = getPaymentField(type);
if (paymentField === null) {
res.status(500).json({ error: "Failed to identify payment field" });
return;
}
const paymentRef = await db.collection("payments").findOne<Payment>({ id: paymentId })
if (paymentRef) {
const { [paymentField]: paymentFieldPath } = paymentRef;
// Create a reference to the file to delete
const documentRef = ref(storage, paymentFieldPath);
const url = await getDownloadURL(documentRef);
res.status(200).json({ url, name: documentRef.name });
return;
}
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const { type, paymentId } = req.query as {
type: FilesStorage;
paymentId: string;
};
const paymentField = getPaymentField(type);
if (paymentField === null) {
res.status(500).json({ error: "Failed to identify payment field" });
return;
}
try {
const ref = await handleUpload(req, paymentId, paymentField);
const updatedDoc = await db.collection("payments").findOne<Payment>({ id: paymentId })
if (updatedDoc && updatedDoc.commissionTransfer && updatedDoc.corporateTransfer) {
await db.collection("payments").updateOne(
{ id: paymentId },
{ $set: { isPaid: true } }
);
await db.collection("users").updateOne(
{ id: updatedDoc.corporate },
{ $set: { status: "active" } }
);
}
res.status(200).json({ ref });
} catch (error) {
res.status(500).json({ error });
}
}
async function del(req: NextApiRequest, res: NextApiResponse) {
const { type, paymentId } = req.query as {
type: FilesStorage;
paymentId: string;
};
const paymentField = getPaymentField(type);
if (paymentField === null) {
res.status(500).json({ error: "Failed to identify payment field" });
return;
}
try {
await handleDelete(paymentId, paymentField);
res.status(200).json({ ok: true });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to delete file" });
}
}
async function patch(req: NextApiRequest, res: NextApiResponse) {
const { type, paymentId } = req.query as {
type: FilesStorage;
paymentId: string;
};
const paymentField = getPaymentField(type);
if (paymentField === null) {
res.status(500).json({ error: "Failed to identify payment field" });
return;
}
try {
await handleDelete(paymentId, paymentField);
} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to delete file" });
return;
}
try {
const ref = await handleUpload(req, paymentId, paymentField);
res.status(200).json({ ref });
} catch (err) {
res.status(500).json({ error: "Failed to upload file" });
}
}
export const config = {
api: {
bodyParser: false,
},
};

View File

@@ -1,37 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Group} from "@/interfaces/user";
import {Payment} from "@/interfaces/paypal";
import {v4} from "uuid";
import ShortUniqueId from "short-unique-id";
import client from "@/lib/mongodb";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") await get(req, res);
if (req.method === "POST") await post(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const payments = await db.collection("payments").find({}).toArray();
res.status(200).json(payments);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const body = req.body as Payment;
const shortUID = new ShortUniqueId();
await db.collection("payments").insertOne({...body, id: shortUID.randomUUID(8)});
res.status(200).json({ok: true});
}

View File

@@ -1,24 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import client from "@/lib/mongodb";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function get(req: NextApiRequest, res: NextApiResponse) {
const payments = await db.collection("paypalpayments").find({}).toArray();
res.status(200).json(payments);
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") await get(req, res);
}

View File

@@ -1,50 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import client from "@/lib/mongodb";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Group} from "@/interfaces/user";
import {Payment} from "@/interfaces/paypal";
import {v4} from "uuid";
import ShortUniqueId from "short-unique-id";
import axios from "axios";
import {IntentionResult, PaymentIntention} from "@/interfaces/paymob";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
if (req.method === "GET") await get(req, res);
if (req.method === "POST") await post(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const snapshot = await db.collection("payments").find().toArray();
res.status(200).json(snapshot);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const intention = req.body as PaymentIntention;
const response = await axios.post<IntentionResult>(
"https://oman.paymob.com/v1/intention/",
{
...intention,
payment_methods: [parseInt(process.env.PAYMOB_INTEGRATION_ID || "0")],
items: [],
extras: {...intention.extras, userID: req.session.user!.id},
} as PaymentIntention,
{headers: {Authorization: `Token ${process.env.PAYMOB_SECRET_KEY}`}},
);
const intentionResult = response.data;
res.status(200).json({
iframeURL: `https://oman.paymob.com/unifiedcheckout/?publicKey=${process.env.PAYMOB_PUBLIC_KEY}&clientSecret=${intentionResult.client_secret}`,
});
}

View File

@@ -1,104 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Group, User } from "@/interfaces/user";
import { DurationUnit, Package, Payment } from "@/interfaces/paypal";
import { v4 } from "uuid";
import ShortUniqueId from "short-unique-id";
import axios from "axios";
import { IntentionResult, PaymentIntention, TransactionResult } from "@/interfaces/paymob";
import moment from "moment";
import client from "@/lib/mongodb";
import { getEntity } from "@/utils/entities.be";
import { Entity } from "@/interfaces/entity";
import { getEntityUsers } from "@/utils/users.be";
import { mapBy } from "@/utils";
const db = client.db(process.env.MONGODB_DB);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") await post(req, res);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const transactionResult = req.body as TransactionResult;
const authToken = await authenticatePaymob();
console.log("WEBHOOK: ", transactionResult);
if (!checkTransaction(authToken, transactionResult.transaction.order.id)) return res.status(404).json({ ok: false });
if (!transactionResult.transaction.success) return res.status(400).json({ ok: false });
const { userID, duration, duration_unit, entity: entityID } = transactionResult.intention.extras.creation_extras as {
userID: string;
duration: number;
duration_unit: DurationUnit;
entity: string
};
const user = await db.collection("users").findOne<User>({ id: userID as string });
if (!user || !duration || !duration_unit) return res.status(404).json({ ok: false });
const subscriptionExpirationDate = user.subscriptionExpirationDate;
if (!subscriptionExpirationDate) return res.status(200).json({ ok: false });
const initialDate = moment(subscriptionExpirationDate).isAfter(moment()) ? moment(subscriptionExpirationDate) : moment();
const updatedSubscriptionExpirationDate = moment(initialDate).add(duration, duration_unit).endOf("day").subtract(2, "hours").toISOString();
await db.collection("users").updateOne(
{ id: userID as string },
{ $set: { subscriptionExpirationDate: updatedSubscriptionExpirationDate, status: "active" } }
);
await db.collection("paypalpayments").insertOne({
id: v4(),
createdAt: new Date().toISOString(),
currency: transactionResult.transaction.currency,
orderId: transactionResult.transaction.id,
status: "COMPLETED",
subscriptionDuration: duration,
subscriptionDurationUnit: duration_unit,
subscriptionExpirationDate: updatedSubscriptionExpirationDate,
userId: userID,
value: transactionResult.transaction.amount_cents / 1000,
});
if (entityID) {
const entity = await getEntity(entityID)
await db.collection<Entity>("entities").updateOne({ id: entityID }, { $set: { expiryDate: req.body.expiryDate } });
const users = await getEntityUsers(entityID, 0, {
subscriptionExpirationDate: entity?.expiryDate,
$and: [
{ type: { $ne: "admin" } },
{ type: { $ne: "developer" } },
]
})
await db.collection<User>("users").updateMany({ id: { $in: mapBy(users, 'id') } }, { $set: { subscriptionExpirationDate: req.body.expiryDate } })
}
res.status(200).json({
ok: true,
});
}
const authenticatePaymob = async () => {
const response = await axios.post<{ token: string }>(
"https://oman.paymob.com/api/auth/tokens",
{
api_key: process.env.PAYMOB_API_KEY,
},
{ headers: { Authorization: `Bearer ${process.env.PAYMOB_SECRET_KEY}` } },
);
return response.data.token;
};
const checkTransaction = async (token: string, orderID: number) => {
const response = await axios.post("https://oman.paymob.com/api/ecommerce/orders/transaction_inquiry", { auth_token: token, order_id: orderID });
return response.status === 200;
};

View File

@@ -1,130 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios from "axios";
import { DurationUnit, TokenError, TokenSuccess } from "@/interfaces/paypal";
import { base64 } from "@firebase/util";
import { v4 } from "uuid";
import { OrderResponseBody } from "@paypal/paypal-js";
import { getAccessToken } from "@/utils/paypal";
import moment from "moment";
import { Group } from "@/interfaces/user";
import { requestUser } from "@/utils/api";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST")
return res.status(404).json({ ok: false, reason: "Method not supported!" });
if (!req.session.user) return res.status(401).json({ ok: false });
const accessToken = await getAccessToken();
if (!accessToken)
return res.status(401).json({ ok: false, reason: "Authorization failed!" });
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const { id, duration, duration_unit, trackingId } = req.body as {
id: string;
duration: number;
duration_unit: DurationUnit;
trackingId: string;
};
if (!trackingId)
return res.status(401).json({ ok: false, reason: "Missing tracking id!" });
const url = `${process.env.PAYPAL_ACCESS_TOKEN_URL}/v2/checkout/orders/${id}/capture`;
const headers = {
headers: {
Authorization: `Bearer ${accessToken}`,
"PayPal-Client-Metadata-Id": trackingId,
},
};
axios
.post(url, {}, headers)
.then(async (request) => {
if (request.data.status === "COMPLETED") {
const user = req.session.user;
const subscriptionExpirationDate =
user!.subscriptionExpirationDate;
const today = moment(new Date());
const dateToBeAddedTo = !subscriptionExpirationDate
? today
: moment(subscriptionExpirationDate).isAfter(today)
? moment(subscriptionExpirationDate)
: today;
const updatedExpirationDate = dateToBeAddedTo.add(
duration,
duration_unit
);
await db.collection("users").updateOne(
{ id: req.session.user!.id },
{
$set: {
subscriptionExpirationDate: updatedExpirationDate.toISOString(),
status: "active",
}
}
);
try {
await db.collection("paypalpayments").insertOne({
id: v4(),
orderId: id,
userId: req.session.user!.id,
status: request.data.status,
createdAt: new Date().toISOString(),
value: request.data.purchase_units[0].payments.captures[0].amount.value,
currency: request.data.purchase_units[0].payments.captures[0].amount.currency_code,
subscriptionDuration: duration,
subscriptionDurationUnit: duration_unit,
subscriptionExpirationDate: updatedExpirationDate.toISOString(),
});
} catch (err) {
console.error("Failed to insert paypal payment!", err);
}
if (user!.type === "corporate") {
const groups = (
await db.collection("groups").find<Group>({}).toArray()
).filter((x) => x.admin === user!.id);
await Promise.all(
groups
.flatMap((x) => x.participants)
.map(
async (x) =>
await db.collection("users").updateOne(
{ id: x },
{
$set: {
subscriptionExpirationDate: updatedExpirationDate.toISOString(),
status: "active",
}
}
)
)
);
}
return res.status(200).json({ ok: true });
}
res.status(404).json({
ok: false,
reason: "Order ID not found or purchase was not approved!",
});
})
.catch((err) => {
console.error(err.response.status, err.response.data);
res.status(err.response.status).json(err.response.data);
});
}

View File

@@ -1,103 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import axios from "axios";
import {v4} from "uuid";
import {OrderResponseBody} from "@paypal/paypal-js";
import {getAccessToken} from "@/utils/paypal";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") return res.status(404).json({ok: false, reason: "Method not supported!"});
if (!req.session.user) return res.status(401).json({ok: false});
const accessToken = await getAccessToken();
if (!accessToken) return res.status(401).json({ok: false, reason: "Authorization failed!"});
const {currencyCode, price, trackingId} = req.body as {
currencyCode: string;
price: number;
trackingId: string;
};
if (!trackingId) return res.status(401).json({ok: false, reason: "Missing tracking id!"});
const url = `${process.env.PAYPAL_ACCESS_TOKEN_URL}/v2/checkout/orders`;
const amount = {
currency_code: currencyCode,
value: price.toString(),
};
const data = {
purchase_units: [
{
invoice_id: `INV-${v4()}`,
amount: {
...amount,
breakdown: {
item_total: amount,
},
},
items: [
{
name: "Encoach Subscription",
quantity: "1",
category: "DIGITAL_GOODS",
unit_amount: amount,
},
],
},
],
payment_source: {
paypal: {
email_address: req.session.user.email || "",
address: {
address_line_1: "",
address_line_2: "",
admin_area_1: "",
admin_area_2: "",
// added default values as requsted by the client, using the default values recommended
// the paypal engineer, otherwise we would have to create something that would detect the location
// of the user and generate a valid postal code for that location...
country_code: "US",
postal_code: "94107",
},
experience_context: {
payment_method_preference: "IMMEDIATE_PAYMENT_REQUIRED",
locale: "en-US",
landing_page: "LOGIN",
shipping_preference: "NO_SHIPPING",
user_action: "PAY_NOW",
brand_name: "Encoach",
},
},
},
intent: "CAPTURE",
};
const headers = {
headers: {
Authorization: `Bearer ${accessToken}`,
"PayPal-Client-Metadata-Id": trackingId,
},
};
console.log(
JSON.stringify({
url,
data,
headers,
}),
);
axios
.post<OrderResponseBody>(url, data, headers)
.then((request) => {
res.status(request.status).json(request.data);
})
.catch((err) => {
console.error(err.response.status, err.response.data);
res.status(err.response.status).json(err.response.data);
});
}

View File

@@ -1,55 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import axios from "axios";
import {v4} from "uuid";
import {OrderResponseBody} from "@paypal/paypal-js";
import {getAccessToken} from "@/utils/paypal";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "PUT") return res.status(404).json({ok: false, reason: "Method not supported!"});
if (!req.session.user) return res.status(401).json({ok: false});
const accessToken = await getAccessToken();
if (!accessToken) return res.status(401).json({ok: false, reason: "Authorization failed!"});
const trackingId = `${req.session.user.id}-${Date.now()}`;
const url = `${process.env.PAYPAL_ACCESS_TOKEN_URL}/v1/risk/transaction-contexts/${process.env.PAYPAL_MERCHANT_ID}/${trackingId}`;
const data = {
additional_data: [
{
key: "user_id",
value: req.session.user.id,
},
],
};
const headers = {
headers: {
Authorization: `Bearer ${accessToken}`,
},
};
console.log(
JSON.stringify({
url,
data,
headers,
}),
);
try {
const request = await axios.put(url, data, headers);
return res.status(request.status).json({
ok: true,
trackingId,
});
} catch (err) {
console.error(url, err);
return res.status(500).json({ok: false, reason: "Failed to create tracking ID"});
}
}

View File

@@ -1,50 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { getPermissionDoc } from "@/utils/permissions.be";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "PATCH") return patch(req, res);
if (req.method === "GET") return get(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const { id } = req.query as { id: string };
const permissionDoc = await getPermissionDoc(id);
return res.status(200).json({ allowed: permissionDoc.users.includes(req.session.user.id) });
}
async function patch(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const { id } = req.query as { id: string };
const { users } = req.body;
try {
await db.collection("permissions").updateOne(
{ id: id },
{ $set: {...users, id: id} },
{ upsert: true }
);
return res.status(200).json({ ok: true });
} catch (err) {
console.error(err);
return res.status(500).json({ ok: false });
}
}

View File

@@ -1,28 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {Permission} from "@/interfaces/permissions";
import {bootstrap} from "@/utils/permissions.be";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
console.log("Boostrap");
try {
await bootstrap();
return res.status(200).json({ok: true});
} catch (err) {
console.error("Failed to update permissions", err);
return res.status(500).json({ok: false});
}
}

View File

@@ -1,21 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { Permission } from "@/interfaces/permissions";
import { getPermissions, getPermissionDocs } from "@/utils/permissions.be";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const docs = await getPermissionDocs();
res.status(200).json(docs);
}

View File

@@ -1,170 +1,38 @@
import { NextApiRequest, NextApiResponse } from "next";
import { createUserWithEmailAndPassword, getAuth } from "firebase/auth";
import { app } from "@/firebase";
import { sessionOptions } from "@/lib/session";
import { withIronSessionApiRoute } from "iron-session/next";
import { Code, CorporateInformation, DemographicInformation, Group, Type } from "@/interfaces/user";
import { addUserToGroupOnCreation } from "@/utils/registration";
import moment from "moment";
import { v4 } from "uuid";
import client from "@/lib/mongodb";
import { addUserToEntity, getEntityWithRoles } from "@/utils/entities.be";
import { findBy } from "@/utils";
const auth = getAuth(app);
const db = client.db(process.env.MONGODB_DB);
import {NextApiRequest, NextApiResponse} from "next";
import {sessionOptions} from "@/lib/session";
import {withIronSessionApiRoute} from "iron-session/next";
import {ODOO_URL} from "@/lib/odoo";
export default withIronSessionApiRoute(register, sessionOptions);
const DEFAULT_DESIRED_LEVELS = {
reading: 9,
listening: 9,
writing: 9,
speaking: 9,
};
const DEFAULT_LEVELS = {
reading: 0,
listening: 0,
writing: 0,
speaking: 0,
};
async function register(req: NextApiRequest, res: NextApiResponse) {
const { type } = req.body as {
type: "individual" | "corporate";
};
if (type === "individual") return registerIndividual(req, res);
if (type === "corporate") return registerCorporate(req, res);
}
async function registerIndividual(req: NextApiRequest, res: NextApiResponse) {
const { email, passport_id, password, code } = req.body as {
email: string;
passport_id?: string;
password: string;
code?: string;
};
const codeDoc = await db.collection("codes").findOne<Code>({ code });
if (code && code.length > 0 && !codeDoc)
return res.status(400).json({ error: "Invalid Code!" });
createUserWithEmailAndPassword(auth, email.toLowerCase(), password)
.then(async (userCredentials) => {
const userId = userCredentials.user.uid;
delete req.body.password;
const user = {
...req.body,
id: userId,
email: email.toLowerCase(),
desiredLevels: DEFAULT_DESIRED_LEVELS,
levels: DEFAULT_LEVELS,
bio: "",
isFirstLogin: codeDoc ? codeDoc.type === "student" : true,
profilePicture: "/defaultAvatar.png",
focus: "academic",
type: email.endsWith("@ecrop.dev") ? "developer" : codeDoc ? codeDoc.type : "student",
subscriptionExpirationDate: codeDoc ? codeDoc.expiryDate : moment().subtract(1, "days").toISOString(),
...(passport_id ? { demographicInformation: { passport_id } } : {}),
registrationDate: new Date().toISOString(),
status: code ? "active" : "paymentDue",
entities: [],
isVerified: !!codeDoc,
};
await db.collection("users").insertOne(user);
if (!!codeDoc) {
await db.collection("codes").updateOne({ code: codeDoc.code }, { $set: { userId } });
if (codeDoc.entity) {
const inviteEntity = await getEntityWithRoles(codeDoc.entity)
if (inviteEntity) {
const defaultRole = findBy(inviteEntity.roles, 'isDefault', true)!
await addUserToEntity(userId, codeDoc.entity, defaultRole.id)
}
}
}
req.session.user = user;
await req.session.save();
res.status(200).json({ user });
})
.catch((error) => {
console.log(error);
res.status(401).json({ error });
try {
const resp = await fetch(`${ODOO_URL}/api/register`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(req.body),
});
}
async function registerCorporate(req: NextApiRequest, res: NextApiResponse) {
const { email, password } = req.body as {
email: string;
password: string;
corporateInformation: CorporateInformation;
};
const data = await resp.json();
createUserWithEmailAndPassword(auth, email.toLowerCase(), password)
.then(async (userCredentials) => {
const userId = userCredentials.user.uid;
delete req.body.password;
if (!resp.ok) {
return res.status(resp.status).json(data);
}
if (data.user && data.token) {
const user = {
...req.body,
id: userId,
email: email.toLowerCase(),
desiredLevels: DEFAULT_DESIRED_LEVELS,
levels: DEFAULT_LEVELS,
bio: "",
isFirstLogin: false,
focus: "academic",
type: "corporate",
subscriptionExpirationDate: req.body.subscriptionExpirationDate || null,
status: "paymentDue",
registrationDate: new Date().toISOString(),
// apparently there's an issue with the verification email system
// therefore we will skip this requirement for now
isVerified: true,
...data.user,
id: String(data.user.id),
};
const defaultTeachersGroup: Group = {
admin: userId,
id: v4(),
name: "Teachers",
participants: [],
disableEditing: true,
};
const defaultStudentsGroup: Group = {
admin: userId,
id: v4(),
name: "Students",
participants: [],
disableEditing: true,
};
const defaultCorporateGroup: Group = {
admin: userId,
id: v4(),
name: "Corporate",
participants: [],
disableEditing: true,
};
await db.collection("users").insertOne(user);
await db.collection("groups").insertMany([defaultCorporateGroup, defaultStudentsGroup, defaultTeachersGroup]);
req.session.user = user;
req.session.token = data.token;
await req.session.save();
return res.status(200).json({user});
}
res.status(200).json({ user });
})
.catch((error) => {
console.log(error);
res.status(401).json({ error });
});
return res.status(200).json(data);
} catch (error) {
console.error("Registration error:", error);
res.status(500).json({error: "Registration failed"});
}
}

View File

@@ -1,17 +1,3 @@
import {NextApiRequest, NextApiResponse} from "next";
import {getAuth, confirmPasswordReset} from "firebase/auth";
import {app} from "@/firebase";
import {sessionOptions} from "@/lib/session";
import {withIronSessionApiRoute} from "iron-session/next";
import {proxyToOdooPublic} from "@/lib/odoo";
const auth = getAuth(app);
export default withIronSessionApiRoute(confirm, sessionOptions);
async function confirm(req: NextApiRequest, res: NextApiResponse) {
const {code, password} = req.body as {code: string; password: string};
confirmPasswordReset(auth, code, password)
.then(() => res.status(200).json({ok: true}))
.catch(() => res.status(404).json({ok: false}));
}
export default proxyToOdooPublic("/api/reset");

View File

@@ -1,17 +1,3 @@
import {NextApiRequest, NextApiResponse} from "next";
import {getAuth, sendPasswordResetEmail} from "firebase/auth";
import {app} from "@/firebase";
import {sessionOptions} from "@/lib/session";
import {withIronSessionApiRoute} from "iron-session/next";
import {proxyToOdooPublic} from "@/lib/odoo";
const auth = getAuth(app);
export default withIronSessionApiRoute(reset, sessionOptions);
async function reset(req: NextApiRequest, res: NextApiResponse) {
const {email} = req.body as {email: string};
sendPasswordResetEmail(auth, email)
.then(() => res.status(200).json({ok: true}))
.catch(() => res.status(404).json({ok: false}));
}
export default proxyToOdooPublic("/api/reset");

View File

@@ -1,32 +1,3 @@
import {NextApiRequest, NextApiResponse} from "next";
import {getAuth, sendEmailVerification, sendSignInLinkToEmail, User} from "firebase/auth";
import {getAuth as getAdminAuth, UserRecord} from "firebase-admin/auth";
import {app, adminApp} from "@/firebase";
import {sessionOptions} from "@/lib/session";
import {withIronSessionApiRoute} from "iron-session/next";
import {prepareMailer, prepareMailOptions, sendEmail} from "@/email";
import ShortUniqueId from "short-unique-id";
import {proxyToOdooPublic} from "@/lib/odoo";
export default withIronSessionApiRoute(sendVerification, sessionOptions);
async function sendVerification(req: NextApiRequest, res: NextApiResponse) {
const short = new ShortUniqueId();
if (req.session.user) {
const ok = await sendEmail(
"verification",
{
name: req.session.user.name,
code: short.randomUUID(6),
email: req.session.user.email,
environment: process.env.ENVIRONMENT,
},
[req.session.user.email],
"EnCoach Verification",
);
res.status(200).json({ok});
return;
}
res.status(404).json({ok: false});
}
export default proxyToOdooPublic("/api/reset/sendVerification");

View File

@@ -1,29 +1,3 @@
import {NextApiRequest, NextApiResponse} from "next";
import {getAuth} from "firebase-admin/auth";
import {adminApp} from "@/firebase";
import client from "@/lib/mongodb";
import {sessionOptions} from "@/lib/session";
import {withIronSessionApiRoute} from "iron-session/next";
import {proxyToOdoo} from "@/lib/odoo";
const db = client.db(process.env.MONGODB_DB);
const auth = getAuth(adminApp);
export default withIronSessionApiRoute(verify, sessionOptions);
async function verify(req: NextApiRequest, res: NextApiResponse) {
const {email} = req.body as {email: string};
const user = await auth.getUserByEmail(email);
if (!user) {
res.status(404).json({ok: false});
return;
}
await db.collection("users").updateOne(
{ id: user.uid},
{ $set: {isVerified: true} }
);
res.status(200).json({ok: true});
}
export default proxyToOdoo("/api/reset");

View File

@@ -1,79 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {getEntityWithRoles} from "@/utils/entities.be";
import client from "@/lib/mongodb";
import {Entity} from "@/interfaces/entity";
import { deleteRole, getRole, transferRole } from "@/utils/roles.be";
import { doesEntityAllow } from "@/utils/permissions";
import { findBy } from "@/utils";
import { requestUser } from "@/utils/api";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return await get(req, res);
if (req.method === "PATCH") return await patch(req, res);
if (req.method === "DELETE") return await del(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const {id} = req.query as {id: string};
const role = await getRole(id)
if (!role) return res.status(404).json({ok: false})
res.status(200).json(role);
}
async function del(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const { id } = req.query as { id: string };
const role = await getRole(id)
if (!role) return res.status(404).json({ok: false})
if (role.isDefault) return res.status(403).json({ok: false})
const entity = await getEntityWithRoles(role.entityID)
if (!entity) return res.status(404).json({ok: false})
if (!doesEntityAllow(user, entity, "delete_entity_role")) return res.status(403).json({ok: false})
const defaultRole = findBy(entity.roles, 'isDefault', true)!
await transferRole(role.id, defaultRole.id)
await deleteRole(role.id)
return res.status(200).json({ok: true});
}
async function patch(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const { id } = req.query as { id: string };
const {label, permissions} = req.body as {label?: string, permissions?: string}
const role = await getRole(id)
if (!role) return res.status(404).json({ok: false})
const entity = await getEntityWithRoles(role.entityID)
if (!entity) return res.status(404).json({ok: false})
if (!doesEntityAllow(user, entity, "rename_entity_role") && !!label) return res.status(403).json({ok: false})
if (!doesEntityAllow(user, entity, "edit_role_permissions") && !!permissions) return res.status(403).json({ok: false})
if (!!label) await db.collection<Entity>("roles").updateOne({ id }, { $set: {label} });
if (!!permissions) await db.collection<Entity>("roles").updateOne({ id }, { $set: {permissions} });
return res.status(200).json({ok: true});
}

View File

@@ -1,40 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {getEntityWithRoles} from "@/utils/entities.be";
import client from "@/lib/mongodb";
import {Entity} from "@/interfaces/entity";
import { assignRoleToUsers, deleteRole, getRole, transferRole } from "@/utils/roles.be";
import { doesEntityAllow } from "@/utils/permissions";
import { findBy } from "@/utils";
import { getUser } from "@/utils/users.be";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return await post(req, res);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) return res.status(401).json({ ok: false })
const user = await getUser(req.session.user.id);
if (!user) return res.status(401).json({ ok: false })
const { id } = req.query as { id: string };
const {users} = req.body as {users: string[]}
const role = await getRole(id)
if (!role) return res.status(404).json({ok: false})
const entity = await getEntityWithRoles(role.entityID)
if (!entity) return res.status(404).json({ok: false})
if (!doesEntityAllow(user, entity, "assign_to_role")) return res.status(403).json({ok: false})
const result = await assignRoleToUsers(users, entity.id, role.id)
return res.status(200).json({ok: result.acknowledged});
}

Some files were not shown because too many files have changed in this diff Show More