Migrate frontend from MongoDB/Firebase to Odoo 19 backend
- Remove all MongoDB and Firebase dependencies - Add Odoo proxy layer (src/lib/odoo.ts) for JWT-authenticated API forwarding - Rewrite auth routes (login, logout, register, reset) to use Odoo endpoints - Add catch-all API route ([...path].ts) to proxy remaining endpoints to Odoo - Rewrite special-case routes for file uploads and binary responses - Delete ~85 legacy API routes now handled by catch-all proxy - Replace *.be.ts MongoDB utilities with stub implementations for SSR compat - Update Dockerfile: remove MONGODB_URI, switch from yarn to npm - Update session.ts to store Odoo JWT token Made-with: Cursor
This commit is contained in:
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
|
||||
|
||||
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",
|
||||
|
||||
@@ -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,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};
|
||||
@@ -14,6 +14,7 @@ export const sessionOptions: IronSessionOptions = {
|
||||
declare module "iron-session" {
|
||||
interface IronSessionData {
|
||||
user?: User | null;
|
||||
token?: string;
|
||||
envVariables?: {[key: string]: string};
|
||||
}
|
||||
}
|
||||
|
||||
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,66 +1,72 @@
|
||||
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 === "GET") return get(req, res);
|
||||
if (req.method === "POST") return post(req, res);
|
||||
if (!req.session.token) return res.status(401).json({ok: false});
|
||||
|
||||
const modules = Array.isArray(req.query.module) ? req.query.module : [req.query.module];
|
||||
let endpoint = modules.join("/");
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
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"});
|
||||
}
|
||||
}
|
||||
|
||||
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 path = `/api/exam/${endpoint}${hasFiles ? "/attachment" : ""}`;
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of req) chunks.push(Buffer.from(chunk));
|
||||
const body = Buffer.concat(chunks);
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
async function get(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("/");
|
||||
|
||||
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/"
|
||||
}
|
||||
|
||||
queryParams.delete('module');
|
||||
const queryString = queryParams.toString();
|
||||
|
||||
const hasFiles = req.headers['content-type']?.startsWith('multipart/form-data');
|
||||
|
||||
// https://github.com/vercel/next.js/discussions/36153#discussioncomment-3029675
|
||||
// This just proxies the request
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
|
||||
@@ -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 {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 modules = Array.isArray(req.query.module) ? req.query.module : [req.query.module];
|
||||
const endpoint = modules[0];
|
||||
|
||||
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,
|
||||
{
|
||||
try {
|
||||
const resp = await fetch(`${ODOO_URL}/api/exam/${endpoint}/media`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||
Accept: 'audio/mpeg'
|
||||
Authorization: `Bearer ${req.session.token}`,
|
||||
"Content-Type": req.headers["content-type"] || "application/json",
|
||||
},
|
||||
responseType: 'arraybuffer',
|
||||
}
|
||||
);
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'audio/mpeg',
|
||||
'Content-Length': response.data.length
|
||||
body: typeof req.body === "object" ? JSON.stringify(req.body) : req.body,
|
||||
});
|
||||
|
||||
res.end(response.data);
|
||||
return;
|
||||
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);
|
||||
}
|
||||
|
||||
if (endpoint === "speaking") {
|
||||
const response = await axios.post(
|
||||
`${process.env.BACKEND_URL}/${endpoint}/media`,
|
||||
req.body,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||
const data = await resp.json();
|
||||
return res.status(resp.status).json(data);
|
||||
} catch {
|
||||
return res.status(502).json({error: "Backend unavailable"});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
res.status(200).json(response.data);
|
||||
return;
|
||||
}
|
||||
|
||||
return res.status(405).json({ "error": "Method not allowed."});
|
||||
}
|
||||
|
||||
@@ -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 {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,
|
||||
{
|
||||
try {
|
||||
const resp = await fetch(`${ODOO_URL}/api/exam/listening/instructions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||
Accept: 'audio/mpeg'
|
||||
Authorization: `Bearer ${req.session.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
responseType: 'arraybuffer',
|
||||
}
|
||||
);
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'audio/mpeg',
|
||||
'Content-Length': response.data.length
|
||||
body: JSON.stringify(req.body),
|
||||
});
|
||||
|
||||
res.end(response.data);
|
||||
return;
|
||||
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 module = req.query.module as string;
|
||||
|
||||
|
||||
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}`},
|
||||
try {
|
||||
const resp = await fetch(`${ODOO_URL}/api/exam/${module}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${req.session.token}`,
|
||||
"Content-Type": req.headers["content-type"] || "application/json",
|
||||
},
|
||||
);
|
||||
body: typeof req.body === "object" ? JSON.stringify(req.body) : req.body,
|
||||
});
|
||||
|
||||
res.status(200).json(result.data);
|
||||
const data = await resp.json();
|
||||
return res.status(resp.status).json(data);
|
||||
} catch {
|
||||
return res.status(502).json({error: "Backend unavailable"});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
try {
|
||||
const resp = await fetch(`${ODOO_URL}/api/login`, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({email: email.toLowerCase(), password}),
|
||||
});
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
const user = await db.collection("users").findOne<User>({id: userId});
|
||||
const data = await resp.json();
|
||||
|
||||
if (!user) return res.status(401).json({error: 401, message: "User does not exist!"});
|
||||
if (!resp.ok) {
|
||||
return res.status(resp.status).json(data);
|
||||
}
|
||||
|
||||
req.session.user = {...user, id: userId};
|
||||
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: {...user, id: userId}});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
res.status(401).json({error});
|
||||
});
|
||||
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 {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),
|
||||
});
|
||||
|
||||
const data = await resp.json();
|
||||
|
||||
if (!resp.ok) {
|
||||
return res.status(resp.status).json(data);
|
||||
}
|
||||
|
||||
async function registerCorporate(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { email, password } = req.body as {
|
||||
email: string;
|
||||
password: string;
|
||||
corporateInformation: CorporateInformation;
|
||||
};
|
||||
|
||||
createUserWithEmailAndPassword(auth, email.toLowerCase(), password)
|
||||
.then(async (userCredentials) => {
|
||||
const userId = userCredentials.user.uid;
|
||||
delete req.body.password;
|
||||
|
||||
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();
|
||||
|
||||
res.status(200).json({ user });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
res.status(401).json({ error });
|
||||
});
|
||||
return res.status(200).json({user});
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {getEntities, getEntitiesWithRoles, getEntity, getEntityWithRoles} from "@/utils/entities.be";
|
||||
import {Entity} from "@/interfaces/entity";
|
||||
import {v4} from "uuid";
|
||||
import { createRole, getRoles, getRolesByEntity } from "@/utils/roles.be";
|
||||
import { mapBy } from "@/utils";
|
||||
import { RolePermission } from "@/resources/entityPermissions";
|
||||
import { doesEntityAllow } from "@/utils/permissions";
|
||||
import { User } from "@/interfaces/user";
|
||||
import { requestUser } from "@/utils/api";
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") return await get(req, res);
|
||||
if (req.method === "POST") return await post(req, res);
|
||||
}
|
||||
|
||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await requestUser(req, res)
|
||||
if (!user) return res.status(401).json({ ok: false });
|
||||
|
||||
if (["admin", "developer"].includes(user.type)) return res.status(200).json(await getRoles());
|
||||
res.status(200).json(await getRoles(mapBy(user.entities, 'role')));
|
||||
}
|
||||
|
||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await requestUser(req, res)
|
||||
if (!user) return res.status(401).json({ ok: false });
|
||||
|
||||
const {entityID, label, permissions} = req.body as {entityID: string, label: string, permissions: RolePermission[]}
|
||||
|
||||
const entity = await getEntityWithRoles(entityID)
|
||||
if (!entity) return res.status(404).json({ok: false})
|
||||
|
||||
if (!doesEntityAllow(user, entity, "create_entity_role")) return res.status(403).json({ok: false})
|
||||
|
||||
const role = {
|
||||
id: v4(),
|
||||
entityID,
|
||||
label,
|
||||
permissions
|
||||
}
|
||||
|
||||
await createRole(role)
|
||||
return res.status(200).json(role);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import client from "@/lib/mongodb";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {Session} from "@/hooks/useSessions";
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") return get(req, res);
|
||||
if (req.method === "DELETE") return del(req, res);
|
||||
}
|
||||
|
||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
const {id} = req.query as {id: string};
|
||||
|
||||
const docSnap = await db.collection("sessions").findOne({ id: id });
|
||||
|
||||
if (docSnap) {
|
||||
res.status(200).json(docSnap);
|
||||
} else {
|
||||
res.status(404).json(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async function del(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
const {id} = req.query as {id: string};
|
||||
|
||||
const docSnap = await db.collection("sessions").findOne({ id: id });
|
||||
|
||||
if (!docSnap) return res.status(404).json({ok: false});
|
||||
await db.collection("sessions").deleteOne({ id: id });
|
||||
|
||||
return res.status(200).json({ok: true});
|
||||
}
|
||||
@@ -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 { Session } from "@/hooks/useSessions";
|
||||
import moment from "moment";
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") return get(req, res);
|
||||
if (req.method === "POST") return post(req, res);
|
||||
}
|
||||
|
||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const { user } = req.query as { user?: string };
|
||||
|
||||
const q = user ? { user: user } : {};
|
||||
const sessions = await db.collection("sessions").find<Session>({
|
||||
...q,
|
||||
}).limit(12).sort({ date: -1 }).toArray();
|
||||
console.log(sessions)
|
||||
|
||||
res.status(200).json(
|
||||
sessions.filter((x) => {
|
||||
if (!x.assignment) return true;
|
||||
if (x.assignment.results.filter((y) => y.user === user).length > 0) return false;
|
||||
return !moment().isAfter(moment(x.assignment.endDate));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
const session = req.body;
|
||||
|
||||
await db.collection("sessions").updateOne({ id: session.id }, { $set: session }, { upsert: true });
|
||||
|
||||
res.status(200).json({ ok: true });
|
||||
const sessions = await db.collection("sessions").find<Session>({ user: session.user }, { projection: { id: 1 } }).sort({ date: 1 }).toArray();
|
||||
// Delete old sessions
|
||||
if (sessions.length > 5) {
|
||||
await db.collection("sessions").deleteOne({ id: { $in: sessions.slice(0, sessions.length - 5).map(x => x.id) } });
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,3 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {getDownloadURL, getStorage, ref} from "firebase/storage";
|
||||
import {app, storage} from "@/firebase";
|
||||
import axios from "axios";
|
||||
import {proxyToOdoo} from "@/lib/odoo";
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
const {path} = req.body as {path: string};
|
||||
|
||||
const pathReference = ref(storage, path);
|
||||
const url = await getDownloadURL(pathReference);
|
||||
|
||||
const response = await axios.get(url, {responseType: "arraybuffer"});
|
||||
|
||||
res.status(200).send(response.data);
|
||||
}
|
||||
export default proxyToOdoo("/api/exam/speaking/media");
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { getDownloadURL, getStorage, ref } from "firebase/storage";
|
||||
import { app, storage } from "@/firebase";
|
||||
import axios from "axios";
|
||||
import { requestUser } from "@/utils/api";
|
||||
import { checkAccess } from "@/utils/permissions";
|
||||
import { EntityWithRoles } from "@/interfaces/entity";
|
||||
import { Stat, StudentUser } from "@/interfaces/user";
|
||||
import { Assignment, AssignmentResult } from "@/interfaces/results";
|
||||
import { Exam } from "@/interfaces/exam";
|
||||
import { capitalize, groupBy, uniqBy } from "lodash";
|
||||
import { findBy, mapBy } from "@/utils";
|
||||
import ExcelJS from "exceljs";
|
||||
import moment from "moment";
|
||||
import { Session } from "@/hooks/useSessions";
|
||||
import { getGradingSystemByEntity } from "@/utils/grading.be";
|
||||
import { getGradingLabel } from "@/utils/score";
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
interface Item {
|
||||
student: StudentUser
|
||||
result: AssignmentResult
|
||||
assignment: Assignment
|
||||
exams: Exam[]
|
||||
session?: Session
|
||||
}
|
||||
|
||||
interface Body {
|
||||
entities: EntityWithRoles[]
|
||||
items: Item[]
|
||||
assignments: Assignment[]
|
||||
startDate: Date
|
||||
endDate: Date
|
||||
}
|
||||
|
||||
interface EntityInformation {
|
||||
entity: EntityWithRoles
|
||||
exams: Exam[]
|
||||
numberOfAssignees: number
|
||||
numberOfSubmissions: number
|
||||
numberOfAbsentees: number
|
||||
assignment: Assignment
|
||||
items: Item[]
|
||||
}
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") return res.status(404).json({ ok: false })
|
||||
|
||||
const user = await requestUser(req, res)
|
||||
if (!user) return res.status(401).json({ ok: false });
|
||||
if (!checkAccess(user, ['admin', 'developer', 'mastercorporate', 'corporate'])) return res.status(403).json({ ok: false });
|
||||
|
||||
const { entities, items, assignments } = req.body as Body
|
||||
const entityInformations: EntityInformation[] = []
|
||||
|
||||
for (const entity of entities) {
|
||||
const entityItems = items.filter(i => i.assignment.entity === entity.id)
|
||||
const groupedByAssignments = groupBy(entityItems, (a) => a.assignment.id)
|
||||
for (const assignmentID of Object.keys(groupedByAssignments)) {
|
||||
const assignmentItems = groupedByAssignments[assignmentID]
|
||||
const assignment = findBy(assignments, 'id', assignmentID)!
|
||||
const assignmentExams =
|
||||
uniqBy(assignmentItems.flatMap(a => a.exams.map(e => ({ ...e, moduleID: `${e.id}_${e.module}` }))), 'moduleID')
|
||||
|
||||
const assignmentEntityInformation: EntityInformation = {
|
||||
entity,
|
||||
exams: assignmentExams,
|
||||
numberOfAssignees: assignmentItems.length,
|
||||
numberOfSubmissions: assignmentItems.filter(x => !!x.result).length,
|
||||
numberOfAbsentees: assignmentItems.filter(x => !x.result).length,
|
||||
assignment,
|
||||
items: assignmentItems
|
||||
}
|
||||
|
||||
entityInformations.push(assignmentEntityInformation)
|
||||
}
|
||||
}
|
||||
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet("Statistical");
|
||||
|
||||
for (const e of entityInformations) {
|
||||
await addEntityInformationToWorksheet(worksheet, e)
|
||||
}
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer()
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
res.status(200).send(buffer);
|
||||
}
|
||||
|
||||
const addEntityInformationToWorksheet = async (worksheet: ExcelJS.Worksheet, entityInformation: EntityInformation) => {
|
||||
const data = [
|
||||
['Entity', undefined, undefined, entityInformation.entity.label],
|
||||
['Assignment', undefined, undefined, entityInformation.assignment.name],
|
||||
['Date of the Assignment', undefined, undefined, moment(entityInformation.assignment.startDate).format("DD/MM/YYYY")],
|
||||
['Exams', undefined, undefined, mapBy(entityInformation.exams, 'id').join(', ')],
|
||||
['Modules', undefined, undefined, entityInformation.exams.map(e => capitalize(e.module)).join(', ')],
|
||||
['Number of Assignees', undefined, undefined, entityInformation.numberOfAssignees],
|
||||
['Number of Submissions', undefined, undefined, entityInformation.numberOfSubmissions],
|
||||
['Number of Absentees', undefined, undefined, entityInformation.numberOfAbsentees]
|
||||
]
|
||||
|
||||
const dataRows = worksheet.addRows(data);
|
||||
dataRows.forEach(row => row.getCell(1).font = { bold: true, color: { argb: "ffffffff" } })
|
||||
dataRows.forEach(row => row.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "ff674ea7" } })
|
||||
dataRows.forEach(row => worksheet.mergeCells(`${row.getCell(1).address}:${row.getCell(3).address}`))
|
||||
dataRows.forEach(row => row.getCell(4).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "FFD9D2E9" } })
|
||||
dataRows.forEach(row => worksheet.mergeCells(`${row.getCell(4).address}:${row.getCell(7).address}`))
|
||||
|
||||
worksheet.addRows([[], []]);
|
||||
const gradingSystem = await getGradingSystemByEntity(entityInformation.entity.id)
|
||||
|
||||
for (const exam of entityInformation.exams) {
|
||||
const examRow = worksheet.addRow([`${capitalize(exam.module)} Exam`, undefined, exam.id])
|
||||
examRow.getCell(1).font = { bold: true, color: { argb: "ffffffff" } }
|
||||
examRow.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "ff674ea7" } }
|
||||
examRow.getCell(3).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "FFD9D2E9" } }
|
||||
|
||||
worksheet.mergeCells(`${examRow.getCell(1).address}:${examRow.getCell(2).address}`)
|
||||
worksheet.mergeCells(`${examRow.getCell(3).address}:${examRow.getCell(6).address}`)
|
||||
|
||||
const parts = exam.module === "level" || exam.module === "listening" || exam.module === "reading" ? exam.parts : []
|
||||
|
||||
const header = worksheet.addRow([
|
||||
"#",
|
||||
"Name",
|
||||
"E-mail",
|
||||
"Student ID",
|
||||
"Passport/ID",
|
||||
"Gender",
|
||||
"Finished at",
|
||||
"Score",
|
||||
...(exam.module === "level" ? ["Grade"] : []),
|
||||
...parts.map((_, i) => `Part ${i + 1}`)
|
||||
])
|
||||
header.font = { bold: true, color: { argb: "FFFFFFFF" } }
|
||||
header.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: "FFD9D2E9" } }
|
||||
|
||||
const examItems =
|
||||
entityInformation.items
|
||||
.filter(i => !!i.result)
|
||||
.map(i => ({
|
||||
...i,
|
||||
result: { ...i.result, stats: i.result.stats.filter(x => x.exam === exam.id) },
|
||||
}))
|
||||
|
||||
const orderedItems = examItems.sort((a, b) => {
|
||||
const aTotalScore = a.result.stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.correct, 0)
|
||||
const bTotalScore = b.result.stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.correct, 0)
|
||||
|
||||
return bTotalScore - aTotalScore
|
||||
})
|
||||
|
||||
const itemRows = orderedItems.map((item, index) => {
|
||||
const { total, correct } = calculateScore(item.result.stats)
|
||||
const score = `${correct} / ${total}`
|
||||
|
||||
const finishTimestamp = [...item.result.stats].sort((a, b) => b.date - a.date).shift()?.date || -1
|
||||
const finishDate = finishTimestamp === -1 ? "N/A" : moment(new Date(finishTimestamp)).format("DD/MM/YYYY HH:mm")
|
||||
|
||||
const grade = getGradingLabel(correct, gradingSystem.steps)
|
||||
|
||||
return [
|
||||
index + 1,
|
||||
item.student.name,
|
||||
item.student.email,
|
||||
item.student.studentID || "N/A",
|
||||
item.student.demographicInformation?.passport_id || "N/A",
|
||||
item.student.demographicInformation?.gender || "N/A",
|
||||
finishDate,
|
||||
score,
|
||||
...(exam.module === "level" ? [grade] : []),
|
||||
...parts.map((part) => {
|
||||
const exerciseIDs = mapBy(part.exercises, 'id')
|
||||
const { total, correct } = calculateScore(item.result.stats.filter(s => exerciseIDs.includes(s.exercise)))
|
||||
|
||||
return `${correct} / ${total}`
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
worksheet.addRows(itemRows)
|
||||
worksheet.addRows([[]]);
|
||||
}
|
||||
worksheet.addRows([[], []]);
|
||||
}
|
||||
|
||||
const calculateScore = (stats: Stat[]) => {
|
||||
const total = stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.total, 0)
|
||||
const correct = stats.filter(x => !x.isPractice).reduce((acc, curr) => acc + curr.score.correct, 0)
|
||||
|
||||
return { total, correct }
|
||||
}
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: {
|
||||
sizeLimit: '20mb',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,421 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { storage } from "@/firebase";
|
||||
import client from "@/lib/mongodb";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import ReactPDF from "@react-pdf/renderer";
|
||||
import TestReport from "@/exams/pdf/test.report";
|
||||
import LevelTestReport from "@/exams/pdf/level.test.report";
|
||||
import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
|
||||
import {
|
||||
DemographicInformation,
|
||||
Stat,
|
||||
StudentUser,
|
||||
User,
|
||||
} from "@/interfaces/user";
|
||||
import { Module } from "@/interfaces";
|
||||
import { ModuleScore } from "@/interfaces/module.scores";
|
||||
import { SkillExamDetails } from "@/exams/pdf/details/skill.exam";
|
||||
import { calculateBandScore } from "@/utils/score";
|
||||
import axios from "axios";
|
||||
import { moduleLabels } from "@/utils/moduleUtils";
|
||||
import {
|
||||
generateQRCode,
|
||||
getRadialProgressPNG,
|
||||
streamToBuffer,
|
||||
} from "@/utils/pdf";
|
||||
import moment from "moment-timezone";
|
||||
import { getCorporateNameForStudent } from "@/utils/groups.be";
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") return get(req, res);
|
||||
if (req.method === "POST") return post(req, res);
|
||||
}
|
||||
|
||||
const getExamSummary = (score: number) => {
|
||||
if (score > 0.8) {
|
||||
return "Scoring between 81% and 100% on the English exam demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. Continuing to challenge oneself with advanced material in writing, speaking, listening, and reading will further refine the already impressive command of the English language.";
|
||||
}
|
||||
|
||||
if (score > 0.6) {
|
||||
return "Scoring between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflects a commendable level of proficiency in each domain. There's evidence of a solid grasp of key concepts, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for further mastery.";
|
||||
}
|
||||
|
||||
if (score > 0.4) {
|
||||
return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading demonstrates a moderate level of understanding in each domain. While there's a commendable grasp of key concepts, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. Consistent effort and targeted focus on weaker areas are recommended.";
|
||||
}
|
||||
|
||||
if (score > 0.2) {
|
||||
return "Scoring between 21% and 40% on the English exam, spanning writing, speaking, listening, and reading, indicates some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills. Strengthening writing, speaking, listening, and reading abilities through consistent effort and focused study will contribute to overall proficiency.";
|
||||
}
|
||||
|
||||
return "This student's performance on the English exam, encompassing writing, speaking, listening, and reading, reflects a significant need for improvement, scoring between 0% and 20%. There's a notable gap in understanding key concepts across all language domains. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial. Developing a consistent study routine and seeking additional support in each area can contribute to substantial progress.";
|
||||
};
|
||||
|
||||
const getLevelSummary = (score: number) => {
|
||||
if (score > 0.8) {
|
||||
return "Scoring between 81% and 100% on the English exam showcases an outstanding level of understanding and proficiency. Your performance reflects a mastery of key concepts, including grammar, vocabulary, and comprehension. You exhibit a high level of skill in applying these elements effectively. Your dedication to excellence is evident, and your consistent, stellar performance is commendable. Continue to challenge yourself with advanced material to further refine your already impressive command of the English language. Your commitment to excellence positions you as a standout student in English studies, and your achievements are a testament to your hard work and capability.";
|
||||
}
|
||||
|
||||
if (score > 0.6) {
|
||||
return "Scoring between 61% and 80% on the English exam reflects a commendable level of understanding and proficiency. You have demonstrated a solid grasp of key concepts, including grammar, vocabulary, and comprehension. There's evidence of effective application of skills, but room for refinement and deeper exploration remains. Consistent effort in honing nuanced aspects of language will contribute to even greater mastery. Continue engaging with challenging material and seeking opportunities for advanced comprehension. With sustained dedication, you have the potential to elevate your performance to an exceptional level and further excel in your English studies.";
|
||||
}
|
||||
|
||||
if (score > 0.4) {
|
||||
return "Scoring between 41% and 60% on the English exam reflects a moderate level of understanding. You demonstrate a grasp of some key concepts, but there's room for refinement in areas like grammar, vocabulary, and comprehension. Consistent effort and a strategic focus on weaker areas can lead to notable improvement. Engaging with supplementary resources and seeking feedback will further enhance your skills. With continued dedication, there's a solid foundation to build upon, and achieving a higher level of proficiency is within reach. Keep up the good work and aim for sustained progress in your English studies.";
|
||||
}
|
||||
|
||||
if (score > 0.2) {
|
||||
return "Scoring between 21% and 40% on the English exam shows some understanding of key concepts, but there's still ample room for improvement. Strengthening foundational skills, such as grammar, vocabulary, and comprehension, is essential. Consistent effort and focused study can help bridge gaps in knowledge and elevate your performance. Consider seeking additional guidance or resources to refine your understanding of the material. With commitment and targeted improvements, you have the potential to make significant strides in your English proficiency.";
|
||||
}
|
||||
|
||||
return "Your performance on the English exam falls within the 0% to 20% range, indicating a need for improvement. There's room to enhance your grasp of fundamental concepts like grammar, vocabulary, and comprehension. Establishing a consistent study routine and seeking extra support can be beneficial. With dedication and targeted efforts, you have the potential to significantly boost your performance in upcoming assessments.";
|
||||
};
|
||||
|
||||
const getPerformanceSummary = (module: Module, score: number) => {
|
||||
if (module === "level") return getLevelSummary(score);
|
||||
return getExamSummary(score);
|
||||
};
|
||||
interface SkillsFeedbackRequest {
|
||||
code: Module;
|
||||
name: string;
|
||||
grade: number;
|
||||
}
|
||||
|
||||
interface SkillsFeedbackResponse extends SkillsFeedbackRequest {
|
||||
evaluation: string;
|
||||
suggestions: string;
|
||||
bullet_points?: string[];
|
||||
}
|
||||
|
||||
const getSkillsFeedback = async (sections: SkillsFeedbackRequest[]) => {
|
||||
const backendRequest = await axios.post(
|
||||
`${process.env.BACKEND_URL}/grade/summary`,
|
||||
{ sections },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return backendRequest.data?.sections;
|
||||
};
|
||||
|
||||
// perform the request with several retries if needed
|
||||
const handleSkillsFeedbackRequest = async (
|
||||
sections: SkillsFeedbackRequest[]
|
||||
): Promise<SkillsFeedbackResponse[] | null> => {
|
||||
let i = 0;
|
||||
try {
|
||||
const data = await getSkillsFeedback(sections);
|
||||
return data;
|
||||
} catch (err) {
|
||||
if (i < 3) {
|
||||
i++;
|
||||
return handleSkillsFeedbackRequest(sections);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
async function getDefaultPDFStream(
|
||||
stats: Stat[],
|
||||
user: User,
|
||||
qrcodeUrl: string
|
||||
) {
|
||||
const [stat] = stats;
|
||||
// generate the QR code for the report
|
||||
const qrcode = await generateQRCode(qrcodeUrl);
|
||||
|
||||
if (!qrcode) {
|
||||
throw new Error("Failed to generate QR code");
|
||||
}
|
||||
|
||||
// stats may contain multiple exams of the same type so we need to aggregate them
|
||||
const results = stats
|
||||
.reduce((accm: ModuleScore[], stat: Stat) => {
|
||||
const { module, score } = stat;
|
||||
|
||||
const fixedModuleStr = module[0].toUpperCase() + module.substring(1);
|
||||
if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) {
|
||||
return accm.map((e: ModuleScore) => {
|
||||
if (e.module === fixedModuleStr) {
|
||||
return {
|
||||
...e,
|
||||
score: e.score + score.correct,
|
||||
total: e.total + score.total,
|
||||
};
|
||||
}
|
||||
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
const value = {
|
||||
module: fixedModuleStr,
|
||||
score: score.correct,
|
||||
total: score.total,
|
||||
code: module,
|
||||
} as ModuleScore;
|
||||
|
||||
return [...accm, value];
|
||||
}, [])
|
||||
.map((moduleScore: ModuleScore) => {
|
||||
const { score, total } = moduleScore;
|
||||
// with all the scores aggreated we can calculate the band score for each module
|
||||
const bandScore = calculateBandScore(
|
||||
score,
|
||||
total,
|
||||
moduleScore.code as Module,
|
||||
user.focus
|
||||
);
|
||||
|
||||
return {
|
||||
...moduleScore,
|
||||
// generate the closest radial progress png for the score
|
||||
png: getRadialProgressPNG("azul", score, total),
|
||||
bandScore,
|
||||
};
|
||||
});
|
||||
|
||||
// get the skills feedback from the backend based on the module grade
|
||||
const skillsFeedback = (await handleSkillsFeedbackRequest(
|
||||
results.map(({ code, bandScore }) => ({
|
||||
code,
|
||||
name: moduleLabels[code],
|
||||
grade: bandScore,
|
||||
}))
|
||||
)) as SkillsFeedbackResponse[];
|
||||
|
||||
if (!skillsFeedback) {
|
||||
throw new Error("Failed to get skills feedback");
|
||||
}
|
||||
|
||||
// assign the feedback to the results
|
||||
const finalResults = results.map((result) => {
|
||||
const feedback = skillsFeedback.find(
|
||||
(f: SkillsFeedbackResponse) => f.code === result.code
|
||||
);
|
||||
|
||||
if (feedback) {
|
||||
return {
|
||||
...result,
|
||||
evaluation: feedback?.evaluation,
|
||||
suggestions: feedback?.suggestions,
|
||||
bullet_points: feedback?.bullet_points,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// calculate the overall score out of all the aggregated results
|
||||
const overallScore = results.reduce((accm, { score }) => accm + score, 0);
|
||||
const overallTotal = results.reduce((accm, { total }) => accm + total, 0);
|
||||
const overallResult = overallScore / overallTotal;
|
||||
|
||||
const overallPNG = getRadialProgressPNG(
|
||||
"laranja",
|
||||
overallScore,
|
||||
overallTotal
|
||||
);
|
||||
|
||||
// generate the overall detail report
|
||||
const overallDetail = {
|
||||
module: "Overall",
|
||||
score: overallScore,
|
||||
total: overallTotal,
|
||||
png: overallPNG,
|
||||
} as ModuleScore;
|
||||
const testDetails = [overallDetail, ...finalResults];
|
||||
|
||||
// generate the performance summary based on the overall result
|
||||
const performanceSummary = getPerformanceSummary(stat.module, overallResult);
|
||||
|
||||
const title = "ENGLISH SKILLS TEST RESULT REPORT";
|
||||
const details = <SkillExamDetails testDetails={testDetails} />;
|
||||
|
||||
const demographicInformation =
|
||||
user.demographicInformation as DemographicInformation;
|
||||
return ReactPDF.renderToStream(
|
||||
<TestReport
|
||||
title={title}
|
||||
date={moment(stat.date)
|
||||
.tz(user.demographicInformation?.timezone || "UTC")
|
||||
.format("ll HH:mm:ss")}
|
||||
name={user.name}
|
||||
email={user.email}
|
||||
id={user.id}
|
||||
gender={demographicInformation?.gender}
|
||||
summary={performanceSummary}
|
||||
testDetails={testDetails}
|
||||
renderDetails={details}
|
||||
logo={"public/logo_title.png"}
|
||||
qrcode={qrcode}
|
||||
summaryPNG={overallPNG}
|
||||
summaryScore={`${Math.floor(overallResult * 100)}%`}
|
||||
passportId={demographicInformation?.passport_id || ""}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function getPdfUrl(pdfStream: any, docsSnap: any) {
|
||||
// generate the file ref for storage
|
||||
const fileName = `${Date.now().toString()}.pdf`;
|
||||
const refName = `exam_report/${fileName}`;
|
||||
const fileRef = ref(storage, refName);
|
||||
|
||||
// upload the pdf to storage
|
||||
const pdfBuffer = await streamToBuffer(pdfStream);
|
||||
await uploadBytes(fileRef, pdfBuffer, {
|
||||
contentType: "application/pdf",
|
||||
});
|
||||
|
||||
// update the stats entries with the pdf url to prevent duplication
|
||||
await db.collection("stats").updateOne(
|
||||
{ id: docsSnap.id },
|
||||
{
|
||||
$set: {
|
||||
pdf: {
|
||||
path: refName,
|
||||
version: process.env.PDF_VERSION,
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
return getDownloadURL(fileRef);
|
||||
}
|
||||
|
||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
// verify if it's a logged user that is trying to export
|
||||
if (req.session.user) {
|
||||
const { id } = req.query as { id: string };
|
||||
// fetch stats entries for this particular user with the requested exam session
|
||||
const stats = await db.collection("stats").find<Stat>({ session: id }).toArray();
|
||||
|
||||
if (stats.length == 0) {
|
||||
res.status(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
// verify if the stats already have a pdf generated
|
||||
const hasPDF = stats.find(
|
||||
(s) => s.pdf?.path && s.pdf?.version === process.env.PDF_VERSION
|
||||
);
|
||||
// find the user that generated the stats
|
||||
const statIndex = stats.findIndex((s) => s.user);
|
||||
|
||||
if (statIndex === -1) {
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
const userId = stats[statIndex].user;
|
||||
|
||||
if (hasPDF) {
|
||||
// if it does, return the pdf url
|
||||
const fileRef = ref(storage, hasPDF.pdf!.path);
|
||||
const url = await getDownloadURL(fileRef);
|
||||
|
||||
res.status(200).end(url);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// generate the pdf report
|
||||
const docUser = await db.collection("users").findOne<User>({ id: userId});
|
||||
|
||||
if (docUser) {
|
||||
// we'll need the user in order to get the user data (name, email, focus, etc);
|
||||
|
||||
const [stat] = stats;
|
||||
|
||||
if (stat.module === "level") {
|
||||
const user = docUser as StudentUser;
|
||||
|
||||
const uniqueExercises = stats.map((s) => ({
|
||||
name: "Gramar & Vocabulary",
|
||||
result: `${s.score.correct}/${s.score.total}`,
|
||||
}));
|
||||
const dates = stats.map((s) => moment(s.date));
|
||||
const timeSpent = `${stats.reduce((accm, s: Stat) => accm + (s.timeSpent || 0), 0) / 60
|
||||
} minutes`;
|
||||
const score = stats.reduce((accm, s) => accm + s.score.correct, 0);
|
||||
const corporateName = await getCorporateNameForStudent(userId);
|
||||
const pdfStream = await ReactPDF.renderToStream(
|
||||
<LevelTestReport
|
||||
date={moment.max(dates).format("DD/MM/YYYY")}
|
||||
name={user.name}
|
||||
email={user.email}
|
||||
id={stat.exam}
|
||||
gender={user.demographicInformation?.gender || ""}
|
||||
passportId={user.demographicInformation?.passport_id || ""}
|
||||
corporateName={corporateName}
|
||||
downloadDate={moment().format("DD/MM/YYYY")}
|
||||
userId={userId}
|
||||
uniqueExercises={uniqueExercises}
|
||||
timeSpent={timeSpent}
|
||||
score={score.toString()}
|
||||
/>
|
||||
);
|
||||
|
||||
const url = await getPdfUrl(pdfStream, docUser);
|
||||
res.status(200).end(url);
|
||||
return;
|
||||
}
|
||||
const user = docUser as User;
|
||||
|
||||
try {
|
||||
const pdfStream = await getDefaultPDFStream(
|
||||
stats,
|
||||
user,
|
||||
`${req.headers.origin || ""}${req.url}`
|
||||
);
|
||||
|
||||
const url = await getPdfUrl(pdfStream, docUser);
|
||||
res.status(200).end(url);
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
|
||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { id } = req.query as { id: string };
|
||||
const stats = await db.collection("stats").find<Stat>({ session: id }).toArray();
|
||||
|
||||
if (stats.length == 0) {
|
||||
res.status(404).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const hasPDF = stats.find((s) => s.pdf?.path);
|
||||
|
||||
if (hasPDF) {
|
||||
const fileRef = ref(storage, hasPDF.pdf!.path);
|
||||
const url = await getDownloadURL(fileRef);
|
||||
return res.redirect(url);
|
||||
}
|
||||
|
||||
res.status(500).end();
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import client from "@/lib/mongodb";
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") return GET(req, res);
|
||||
|
||||
res.status(404).json({ok: false});
|
||||
}
|
||||
|
||||
async function GET(req: NextApiRequest, res: NextApiResponse) {
|
||||
const {id} = req.query;
|
||||
|
||||
const snapshot = await db.collection("stats").findOne({ id: id as string});
|
||||
if (!snapshot) return res.status(404).json({id: id as string});
|
||||
|
||||
res.status(200).json({...snapshot, id: snapshot.id});
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import client from "@/lib/mongodb";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { requestUser } from "@/utils/api";
|
||||
import { UserSolution } from "@/interfaces/exam";
|
||||
import { WithId } from "mongodb";
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await requestUser(req, res)
|
||||
if (!user) return res.status(401).json({ ok: false });
|
||||
|
||||
if (req.method === "POST") return post(req, res);
|
||||
}
|
||||
|
||||
|
||||
interface Body {
|
||||
solutions: UserSolution[];
|
||||
sessionId: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { userId, solutions, sessionId } = req.body as Body;
|
||||
const disabledStats = await db.collection("stats").find(
|
||||
{ user: userId, session: sessionId, isDisabled: true }
|
||||
).toArray();
|
||||
|
||||
await Promise.all(disabledStats.map(async (stat) => {
|
||||
const matchingSolution = solutions.find(s => s.exercise === stat.exercise);
|
||||
if (matchingSolution) {
|
||||
const { _id, ...updateFields } = matchingSolution as WithId<UserSolution>;
|
||||
await db.collection("stats").updateOne(
|
||||
{ id: stat.id },
|
||||
{ $set: { ...updateFields } }
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
return res.status(200).json({ ok: true });
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import client from "@/lib/mongodb";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { Stat } from "@/interfaces/user";
|
||||
import { Assignment } from "@/interfaces/results";
|
||||
import { groupBy } from "lodash";
|
||||
import { requestUser } from "@/utils/api";
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") return get(req, res);
|
||||
if (req.method === "POST") return post(req, res);
|
||||
}
|
||||
|
||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await requestUser(req, res)
|
||||
if (!user) return res.status(401).json({ ok: false });
|
||||
|
||||
const snapshot = await db.collection("stats").find<Stat>({}).toArray();
|
||||
|
||||
res.status(200).json(snapshot);
|
||||
}
|
||||
|
||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await requestUser(req, res)
|
||||
if (!user) return res.status(401).json({ ok: false });
|
||||
|
||||
const stats = req.body as Stat[];
|
||||
stats.forEach(async (stat) => await db.collection("stats").updateOne(
|
||||
{ id: stat.id },
|
||||
{ $set: stat },
|
||||
{ upsert: true }
|
||||
));
|
||||
stats.forEach(async (stat) => {
|
||||
await db.collection("sessions").deleteOne({ id: stat.session })
|
||||
});
|
||||
|
||||
const groupedStatsByAssignment = groupBy(
|
||||
stats.filter((x) => !!x.assignment),
|
||||
"assignment",
|
||||
);
|
||||
if (Object.keys(groupedStatsByAssignment).length > 0) {
|
||||
const assignments = Object.keys(groupedStatsByAssignment);
|
||||
|
||||
assignments.forEach(async (assignmentId) => {
|
||||
const assignmentStats = groupedStatsByAssignment[assignmentId] as Stat[];
|
||||
|
||||
const assignmentSnapshot = await db.collection("assignments").findOne<Assignment>({ id: assignmentId });
|
||||
await db.collection("assignments").updateOne(
|
||||
{ id: assignmentId },
|
||||
{
|
||||
$set: {
|
||||
results: [
|
||||
...assignmentSnapshot ? assignmentSnapshot.results : [],
|
||||
{ user: user.id, type: user.focus, stats: assignmentStats },
|
||||
],
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({ ok: true });
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import client from "@/lib/mongodb";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
const {session} = req.query;
|
||||
const snapshot = await db.collection("stats").find({ user: req.session.user.id, session }).toArray();
|
||||
|
||||
res.status(200).json(snapshot);
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { MODULES } from "@/constants/ielts";
|
||||
import { app } from "@/firebase";
|
||||
import { Module } from "@/interfaces";
|
||||
import { Stat, User } from "@/interfaces/user";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { calculateAverageLevel, calculateBandScore } from "@/utils/score";
|
||||
import { groupByModule, groupBySession } from "@/utils/stats";
|
||||
import { MODULE_ARRAY } from "@/utils/moduleUtils";
|
||||
import client from "@/lib/mongodb";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { groupBy } from "lodash";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { requestUser } from "@/utils/api";
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
export default withIronSessionApiRoute(update, sessionOptions);
|
||||
|
||||
async function update(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await requestUser(req, res)
|
||||
if (user) {
|
||||
const docUser = await db.collection("users").findOne({ id: user.id });
|
||||
|
||||
if (!docUser) {
|
||||
res.status(401).json(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = await db.collection("stats").find<Stat>({ user: user.id }).toArray();
|
||||
|
||||
const groupedStats = groupBySession(stats);
|
||||
const sessionLevels: { [key in Module]: { correct: number; total: number } }[] = Object.keys(groupedStats).map((key) => {
|
||||
const sessionStats = groupedStats[key].map((stat) => ({ module: stat.module, correct: stat.score.correct, total: stat.score.total }));
|
||||
const sessionLevels = {
|
||||
reading: {
|
||||
correct: 0,
|
||||
total: 0,
|
||||
},
|
||||
listening: {
|
||||
correct: 0,
|
||||
total: 0,
|
||||
},
|
||||
writing: {
|
||||
correct: 0,
|
||||
total: 0,
|
||||
},
|
||||
speaking: {
|
||||
correct: 0,
|
||||
total: 0,
|
||||
},
|
||||
level: {
|
||||
correct: 0,
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
|
||||
MODULE_ARRAY.forEach((module: Module) => {
|
||||
const moduleStats = sessionStats.filter((x) => x.module === module);
|
||||
if (moduleStats.length === 0) return;
|
||||
|
||||
const moduleScore = moduleStats.reduce(
|
||||
(accumulator, current) => ({ correct: accumulator.correct + current.correct, total: accumulator.total + current.total }),
|
||||
{ correct: 0, total: 0 },
|
||||
);
|
||||
|
||||
sessionLevels[module] = moduleScore;
|
||||
});
|
||||
|
||||
return sessionLevels;
|
||||
});
|
||||
|
||||
const readingLevel = sessionLevels
|
||||
.map((x) => x.reading)
|
||||
.filter((x) => x.total > 0)
|
||||
.reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 });
|
||||
const listeningLevel = sessionLevels
|
||||
.map((x) => x.listening)
|
||||
.filter((x) => x.total > 0)
|
||||
.reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 });
|
||||
const writingLevel = sessionLevels
|
||||
.map((x) => x.writing)
|
||||
.filter((x) => x.total > 0)
|
||||
.reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 });
|
||||
const speakingLevel = sessionLevels
|
||||
.map((x) => x.speaking)
|
||||
.filter((x) => x.total > 0)
|
||||
.reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 });
|
||||
|
||||
const levelLevel = sessionLevels
|
||||
.map((x) => x.level)
|
||||
.filter((x) => x.total > 0)
|
||||
.reduce((acc, cur) => ({ total: acc.total + cur.total, correct: acc.correct + cur.correct }), { total: 0, correct: 0 });
|
||||
|
||||
|
||||
const levels = {
|
||||
reading: calculateBandScore(readingLevel.correct, readingLevel.total, "reading", user.focus),
|
||||
listening: calculateBandScore(listeningLevel.correct, listeningLevel.total, "listening", user.focus),
|
||||
writing: calculateBandScore(writingLevel.correct, writingLevel.total, "writing", user.focus),
|
||||
speaking: calculateBandScore(speakingLevel.correct, speakingLevel.total, "speaking", user.focus),
|
||||
level: calculateBandScore(levelLevel.correct, levelLevel.total, "level", user.focus),
|
||||
};
|
||||
|
||||
const averageLevel = calculateAverageLevel(levels)
|
||||
|
||||
await db.collection("users").updateOne(
|
||||
{ id: user.id },
|
||||
{ $set: { levels, averageLevel } }
|
||||
);
|
||||
|
||||
res.status(200).json({ ok: true });
|
||||
} else {
|
||||
res.status(401).json(undefined);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { withIronSessionApiRoute } from "iron-session/next";
|
||||
import { sessionOptions } from "@/lib/session";
|
||||
import { getDetailedStatsByUser } from "../../../../utils/stats.be";
|
||||
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ ok: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const { user, query } = req.query as { user: string, query?: string };
|
||||
|
||||
const snapshot = await getDetailedStatsByUser(user, query);
|
||||
res.status(200).json(snapshot);
|
||||
}
|
||||
@@ -1,27 +1,3 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type {NextApiRequest, NextApiResponse} from "next";
|
||||
import {app} from "@/firebase";
|
||||
import {withIronSessionApiRoute} from "iron-session/next";
|
||||
import {sessionOptions} from "@/lib/session";
|
||||
import {Session} from "@/hooks/useSessions";
|
||||
import {deleteObject, getStorage, ref} from "firebase/storage";
|
||||
import {proxyToOdoo} from "@/lib/odoo";
|
||||
|
||||
const storage = getStorage(app);
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "POST") return post(req, res);
|
||||
}
|
||||
|
||||
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
const {path} = req.body as {path: string};
|
||||
await deleteObject(ref(storage, path));
|
||||
|
||||
return res.status(200).json({ok: true});
|
||||
}
|
||||
export default proxyToOdoo("/api/storage/delete");
|
||||
|
||||
@@ -1,68 +1,3 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
|
||||
import { IncomingForm, Files, Fields } from 'formidable';
|
||||
import { promises as fs } from 'fs';
|
||||
import { withIronSessionApiRoute } from 'iron-session/next';
|
||||
import { storage } from '@/firebase';
|
||||
import { sessionOptions } from '@/lib/session';
|
||||
import { v4 } from 'uuid';
|
||||
import {proxyToOdoo} from "@/lib/odoo";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
export async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ error: 'Not authorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Not allowed' });
|
||||
}
|
||||
|
||||
const directory = (req.query.directory as string) || "uploads";
|
||||
|
||||
try {
|
||||
const form = new IncomingForm({
|
||||
keepExtensions: true,
|
||||
multiples: true,
|
||||
});
|
||||
|
||||
const [fields, files]: [Fields, Files] = await new Promise((resolve, reject) => {
|
||||
form.parse(req, (err, fields, files) => {
|
||||
if (err) reject(err);
|
||||
resolve([fields, files]);
|
||||
});
|
||||
});
|
||||
|
||||
const fileArray = files.file;
|
||||
if (!fileArray) {
|
||||
return res.status(400).json({ error: 'No files provided' });
|
||||
}
|
||||
|
||||
const filesToProcess = Array.isArray(fileArray) ? fileArray : [fileArray];
|
||||
|
||||
const uploadPromises = filesToProcess.map(async (file) => {
|
||||
const split = file.originalFilename?.split('.') || ["bin"];
|
||||
const extension = split[split.length - 1];
|
||||
|
||||
const buffer = await fs.readFile(file.filepath);
|
||||
const storageRef = ref(storage, `${directory}/${v4()}.${extension}`);
|
||||
await uploadBytes(storageRef, buffer);
|
||||
const downloadURL = await getDownloadURL(storageRef);
|
||||
await fs.unlink(file.filepath);
|
||||
return downloadURL;
|
||||
});
|
||||
|
||||
const urls = await Promise.all(uploadPromises);
|
||||
res.status(200).json({ urls });
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
res.status(500).json({ error: 'Upload failed' });
|
||||
}
|
||||
}
|
||||
export default proxyToOdoo("/api/storage");
|
||||
|
||||
@@ -1,38 +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, {AxiosResponse} from "axios";
|
||||
import formidable from "formidable-serverless";
|
||||
import {getDownloadURL, ref, uploadBytes} from "firebase/storage";
|
||||
import fs from "fs";
|
||||
import {app, storage} from "@/firebase";
|
||||
import {proxyStreamToOdoo} from "@/lib/odoo";
|
||||
|
||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session.user) {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
const form = formidable({keepExtensions: true});
|
||||
await form.parse(req, async (err: any, fields: any, files: any) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return res.status(500).json({ok: false});
|
||||
}
|
||||
|
||||
const audioFile = files.audio;
|
||||
const audioFileRef = ref(storage, `${fields.root}/${(audioFile as any).path.replace("upload_", "")}`);
|
||||
|
||||
const binary = fs.readFileSync((audioFile as any).path).buffer;
|
||||
const snapshot = await uploadBytes(audioFileRef, binary);
|
||||
|
||||
const path = await getDownloadURL(snapshot.ref);
|
||||
res.status(200).json({path});
|
||||
});
|
||||
}
|
||||
export default proxyStreamToOdoo("/api/storage");
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
|
||||
@@ -1,81 +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 {Type, User} from "@/interfaces/user";
|
||||
import {PERMISSIONS} from "@/constants/userPermissions";
|
||||
import {uuidv4} from "@firebase/util";
|
||||
import {prepareMailer, prepareMailOptions} from "@/email";
|
||||
import * as Stripe from "stripe";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import moment from "moment";
|
||||
|
||||
|
||||
const db = client.db(process.env.MONGODB_DB);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const {email, days, key, checkout} = req.body as {email: string; days: string; key: string; checkout: string};
|
||||
if (!key || key !== process.env.STRIPE_KEY) {
|
||||
res.status(403).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
const uid = new ShortUniqueId();
|
||||
const code = uid.randomUUID(6);
|
||||
|
||||
const codeCheckerRef = await db.collection("codes").find({ checkout: checkout }).toArray();
|
||||
if (codeCheckerRef.length !== 0) {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
|
||||
const emailCheckerRef = await db.collection("users").find({ email: email }).toArray();
|
||||
if (emailCheckerRef.length !== 0) {
|
||||
const user = emailCheckerRef[0];
|
||||
if (!user.subscriptionExpirationDate) {
|
||||
res.status(200).json({ok: true});
|
||||
return;
|
||||
}
|
||||
|
||||
const codeUserRef = await db.collection("codes").find({ userId: user.id }).toArray();
|
||||
const userCode = codeUserRef[0];
|
||||
|
||||
if (userCode.checkout && userCode.checkout === checkout) {
|
||||
res.status(401).json({ok: false});
|
||||
return;
|
||||
}
|
||||
await db.collection("users").updateOne(
|
||||
{ id: user.id },
|
||||
{ $set: {subscriptionExpirationDate: moment(user.subscriptionExpirationDate).add(days, "days").toISOString()} }
|
||||
);
|
||||
await db.collection("codes").updateOne(
|
||||
{ id: userCode.id },
|
||||
{ $set: {checkout} }
|
||||
);
|
||||
|
||||
res.status(200).json({ok: true});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.collection("codes").updateOne(
|
||||
{ id: code },
|
||||
{ $set: {type: "student", code, expiryDate: moment(new Date()).add(days, "days").toISOString(), checkout} }
|
||||
);
|
||||
|
||||
const transport = prepareMailer();
|
||||
const mailOptions = prepareMailOptions(
|
||||
{
|
||||
type: "student",
|
||||
code,
|
||||
environment: process.env.ENVIRONMENT,
|
||||
},
|
||||
[email],
|
||||
"EnCoach Registration",
|
||||
"main",
|
||||
);
|
||||
|
||||
await transport.sendMail(mailOptions);
|
||||
|
||||
res.status(200).json({ok: true});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user