Compare commits

..

14 Commits

Author SHA1 Message Date
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
145 changed files with 12326 additions and 12397 deletions

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

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

@@ -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

@@ -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

@@ -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

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

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 module = req.query.module as string;
try {
const resp = await fetch(`${ODOO_URL}/api/exam/${module}`, {
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});
}

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 {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {getEntities, getEntitiesWithRoles, getEntity, getEntityWithRoles} from "@/utils/entities.be";
import {Entity} from "@/interfaces/entity";
import {v4} from "uuid";
import { createRole, getRoles, getRolesByEntity } from "@/utils/roles.be";
import { mapBy } from "@/utils";
import { RolePermission } from "@/resources/entityPermissions";
import { doesEntityAllow } from "@/utils/permissions";
import { User } from "@/interfaces/user";
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 });
if (["admin", "developer"].includes(user.type)) return res.status(200).json(await getRoles());
res.status(200).json(await getRoles(mapBy(user.entities, 'role')));
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const {entityID, label, permissions} = req.body as {entityID: string, label: string, permissions: RolePermission[]}
const entity = await getEntityWithRoles(entityID)
if (!entity) return res.status(404).json({ok: false})
if (!doesEntityAllow(user, entity, "create_entity_role")) return res.status(403).json({ok: false})
const role = {
id: v4(),
entityID,
label,
permissions
}
await createRole(role)
return res.status(200).json(role);
}

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";
import {Session} from "@/hooks/useSessions";
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);
}
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("sessions").findOne({ id: id });
if (docSnap) {
res.status(200).json(docSnap);
} else {
res.status(404).json(undefined);
}
}
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("sessions").findOne({ id: id });
if (!docSnap) return res.status(404).json({ok: false});
await db.collection("sessions").deleteOne({ id: id });
return res.status(200).json({ok: true});
}

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 { Session } from "@/hooks/useSessions";
import moment from "moment";
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);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const { user } = req.query as { user?: string };
const q = user ? { user: user } : {};
const sessions = await db.collection("sessions").find<Session>({
...q,
}).limit(12).sort({ date: -1 }).toArray();
console.log(sessions)
res.status(200).json(
sessions.filter((x) => {
if (!x.assignment) return true;
if (x.assignment.results.filter((y) => y.user === user).length > 0) return false;
return !moment().isAfter(moment(x.assignment.endDate));
}),
);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const session = req.body;
await db.collection("sessions").updateOne({ id: session.id }, { $set: session }, { upsert: true });
res.status(200).json({ ok: true });
}

View File

@@ -1,25 +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 {getDownloadURL, getStorage, ref} from "firebase/storage";
import {app, storage} from "@/firebase";
import axios from "axios";
import {proxyToOdoo} from "@/lib/odoo";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {path} = req.body as {path: string};
const pathReference = ref(storage, path);
const url = await getDownloadURL(pathReference);
const response = await axios.get(url, {responseType: "arraybuffer"});
res.status(200).send(response.data);
}
export default proxyToOdoo("/api/exam/speaking/media");

View File

