Compare commits
21 Commits
d77336374d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c01168976f | ||
|
|
e27d8231e9 | ||
|
|
1544215008 | ||
|
|
3e143526e8 | ||
|
|
e750317757 | ||
|
|
65554ed4ce | ||
|
|
3c580498d3 | ||
|
|
5fb82abafd | ||
|
|
2bfb94d01b | ||
|
|
df84aaadf4 | ||
|
|
2789660e8a | ||
|
|
a78e6eb64f | ||
|
|
6c7d189957 | ||
|
|
31f2a21a76 | ||
|
|
c49b1c8070 | ||
|
|
d78654a30f | ||
|
|
655e019bf6 | ||
|
|
d7a8f496c0 | ||
|
|
5e363e9951 | ||
|
|
3370f3c648 | ||
|
|
53d6b0dd51 |
0
9c3d1115-4845-4980-a0d5-33767025c260
Normal file
0
9c3d1115-4845-4980-a0d5-33767025c260
Normal file
12
Dockerfile
12
Dockerfile
@@ -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
|
||||
|
||||
8
GOOGLE_CLOUD_ENCOACH_ANALYSIS_CHAT.md
Normal file
8
GOOGLE_CLOUD_ENCOACH_ANALYSIS_CHAT.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Google Cloud EnCoach Analysis
|
||||
|
||||
Open this Cursor transcript using the chat UUID:
|
||||
|
||||
- [Google Cloud EnCoach Analysis](9c3d1115-4845-4980-a0d5-33767025c260)
|
||||
|
||||
Notes:
|
||||
- This is the transcript where `talal@encoach.com` was used for the Google Cloud EnCoach infrastructure analysis.
|
||||
10505
package-lock.json
generated
Normal file
10505
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import validateBlanks from "../validateBlanks";
|
||||
import { toast } from "react-toastify";
|
||||
import setEditingAlert from "../../Shared/setEditingAlert";
|
||||
import PromptEdit from "../../Shared/PromptEdit";
|
||||
import { uuidv4 } from "@firebase/util";
|
||||
import { uuidv4 } from "@/utils/uuid";
|
||||
|
||||
interface Word {
|
||||
letter: string;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { toast } from "react-toastify";
|
||||
import setEditingAlert from "../../Shared/setEditingAlert";
|
||||
import { MdEdit, MdEditOff } from "react-icons/md";
|
||||
import MCOption from "./MCOption";
|
||||
import { uuidv4 } from "@firebase/util";
|
||||
import { uuidv4 } from "@/utils/uuid";
|
||||
|
||||
|
||||
const FillBlanksMC: React.FC<{ exercise: FillBlanksExercise; sectionId: number }> = ({ exercise, sectionId }) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { toast } from 'react-toastify';
|
||||
import { DragEndEvent } from '@dnd-kit/core';
|
||||
import { handleMatchSentencesReorder } from '@/stores/examEditor/reorder/local';
|
||||
import PromptEdit from '../Shared/PromptEdit';
|
||||
import { uuidv4 } from '@firebase/util';
|
||||
import { uuidv4 } from '@/utils/uuid';
|
||||
|
||||
const MatchSentences: React.FC<{ exercise: MatchSentencesExercise, sectionId: number }> = ({ exercise, sectionId }) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { MdAdd } from "react-icons/md";
|
||||
import Alert, { AlertItem } from "../../Shared/Alert";
|
||||
import PromptEdit from "../../Shared/PromptEdit";
|
||||
import { uuidv4 } from "@firebase/util";
|
||||
import { uuidv4 } from "@/utils/uuid";
|
||||
|
||||
|
||||
const UnderlineMultipleChoice: React.FC<{exercise: MultipleChoiceExercise, sectionId: number}> = ({
|
||||
|
||||
@@ -18,7 +18,7 @@ import SortableQuestion from '../../Shared/SortableQuestion';
|
||||
import setEditingAlert from '../../Shared/setEditingAlert';
|
||||
import { handleMultipleChoiceReorder } from '@/stores/examEditor/reorder/local';
|
||||
import PromptEdit from '../../Shared/PromptEdit';
|
||||
import { uuidv4 } from '@firebase/util';
|
||||
import { uuidv4 } from '@/utils/uuid';
|
||||
|
||||
interface MultipleChoiceProps {
|
||||
exercise: MultipleChoiceExercise;
|
||||
|
||||
@@ -16,7 +16,7 @@ import setEditingAlert from '../Shared/setEditingAlert';
|
||||
import { DragEndEvent } from '@dnd-kit/core';
|
||||
import { handleTrueFalseReorder } from '@/stores/examEditor/reorder/local';
|
||||
import PromptEdit from '../Shared/PromptEdit';
|
||||
import { uuidv4 } from '@firebase/util';
|
||||
import { uuidv4 } from '@/utils/uuid';
|
||||
|
||||
const TrueFalse: React.FC<{ exercise: TrueFalseExercise, sectionId: number }> = ({ exercise, sectionId }) => {
|
||||
const { currentModule, dispatch } = useExamEditorStore();
|
||||
|
||||
@@ -22,7 +22,7 @@ import { validateEmptySolutions, validateQuestionText, validateWordCount } from
|
||||
import { handleWriteBlanksReorder } from '@/stores/examEditor/reorder/local';
|
||||
import { ParsedQuestion, parseText, reconstructText } from './parsing';
|
||||
import PromptEdit from '../Shared/PromptEdit';
|
||||
import { uuidv4 } from '@firebase/util';
|
||||
import { uuidv4 } from '@/utils/uuid';
|
||||
|
||||
|
||||
const WriteBlanks: React.FC<{ sectionId: number; exercise: WriteBlanksExercise; }> = ({ sectionId, exercise }) => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { validateQuestions, validateEmptySolutions, validateWordCount } from "./
|
||||
import Header from "../../Shared/Header";
|
||||
import BlanksFormEditor from "./BlanksFormEditor";
|
||||
import PromptEdit from "../Shared/PromptEdit";
|
||||
import { uuidv4 } from "@firebase/util";
|
||||
import { uuidv4 } from "@/utils/uuid";
|
||||
|
||||
|
||||
const WriteBlanksForm: React.FC<{ sectionId: number; exercise: WriteBlanksExercise }> = ({ sectionId, exercise }) => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import ai_usage from "@/utils/ai.detection";
|
||||
import { calculateBandScore, getGradingLabel } from "@/utils/score";
|
||||
import moment from "moment";
|
||||
import { Assignment } from "@/interfaces/results";
|
||||
import { uuidv4 } from "@firebase/util";
|
||||
import { uuidv4 } from "@/utils/uuid";
|
||||
import { useRouter } from "next/router";
|
||||
import { uniqBy } from "lodash";
|
||||
import { sortByModule } from "@/utils/moduleUtils";
|
||||
|
||||
@@ -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: "",
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ObjectId } from "mongodb";
|
||||
import { Module } from ".";
|
||||
import { Type, User, userTypeLabels, userTypeLabelsShort } from "./user";
|
||||
|
||||
export interface ApprovalWorkflow {
|
||||
_id?: ObjectId,
|
||||
_id?: string,
|
||||
name: string,
|
||||
entityId: string,
|
||||
requester: User["id"],
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface ExamBase {
|
||||
access: AccessType;
|
||||
label?: string;
|
||||
requiresApproval?: boolean;
|
||||
approved?: boolean;
|
||||
}
|
||||
export interface ReadingExam extends ExamBase {
|
||||
module: "reading";
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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
136
src/lib/odoo.ts
Normal 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};
|
||||
@@ -6,7 +6,7 @@ export const sessionOptions: IronSessionOptions = {
|
||||
password: process.env.SECRET_COOKIE_PASSWORD as string,
|
||||
cookieName: "eCrop/ielts",
|
||||
cookieOptions: {
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
secure: process.env.COOKIE_SECURE === "true",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ export const sessionOptions: IronSessionOptions = {
|
||||
declare module "iron-session" {
|
||||
interface IronSessionData {
|
||||
user?: User | null;
|
||||
token?: string;
|
||||
envVariables?: {[key: string]: string};
|
||||
}
|
||||
}
|
||||
|
||||
5
src/mongodb.d.ts
vendored
5
src/mongodb.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
import {MongoClient} from "mongodb";
|
||||
|
||||
declare global {
|
||||
var _mongoClientPromise: Promise<MongoClient>;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
3
src/pages/api/[...path].ts
Normal file
3
src/pages/api/[...path].ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import {proxyToOdoo} from "@/lib/odoo";
|
||||
|
||||
export default proxyToOdoo();
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
@@ -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!",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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"});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import axios from "axios";
|
||||
import queryToURLSearchParams from "@/utils/query.to.url.params";
|
||||
import {ODOO_URL} from "@/lib/odoo";
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "POST") return post(req, res);
|
||||
if (req.method !== "POST") return res.status(404).json({ok: false});
|
||||
if (!req.session.token) return res.status(401).json({ok: false});
|
||||
|
||||
return res.status(404).json({ok: false});
|
||||
const examModule = req.query.module as string;
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${ODOO_URL}/api/exam/${examModule}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${req.session.token}`,
|
||||
"Content-Type": req.headers["content-type"] || "application/json",
|
||||
},
|
||||
body: typeof req.body === "object" ? JSON.stringify(req.body) : req.body,
|
||||
});
|
||||
|
||||
const data = await resp.json();
|
||||
return res.status(resp.status).json(data);
|
||||
} catch {
|
||||
return res.status(502).json({error: "Backend unavailable"});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) return res.status(401).json({ok: false});
|
||||
|
||||
const queryParams = queryToURLSearchParams(req);
|
||||
const endpoint = queryParams.getAll('module').join("/");
|
||||
|
||||
const result = await axios.post(`${process.env.BACKEND_URL}/${endpoint}`,
|
||||
req.body,
|
||||
{
|
||||
headers: {Authorization: `Bearer ${process.env.BACKEND_JWT}`},
|
||||
},
|
||||
);
|
||||
|
||||
res.status(200).json(result.data);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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>);
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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"});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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)],
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}`,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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"});
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {getEntityWithRoles} from "@/utils/entities.be";
|
||||
import client from "@/lib/mongodb";
|
||||
import {Entity} from "@/interfaces/entity";
|
||||
import { assignRoleToUsers, deleteRole, getRole, transferRole } from "@/utils/roles.be";
|
||||
import { doesEntityAllow } from "@/utils/permissions";
|
||||
import { findBy } from "@/utils";
|
||||
import { getUser } from "@/utils/users.be";
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "POST") return await post(req, res);
|
||||
}
|
||||
|
||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) return res.status(401).json({ ok: false })
|
||||
|
||||
const user = await getUser(req.session.user.id);
|
||||
if (!user) return res.status(401).json({ ok: false })
|
||||
|
||||
const { id } = req.query as { id: string };
|
||||
const {users} = req.body as {users: string[]}
|
||||
|
||||
const role = await getRole(id)
|
||||
if (!role) return res.status(404).json({ok: false})
|
||||
|
||||
const entity = await getEntityWithRoles(role.entityID)
|
||||
if (!entity) return res.status(404).json({ok: false})
|
||||
|
||||
if (!doesEntityAllow(user, entity, "assign_to_role")) return res.status(403).json({ok: false})
|
||||
|
||||
const result = await assignRoleToUsers(users, entity.id, role.id)
|
||||
return res.status(200).json({ok: result.acknowledged});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user