@@ -1,205 +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 { getDownloadURL, getStorage, ref } from "firebase/storage";
import { app, storage } from "@/firebase";
import axios from "axios";
import { requestUser } from "@/utils/api";
import { checkAccess } from "@/utils/permissions";
import { EntityWithRoles } from "@/interfaces/entity";
import { Stat, StudentUser } from "@/interfaces/user";
import { Assignment, AssignmentResult } from "@/interfaces/results";
import { Exam } from "@/interfaces/exam";
import { capitalize, groupBy, uniqBy } from "lodash";
import { findBy, mapBy } from "@/utils";
import ExcelJS from "exceljs";
import moment from "moment";
import { Session } from "@/hooks/useSessions";
import { getGradingSystemByEntity } from "@/utils/grading.be";
import { getGradingLabel } from "@/utils/score";
export default withIronSessionApiRoute(handler, sessionOptions);
interface Item {
student: StudentUser
result: AssignmentResult
assignment: Assignment
exams: Exam[]
session?: Session
}
interface Body {
entities: EntityWithRoles[]
items: Item[]
assignments: Assignment[]
startDate: Date
endDate: Date
}
interface EntityInformation {
entity: EntityWithRoles
exams: Exam[]
numberOfAssignees: number
numberOfSubmissions: number
numberOfAbsentees: number
assignment: Assignment
items: Item[]
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") return res.status(404).json({ ok: false })
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
if (!checkAccess(user, ['admin', 'developer', 'mastercorporate', 'corporate'])) return res.status(403).json({ ok: false });
const { entities, items, assignments } = req.body as Body
const entityInformations: EntityInformation[] = []
for (const entity of entities) {
const entityItems = items.filter(i => i.assignment.entity === entity.id)
const groupedByAssignments = groupBy(entityItems, (a) => a.assignment.id)
for (const assignmentID of Object.keys(groupedByAssignments)) {
const assignmentItems = groupedByAssignments[assignmentID]
const assignment = findBy(assignments, 'id', assignmentID)!
const assignmentExams =
uniqBy(assignmentItems.flatMap(a => a.exams.map(e => ({ ...e, moduleID: `${e.id}_${e.module}` }))), 'moduleID')
const assignmentEntityInformation: EntityInformation = {
entity,
exams: assignmentExams,
numberOfAssignees: assignmentItems.length,
numberOfSubmissions: assignmentItems.filter(x => !!x.result).length,
numberOfAbsentees: assignmentItems.filter(x => !x.result).length,
assignment,
items: assignmentItems
}
entityInformations.push(assignmentEntityInformation)
}
}
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Statistical");
for (const e of entityInformations) {
await addEntityInformationToWorksheet(worksheet, e)
}
const buffer = await workbook.xlsx.writeBuffer()
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
res.status(200).send(buffer);
}
const addEntityInformationToWorksheet = async (worksheet: ExcelJS.Worksheet, entityInformation: EntityInformation) => {
const data = [
['Entity', undefined, undefined, entityInformation.entity.label],
['Assignment', undefined, undefined, entityInformation.assignment.name],
['Date of the Assignment', undefined, undefined, moment(entityInformation.assignment.startDate).format("DD/MM/YYYY")],
['Exams', undefined, undefined, mapBy(entityInformation.exams, 'id').join(', ')],
['Modules', undefined, undefined, entityInformation.exams.map(e => capitalize(e.module)).join(', ')],
['Number of Assignees', undefined, undefined, entityInformation.numberOfAssignees],
['Number of Submissions', undefined, undefined, entityInformation.numberOfSubmissions],
['Number of Absentees', undefined, undefined, entityInformation.numberOfAbsentees]
]
const dataRows = worksheet.addRows(data);
dataRows.forEach(row => row.getCell(1).font = { bold: true, color: { argb: "ffffffff" } })
dataRows.forEach(row => row.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "ff674ea7" } })
dataRows.forEach(row => worksheet.mergeCells(`${row.getCell(1).address}:${row.getCell(3).address}`))
dataRows.forEach(row => row.getCell(4).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "FFD9D2E9" } })
dataRows.forEach(row => worksheet.mergeCells(`${row.getCell(4).address}:${row.getCell(7).address}`))
worksheet.addRows([[], []]);
const gradingSystem = await getGradingSystemByEntity(entityInformation.entity.id)
for (const exam of entityInformation.exams) {
const examRow = worksheet.addRow([`${capitalize(exam.module)} Exam`, undefined, exam.id])
examRow.getCell(1).font = { bold: true, color: { argb: "ffffffff" } }
examRow.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "ff674ea7" } }
examRow.getCell(3).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "FFD9D2E9" } }
worksheet.mergeCells(`${examRow.getCell(1).address}:${examRow.getCell(2).address}`)
worksheet.mergeCells(`${examRow.getCell(3).address}:${examRow.getCell(6).address}`)
const parts = exam.module === "level" || exam.module === "listening" || exam.module === "reading" ? exam.parts : []
const header = worksheet.addRow([
"#",
"Name",
"E-mail",
"Student ID",
"Passport/ID",
"Gender",
"Finished at",
"Score",
...(exam.module === "level" ? ["Grade"] : []),
...parts.map((_, i) => `Part ${i + 1}`)
])
header.font = { bold: true, color: { argb: "FFFFFFFF" } }
header.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "FFD9D2E9" } }
const examItems =
entityInformation.items
.filter(i => !!i.result)
.map(i => ({
...i,
result: { ...i.result, stats: i.result.stats.filter(x => x.exam === exam.id) },
}))
const orderedItems = examItems.sort((a, b) => {
const aTotalScore = a.result.stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.correct, 0)
const bTotalScore = b.result.stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.correct, 0)
return bTotalScore - aTotalScore
})
const itemRows = orderedItems.map((item, index) => {
const { total, correct } = calculateScore(item.result.stats)
const score = `${correct} / ${total}`
const finishTimestamp = [...item.result.stats].sort((a, b) => b.date - a.date).shift()?.date || -1
const finishDate = finishTimestamp === -1 ? "N/A" : moment(new Date(finishTimestamp)).format("DD/MM/YYYY HH:mm")
const grade = getGradingLabel(correct, gradingSystem.steps)
return [
index + 1,
item.student.name,
item.student.email,
item.student.studentID || "N/A",
item.student.demographicInformation?.passport_id || "N/A",
item.student.demographicInformation?.gender || "N/A",
finishDate,
score,
...(exam.module === "level" ? [grade] : []),
...parts.map((part) => {
const exerciseIDs = mapBy(part.exercises, 'id')
const { total, correct } = calculateScore(item.result.stats.filter(s => exerciseIDs.includes(s.exercise)))
return `${correct} / ${total}`
})
]
})
worksheet.addRows(itemRows)
worksheet.addRows([[]]);
}
worksheet.addRows([[], []]);
}
const calculateScore = (stats: Stat[]) => {
const total = stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.total, 0)
const correct = stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.correct, 0)
return { total, correct }
}
export const config = {
api: {
bodyParser: {
sizeLimit: '20mb',
},
},
};

View File

@@ -1,421 +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 TestReport from "@/exams/pdf/test.report";
import LevelTestReport from "@/exams/pdf/level.test.report";
import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
import {
DemographicInformation,
Stat,
StudentUser,
User,
} from "@/interfaces/user";
import { Module } from "@/interfaces";
import { ModuleScore } from "@/interfaces/module.scores";
import { SkillExamDetails } from "@/exams/pdf/details/skill.exam";
import { calculateBandScore } from "@/utils/score";
import axios from "axios";
import { moduleLabels } from "@/utils/moduleUtils";
import {
generateQRCode,
getRadialProgressPNG,
streamToBuffer,
} from "@/utils/pdf";
import moment from "moment-timezone";
import { getCorporateNameForStudent } from "@/utils/groups.be";
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 demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. Continuing to challenge oneself with advanced material in writing, speaking, listening, and reading will further refine the already impressive command of the English language.";
}
if (score > 0.6) {
return "Scoring between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflects a commendable level of proficiency in each domain. There's evidence of a solid grasp of key concepts, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for further mastery.";
}
if (score > 0.4) {
return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading demonstrates a moderate level of understanding in each domain. While there's a commendable grasp of key concepts, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. Consistent effort and targeted focus on weaker areas are recommended.";
}
if (score > 0.2) {
return "Scoring between 21% and 40% on the English exam, spanning writing, speaking, listening, and reading, indicates some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills. Strengthening writing, speaking, listening, and reading abilities through consistent effort and focused study will contribute to overall proficiency.";
}
return "This student's performance on the English exam, encompassing writing, speaking, listening, and reading, reflects a significant need for improvement, scoring between 0% and 20%. There's a notable gap in understanding key concepts across all language domains. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial. Developing a consistent study routine and seeking additional 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 showcases an outstanding level of understanding and proficiency. Your performance reflects a mastery of key concepts, including grammar, vocabulary, and comprehension. You exhibit a high level of skill in applying these elements effectively. Your dedication to excellence is evident, and your consistent, stellar performance is commendable. Continue to challenge yourself with advanced material to further refine your already impressive command of the English language. Your commitment to excellence positions you as a standout student in English studies, and your achievements are a testament to your hard work and capability.";
}
if (score > 0.6) {
return "Scoring between 61% and 80% on the English exam reflects a commendable level of understanding and proficiency. You have demonstrated a solid grasp of key concepts, including grammar, vocabulary, and comprehension. There's evidence of effective application of skills, but room for refinement and deeper exploration remains. Consistent effort in honing nuanced aspects of language will contribute to even greater mastery. Continue engaging with challenging material and seeking opportunities for advanced comprehension. With sustained dedication, you have the potential to elevate your performance to an exceptional level and further excel in your English studies.";
}
if (score > 0.4) {
return "Scoring between 41% and 60% on the English exam reflects a moderate level of understanding. You demonstrate a grasp of some key concepts, but there's room for refinement in areas like grammar, vocabulary, and comprehension. Consistent effort and a strategic focus on weaker areas can lead to notable improvement. Engaging with supplementary resources and seeking feedback will further enhance your skills. With continued dedication, there's a solid foundation to build upon, and achieving a higher level of proficiency is within reach. Keep up the good work and aim for sustained progress in your English studies.";
}
if (score > 0.2) {
return "Scoring between 21% and 40% on the English exam shows some understanding of key concepts, but there's still ample room for improvement. Strengthening foundational skills, such as grammar, vocabulary, and comprehension, is essential. Consistent effort and focused study can help bridge gaps in knowledge and elevate your performance. Consider seeking additional guidance or resources to refine your understanding of the material. With commitment and targeted improvements, you have the potential to make significant strides in your English proficiency.";
}
return "Your performance on the English exam falls within the 0% to 20% range, indicating a need for improvement. There's room to enhance your grasp of fundamental concepts like grammar, vocabulary, and comprehension. Establishing a consistent study routine and seeking extra support can be beneficial. With dedication and targeted efforts, you have the potential to significantly boost your performance in upcoming assessments.";
};
const getPerformanceSummary = (module: Module, score: number) => {
if (module === "level") return getLevelSummary(score);
return getExamSummary(score);
};
interface SkillsFeedbackRequest {
code: Module;
name: string;
grade: number;
}
interface SkillsFeedbackResponse extends SkillsFeedbackRequest {
evaluation: string;
suggestions: string;
bullet_points?: string[];
}
const getSkillsFeedback = async (sections: SkillsFeedbackRequest[]) => {
const backendRequest = await axios.post(
`${process.env.BACKEND_URL}/grade/summary`,
{ sections },
{
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
}
);
return backendRequest.data?.sections;
};
// perform the request with several retries if needed
const handleSkillsFeedbackRequest = async (
sections: SkillsFeedbackRequest[]
): Promise<SkillsFeedbackResponse[] | null> => {
let i = 0;
try {
const data = await getSkillsFeedback(sections);
return data;
} catch (err) {
if (i < 3) {
i++;
return handleSkillsFeedbackRequest(sections);
}
return null;
}
};
async function getDefaultPDFStream(
stats: Stat[],
user: User,
qrcodeUrl: string
) {
const [stat] = stats;
// generate the QR code for the report
const qrcode = await generateQRCode(qrcodeUrl);
if (!qrcode) {
throw new Error("Failed to generate QR code");
}
// stats may contain multiple exams of the same type so we need to aggregate them
const results = stats
.reduce((accm: ModuleScore[], stat: Stat) => {
const { module, score } = stat;
const fixedModuleStr = module[0].toUpperCase() + module.substring(1);
if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) {
return accm.map((e: ModuleScore) => {
if (e.module === fixedModuleStr) {
return {
...e,
score: e.score + score.correct,
total: e.total + score.total,
};
}
return e;
});
}
const value = {
module: fixedModuleStr,
score: score.correct,
total: score.total,
code: module,
} as ModuleScore;
return [...accm, value];
}, [])
.map((moduleScore: ModuleScore) => {
const { score, total } = moduleScore;
// with all the scores aggreated we can calculate the band score for each module
const bandScore = calculateBandScore(
score,
total,
moduleScore.code as Module,
user.focus
);
return {
...moduleScore,
// generate the closest radial progress png for the score
png: getRadialProgressPNG("azul", score, total),
bandScore,
};
});
// get the skills feedback from the backend based on the module grade
const skillsFeedback = (await handleSkillsFeedbackRequest(
results.map(({ code, bandScore }) => ({
code,
name: moduleLabels[code],
grade: bandScore,
}))
)) as SkillsFeedbackResponse[];
if (!skillsFeedback) {
throw new Error("Failed to get skills feedback");
}
// assign the feedback to the results
const finalResults = results.map((result) => {
const feedback = skillsFeedback.find(
(f: SkillsFeedbackResponse) => f.code === result.code
);
if (feedback) {
return {
...result,
evaluation: feedback?.evaluation,
suggestions: feedback?.suggestions,
bullet_points: feedback?.bullet_points,
};
}
return result;
});
// calculate the overall score out of all the aggregated results
const overallScore = results.reduce((accm, { score }) => accm + score, 0);
const overallTotal = results.reduce((accm, { total }) => accm + total, 0);
const overallResult = overallScore / overallTotal;
const overallPNG = getRadialProgressPNG(
"laranja",
overallScore,
overallTotal
);
// generate the overall detail report
const overallDetail = {
module: "Overall",
score: overallScore,
total: overallTotal,
png: overallPNG,
} as ModuleScore;
const testDetails = [overallDetail, ...finalResults];
// generate the performance summary based on the overall result
const performanceSummary = getPerformanceSummary(stat.module, overallResult);
const title = "ENGLISH SKILLS TEST RESULT REPORT";
const details = <SkillExamDetails testDetails={testDetails} />;
const demographicInformation =
user.demographicInformation as DemographicInformation;
return ReactPDF.renderToStream(
<TestReport
title={title}
date={moment(stat.date)
.tz(user.demographicInformation?.timezone || "UTC")
.format("ll HH:mm:ss")}
name={user.name}
email={user.email}
id={user.id}
gender={demographicInformation?.gender}
summary={performanceSummary}
testDetails={testDetails}
renderDetails={details}
logo={"public/logo_title.png"}
qrcode={qrcode}
summaryPNG={overallPNG}
summaryScore={`${Math.floor(overallResult * 100)}%`}
passportId={demographicInformation?.passport_id || ""}
/>
);
}
async function getPdfUrl(pdfStream: any, docsSnap: any) {
// generate the file ref for storage
const fileName = `${Date.now().toString()}.pdf`;
const refName = `exam_report/${fileName}`;
const fileRef = ref(storage, refName);
// upload the pdf to storage
const pdfBuffer = await streamToBuffer(pdfStream);
await uploadBytes(fileRef, pdfBuffer, {
contentType: "application/pdf",
});
// update the stats entries with the pdf url to prevent duplication
await db.collection("stats").updateOne(
{ id: docsSnap.id },
{
$set: {
pdf: {
path: refName,
version: process.env.PDF_VERSION,
}
}
}
);
return getDownloadURL(fileRef);
}
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 };
// fetch stats entries for this particular user with the requested exam session
const stats = await db.collection("stats").find<Stat>({ session: id }).toArray();
if (stats.length == 0) {
res.status(400).end();
return;
}
// verify if the stats already have a pdf generated
const hasPDF = stats.find(
(s) => s.pdf?.path && s.pdf?.version === process.env.PDF_VERSION
);
// find the user that generated the stats
const statIndex = stats.findIndex((s) => s.user);
if (statIndex === -1) {
res.status(401).json({ ok: false });
return;
}
const userId = stats[statIndex].user;
if (hasPDF) {
// if it does, return the pdf url
const fileRef = ref(storage, hasPDF.pdf!.path);
const url = await getDownloadURL(fileRef);
res.status(200).end(url);
return;
}
try {
// generate the pdf report
const docUser = await db.collection("users").findOne<User>({ id: userId});
if (docUser) {
// we'll need the user in order to get the user data (name, email, focus, etc);
const [stat] = stats;
if (stat.module === "level") {
const user = docUser as StudentUser;
const uniqueExercises = stats.map((s) => ({
name: "Gramar & Vocabulary",
result: `${s.score.correct}/${s.score.total}`,
}));
const dates = stats.map((s) => moment(s.date));
const timeSpent = `${stats.reduce((accm, s: Stat) => accm + (s.timeSpent || 0), 0) / 60
} minutes`;
const score = stats.reduce((accm, s) => accm + s.score.correct, 0);
const corporateName = await getCorporateNameForStudent(userId);
const pdfStream = await ReactPDF.renderToStream(
<LevelTestReport
date={moment.max(dates).format("DD/MM/YYYY")}
name={user.name}
email={user.email}
id={stat.exam}
gender={user.demographicInformation?.gender || ""}
passportId={user.demographicInformation?.passport_id || ""}
corporateName={corporateName}
downloadDate={moment().format("DD/MM/YYYY")}
userId={userId}
uniqueExercises={uniqueExercises}
timeSpent={timeSpent}
score={score.toString()}
/>
);
const url = await getPdfUrl(pdfStream, docUser);
res.status(200).end(url);
return;
}
const user = docUser as User;
try {
const pdfStream = await getDefaultPDFStream(
stats,
user,
`${req.headers.origin || ""}${req.url}`
);
const url = await getPdfUrl(pdfStream, docUser);
res.status(200).end(url);
return;
} catch (err) {
console.error(err);
res.status(500).json({ ok: false });
return;
}
}
res.status(401).json({ ok: false });
return;
} catch (err) {
console.error(err);
res.status(500).json({ ok: false });
return;
}
}
res.status(401).json({ ok: false });
return;
}
async function get(req: NextApiRequest, res: NextApiResponse) {
const { id } = req.query as { id: string };
const stats = await db.collection("stats").find<Stat>({ session: id }).toArray();
if (stats.length == 0) {
res.status(404).end();
return;
}
const hasPDF = stats.find((s) => s.pdf?.path);
if (hasPDF) {
const fileRef = ref(storage, hasPDF.pdf!.path);
const url = await getDownloadURL(fileRef);
return res.redirect(url);
}
res.status(500).end();
}

View File

@@ -1,20 +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";
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);
res.status(404).json({ok: false});
}
async function GET(req: NextApiRequest, res: NextApiResponse) {
const {id} = req.query;
const snapshot = await db.collection("stats").findOne({ id: id as string});
if (!snapshot) return res.status(404).json({id: id as string});
res.status(200).json({...snapshot, id: snapshot.id});
}

View File

@@ -1,46 +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 { requestUser } from "@/utils/api";
import { UserSolution } from "@/interfaces/exam";
import { WithId } from "mongodb";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
if (req.method === "POST") return post(req, res);
}
interface Body {
solutions: UserSolution[];
sessionId: string;
userId: string;
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const { userId, solutions, sessionId } = req.body as Body;
const disabledStats = await db.collection("stats").find(
{ user: userId, session: sessionId, isDisabled: true }
).toArray();
await Promise.all(disabledStats.map(async (stat) => {
const matchingSolution = solutions.find(s => s.exercise === stat.exercise);
if (matchingSolution) {
const { _id, ...updateFields } = matchingSolution as WithId<UserSolution>;
await db.collection("stats").updateOne(
{ id: stat.id },
{ $set: { ...updateFields } }
);
}
}));
return res.status(200).json({ ok: true });
}

View File

@@ -1,69 +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 { Stat } from "@/interfaces/user";
import { Assignment } from "@/interfaces/results";
import { groupBy } from "lodash";
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);
if (req.method === "POST") return 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 snapshot = await db.collection("stats").find<Stat>({}).toArray();
res.status(200).json(snapshot);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (!user) return res.status(401).json({ ok: false });
const stats = req.body as Stat[];
stats.forEach(async (stat) => await db.collection("stats").updateOne(
{ id: stat.id },
{ $set: stat },
{ upsert: true }
));
stats.forEach(async (stat) => {
await db.collection("sessions").deleteOne({ id: stat.session })
});
const groupedStatsByAssignment = groupBy(
stats.filter((x) => !!x.assignment),
"assignment",
);
if (Object.keys(groupedStatsByAssignment).length > 0) {
const assignments = Object.keys(groupedStatsByAssignment);
assignments.forEach(async (assignmentId) => {
const assignmentStats = groupedStatsByAssignment[assignmentId] as Stat[];
const assignmentSnapshot = await db.collection("assignments").findOne<Assignment>({ id: assignmentId });
await db.collection("assignments").updateOne(
{ id: assignmentId },
{
$set: {
results: [
...assignmentSnapshot ? assignmentSnapshot.results : [],
{ user: user.id, type: user.focus, stats: assignmentStats },
],
}
}
);
});
}
res.status(200).json({ ok: true });
}

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 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;
}
const {session} = req.query;
const snapshot = await db.collection("stats").find({ user: req.session.user.id, session }).toArray();
res.status(200).json(snapshot);
}

View File

@@ -1,114 +0,0 @@
import { MODULES } from "@/constants/ielts";
import { app } from "@/firebase";
import { Module } from "@/interfaces";
import { Stat, User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session";
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
import { groupByModule, groupBySession } from "@/utils/stats";
import { MODULE_ARRAY } from "@/utils/moduleUtils";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { groupBy } from "lodash";
import { NextApiRequest, NextApiResponse } from "next";
import { requestUser } from "@/utils/api";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(update, sessionOptions);
async function update(req: NextApiRequest, res: NextApiResponse) {
const user = await requestUser(req, res)
if (user) {
const docUser = await db.collection("users").findOne({ id: user.id });
if (!docUser) {
res.status(401).json(undefined);
return;
}
const stats = await db.collection("stats").find<Stat>({ user: user.id }).toArray();
const groupedStats = groupBySession(stats);
const sessionLevels: { [key in Module]: { correct: number; total: number } }[] = Object.keys(groupedStats).map((key) => {
const sessionStats = groupedStats[key].map((stat) => ({ module: stat.module, correct: stat.score.correct, total: stat.score.total }));
const sessionLevels = {
reading: {
correct: 0,
total: 0,
},
listening: {
correct: 0,
total: 0,
},
writing: {
correct: 0,
total: 0,
},
speaking: {
correct: 0,
total: 0,
},
level: {
correct: 0,
total: 0,
},
};
MODULE_ARRAY.forEach((module: Module) => {
const moduleStats = sessionStats.filter((x) => x.module === module);
if (moduleStats.length === 0) return;
const moduleScore = moduleStats.reduce(
(accumulator, current) => ({ correct: accumulator.correct + current.correct, total: accumulator.total + current.total }),
{ correct: 0, total: 0 },
);
sessionLevels[module] = moduleScore;
});
return sessionLevels;
});
const readingLevel = sessionLevels
.map((x) => x.reading)
.filter((x) => x.total > 0)
.reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 });
const listeningLevel = sessionLevels
.map((x) => x.listening)
.filter((x) => x.total > 0)
.reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 });
const writingLevel = sessionLevels
.map((x) => x.writing)
.filter((x) => x.total > 0)
.reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 });
const speakingLevel = sessionLevels
.map((x) => x.speaking)
.filter((x) => x.total > 0)
.reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 });
const levelLevel = sessionLevels
.map((x) => x.level)
.filter((x) => x.total > 0)
.reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 });
const levels = {
reading: calculateBandScore(readingLevel.correct, readingLevel.total, "reading", user.focus),
listening: calculateBandScore(listeningLevel.correct, listeningLevel.total, "listening", user.focus),
writing: calculateBandScore(writingLevel.correct, writingLevel.total, "writing", user.focus),
speaking: calculateBandScore(speakingLevel.correct, speakingLevel.total, "speaking", user.focus),
level: calculateBandScore(levelLevel.correct, levelLevel.total, "level", user.focus),
};
const averageLevel = calculateAverageLevel(levels)
await db.collection("users").updateOne(
{ id: user.id },
{ $set: { levels, averageLevel } }
);
res.status(200).json({ ok: true });
} else {
res.status(401).json(undefined);
}
}

View File

@@ -1,20 +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 { getDetailedStatsByUser } from "../../../../utils/stats.be";
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const { user, query } = req.query as { user: string, query?: string };
const snapshot = await getDetailedStatsByUser(user, query);
res.status(200).json(snapshot);
}

View File

@@ -1,27 +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 {Session} from "@/hooks/useSessions";
import {deleteObject, getStorage, ref} from "firebase/storage";
import {proxyToOdoo} from "@/lib/odoo";
const storage = getStorage(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {path} = req.body as {path: string};
await deleteObject(ref(storage, path));
return res.status(200).json({ok: true});
}
export default proxyToOdoo("/api/storage/delete");

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