openlearn-next 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/AdminDirectoryView-0XV86Ues.js +1 -0
- package/dist/assets/{BatchPickerModal-9BhmNM2R.js → BatchPickerModal-of9a5EJz.js} +1 -1
- package/dist/assets/{ClassesView-DKScQDos.js → ClassesView-CefnsjZ8.js} +2 -2
- package/dist/assets/CloudDriveModal-DgVQLbO5.js +1 -0
- package/dist/assets/ComputerLabView-BId0ZarM.js +1 -0
- package/dist/assets/CourseManagement-Cxhc784Q.js +1 -0
- package/dist/assets/CourseWizardModal-Da6PKhnU.js +3 -0
- package/dist/assets/CoursewareHubPanel-LpUgjqbH.js +1 -0
- package/dist/assets/{Dashboard-BPxkmnvI.js → Dashboard-LWbFbup4.js} +2 -2
- package/dist/assets/{ExportWeightModal-DSzySeqh.js → ExportWeightModal-Cyg1zMEd.js} +1 -1
- package/dist/assets/HelpTour-CHPIK2ae.js +1 -0
- package/dist/assets/{HelpView-CuNDka5n.js → HelpView-CvpgBpdr.js} +3 -3
- package/dist/assets/ImportLessonsModal-CfWIrW1K.js +1 -0
- package/dist/assets/ImportModal-BTfia0Ah.js +1 -0
- package/dist/assets/InteractiveCoursewareViewer-CS5BqfJZ.js +1 -0
- package/dist/assets/InteractiveWhiteboard-U23CsqGo.js +78 -0
- package/dist/assets/{LazyCourseware-g7b8EXR_.js → LazyCourseware-DXL9IxZO.js} +1 -1
- package/dist/assets/{LazyWhiteboard-BIepzrv9.js → LazyWhiteboard-CtkzSFtN.js} +2 -2
- package/dist/assets/LessonEditorView-Bfxf4VVB.js +1 -0
- package/dist/assets/LiveClassroomView-DHzCM4qQ.js +1 -0
- package/dist/assets/{NotificationDetailModal-Bk1g_prI.js → NotificationDetailModal-BxrpnlWV.js} +1 -1
- package/dist/assets/{PluginView-C6s7Ltcy.js → PluginView-12qYzmnq.js} +3 -3
- package/dist/assets/{ProcessLogsModal-B-PY780w.js → ProcessLogsModal-01S7zsw0.js} +1 -1
- package/dist/assets/{QuizGeneratorModal-DWH1zP0X.js → QuizGeneratorModal-DfZI40Ii.js} +1 -1
- package/dist/assets/StudentAssignmentView-BP6zbHuM.js +1 -0
- package/dist/assets/StudentLessonView-DEpMzrH2.js +1 -0
- package/dist/assets/StudentPreviewModal-DCmai53s.js +1 -0
- package/dist/assets/StudentView-CcGlfW25.js +2 -0
- package/dist/assets/SystemResourceLibraryModal-BzHw1edT.js +1 -0
- package/dist/assets/TeacherView-Dw6mKwcv.js +2 -0
- package/dist/assets/{TimetableView-BMsjwi-s.js → TimetableView-DJgOlGSf.js} +3 -3
- package/dist/assets/index-B-YxO39A.css +1 -0
- package/dist/assets/index-C8sCKzmN.js +2 -0
- package/dist/assets/index-CYzNsYZB.js +193 -0
- package/dist/assets/{index.es-DZ6UeYE-.js → index.es-CfxBkm_z.js} +1 -1
- package/dist/assets/{vendor-charts-B7ViBL9w.js → vendor-charts-CRXa4AlU.js} +1 -1
- package/dist/assets/vendor-content-CYR4BTuT.js +3 -0
- package/dist/assets/{vendor-icons-_c3xpwjZ.js → vendor-icons-Dq6MhgH9.js} +1 -1
- package/dist/assets/{vendor-pdf-CVyNPGVr.js → vendor-pdf-Cp-cYteE.js} +2 -2
- package/dist/assets/vendor-utils-BEhuP0Zq.js +1 -0
- package/dist/index.html +2 -2
- package/dist/server.cjs +989 -700
- package/package.json +9 -6
- package/dist/assets/AdminDirectoryView-CD7tW4mc.js +0 -1
- package/dist/assets/CloudDriveModal-cPtTMriS.js +0 -1
- package/dist/assets/ComputerLabView-B6-twB0u.js +0 -1
- package/dist/assets/CourseManagement-AoauIULq.js +0 -1
- package/dist/assets/CourseWizardModal-BOgLpLz7.js +0 -3
- package/dist/assets/CoursewareHubPanel-Cb1mAqHS.js +0 -1
- package/dist/assets/HelpTour-DwM9UBiA.js +0 -1
- package/dist/assets/ImportLessonsModal-Bwy2r-96.js +0 -1
- package/dist/assets/ImportModal-CmUt1YeX.js +0 -1
- package/dist/assets/InteractiveCoursewareViewer-DWnzq20c.js +0 -1
- package/dist/assets/InteractiveWhiteboard-C7JMplbp.js +0 -62
- package/dist/assets/LessonEditorView-C-mvYRUd.js +0 -1
- package/dist/assets/LiveClassroomView-CqRbWNis.js +0 -1
- package/dist/assets/StudentAssignmentView-CfTjeoAp.js +0 -1
- package/dist/assets/StudentLessonView-D2XvNGnW.js +0 -1
- package/dist/assets/StudentPreviewModal-BwXG6Cis.js +0 -1
- package/dist/assets/StudentView-DT-373QP.js +0 -2
- package/dist/assets/SystemResourceLibraryModal-BTL8rB0g.js +0 -1
- package/dist/assets/TeacherView-XdD-y67J.js +0 -2
- package/dist/assets/index-C5FKWIUq.js +0 -190
- package/dist/assets/index-CATki2cp.css +0 -1
- package/dist/assets/index-wdWIWwbg.js +0 -1
- package/dist/assets/vendor-content-V6uLfjnH.js +0 -3
- package/dist/assets/vendor-utils-4FcTwKPj.js +0 -1
package/dist/server.cjs
CHANGED
|
@@ -1557,7 +1557,7 @@ var init_service_host = __esm({
|
|
|
1557
1557
|
init_plugin_namespace();
|
|
1558
1558
|
STACK_CAP = 4096;
|
|
1559
1559
|
ServiceHost = class {
|
|
1560
|
-
constructor(serviceRegistry, capabilityGuard, pluginActorId, manifestCapabilities, eventBus, eventForwarder, pluginId) {
|
|
1560
|
+
constructor(serviceRegistry, capabilityGuard, pluginActorId, manifestCapabilities, eventBus, eventForwarder, pluginId, dbPluginId) {
|
|
1561
1561
|
this.serviceRegistry = serviceRegistry;
|
|
1562
1562
|
this.capabilityGuard = capabilityGuard;
|
|
1563
1563
|
this.pluginActorId = pluginActorId;
|
|
@@ -1565,6 +1565,7 @@ var init_service_host = __esm({
|
|
|
1565
1565
|
this.eventBus = eventBus;
|
|
1566
1566
|
this.eventForwarder = eventForwarder;
|
|
1567
1567
|
this.pluginId = pluginId;
|
|
1568
|
+
this.dbPluginId = dbPluginId;
|
|
1568
1569
|
/**
|
|
1569
1570
|
* @param serviceRegistry - DI container for resolving service instances
|
|
1570
1571
|
* @param capabilityGuard - CapabilityGuard instance (sync access, NOT async ICapabilityService wrapper)
|
|
@@ -1974,13 +1975,30 @@ var init_service_host = __esm({
|
|
|
1974
1975
|
/**
|
|
1975
1976
|
* Security barrier for Worker plugin IDatabase RPC access.
|
|
1976
1977
|
*
|
|
1977
|
-
* 1. Blocks
|
|
1978
|
-
* 2.
|
|
1979
|
-
*
|
|
1978
|
+
* 1. Blocks dangerous SQLite low-level operations (ATTACH, DETACH, PRAGMA, TRIGGERS, VIEWS, VACUUM).
|
|
1979
|
+
* 2. Blocks access to all platform core tables (auth, sessions, roster, grading, lessons, etc.).
|
|
1980
|
+
* 3. Restricts DDL (CREATE/DROP/ALTER TABLE) strictly to the plugin's own namespace (`plugin_<id>_` prefix).
|
|
1980
1981
|
*/
|
|
1981
1982
|
assertDatabaseAccessAllowed(sql) {
|
|
1982
1983
|
if (typeof sql !== "string" || sql.trim() === "") return;
|
|
1983
1984
|
const pluginId = this.pluginId || this.pluginActorId.replace(/^plugin:/, "");
|
|
1985
|
+
const FORBIDDEN_OPERATIONS = [
|
|
1986
|
+
/\bATTACH(?:\s+DATABASE)?\b/i,
|
|
1987
|
+
/\bDETACH(?:\s+DATABASE)?\b/i,
|
|
1988
|
+
/\bPRAGMA\b/i,
|
|
1989
|
+
/\bVACUUM\b/i,
|
|
1990
|
+
/\bCREATE\s+(?:TEMP(?:ORARY)?\s+)?(?:TRIGGER|VIEW)\b/i,
|
|
1991
|
+
/\bDROP\s+(?:TEMP(?:ORARY)?\s+)?(?:TRIGGER|VIEW)\b/i
|
|
1992
|
+
];
|
|
1993
|
+
for (const opRegex of FORBIDDEN_OPERATIONS) {
|
|
1994
|
+
if (opRegex.test(sql)) {
|
|
1995
|
+
throw new WorkerCapabilityError(
|
|
1996
|
+
this.pluginActorId,
|
|
1997
|
+
"@openlearn/core:IDatabase",
|
|
1998
|
+
`Worker plugin "${pluginId}" is forbidden from executing dangerous statement "${sql.trim()}"`
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
1984
2002
|
const FORBIDDEN_CORE_TABLES = [
|
|
1985
2003
|
"users",
|
|
1986
2004
|
"client_sessions",
|
|
@@ -1988,26 +2006,54 @@ var init_service_host = __esm({
|
|
|
1988
2006
|
"plugin_storage",
|
|
1989
2007
|
"ai_providers",
|
|
1990
2008
|
"processes",
|
|
1991
|
-
"pending_commands"
|
|
2009
|
+
"pending_commands",
|
|
2010
|
+
"classes",
|
|
2011
|
+
"students",
|
|
2012
|
+
"class_students",
|
|
2013
|
+
"lessons",
|
|
2014
|
+
"assignments",
|
|
2015
|
+
"assignment_submissions",
|
|
2016
|
+
"attendance",
|
|
2017
|
+
"schedules",
|
|
2018
|
+
"class_grade_weights",
|
|
2019
|
+
"exams",
|
|
2020
|
+
"exam_scores",
|
|
2021
|
+
"student_semester_reports",
|
|
2022
|
+
"student_rollcalls",
|
|
2023
|
+
"student_seats",
|
|
2024
|
+
"student_read_notifications",
|
|
2025
|
+
"student_lesson_progress",
|
|
2026
|
+
"courseware",
|
|
2027
|
+
"courseware_attempt",
|
|
2028
|
+
"system_resources",
|
|
2029
|
+
"events",
|
|
2030
|
+
"_migrations"
|
|
1992
2031
|
];
|
|
1993
|
-
|
|
1994
|
-
|
|
2032
|
+
const ddlMatch = /^\s*(CREATE|DROP|ALTER)\s+TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?["'`]?([a-zA-Z_][\w]*)["'`]?/i.exec(sql);
|
|
2033
|
+
if (ddlMatch) {
|
|
2034
|
+
const table = ddlMatch[2];
|
|
2035
|
+
const validNamespaces = [];
|
|
2036
|
+
if (this.dbPluginId) {
|
|
2037
|
+
validNamespaces.push(`plugin_${this.dbPluginId.replace(/[^a-zA-Z0-9_]/g, "_")}_`);
|
|
2038
|
+
}
|
|
2039
|
+
if (pluginId) {
|
|
2040
|
+
validNamespaces.push(`plugin_${pluginId.replace(/[^a-zA-Z0-9_]/g, "_")}_`);
|
|
2041
|
+
}
|
|
2042
|
+
const isAllowed = validNamespaces.some((ns) => table.startsWith(ns));
|
|
2043
|
+
if (!isAllowed) {
|
|
1995
2044
|
throw new WorkerCapabilityError(
|
|
1996
2045
|
this.pluginActorId,
|
|
1997
2046
|
"@openlearn/core:IDatabase",
|
|
1998
|
-
`Worker plugin "${pluginId}" is
|
|
2047
|
+
`Worker plugin "${pluginId}" is not permitted to perform DDL on table "${table}"`
|
|
1999
2048
|
);
|
|
2000
2049
|
}
|
|
2001
2050
|
}
|
|
2002
|
-
const
|
|
2003
|
-
|
|
2004
|
-
const table = ddlMatch[2];
|
|
2005
|
-
const namespace = `plugin_${pluginId.replace(/-/g, "_")}_`;
|
|
2006
|
-
if (!table.startsWith(namespace)) {
|
|
2051
|
+
for (const table of FORBIDDEN_CORE_TABLES) {
|
|
2052
|
+
if (new RegExp(`\\b${table}\\b`, "i").test(sql)) {
|
|
2007
2053
|
throw new WorkerCapabilityError(
|
|
2008
2054
|
this.pluginActorId,
|
|
2009
2055
|
"@openlearn/core:IDatabase",
|
|
2010
|
-
`Worker plugin "${pluginId}" is
|
|
2056
|
+
`Worker plugin "${pluginId}" is forbidden from accessing core security table "${table}"`
|
|
2011
2057
|
);
|
|
2012
2058
|
}
|
|
2013
2059
|
}
|
|
@@ -3078,7 +3124,11 @@ var init_worker_manager = __esm({
|
|
|
3078
3124
|
workerData: { pluginId, manifestId: manifest.id, serviceTokens, pluginDir },
|
|
3079
3125
|
eval: false,
|
|
3080
3126
|
stdout: true,
|
|
3081
|
-
stderr: true
|
|
3127
|
+
stderr: true,
|
|
3128
|
+
resourceLimits: {
|
|
3129
|
+
maxOldGenerationSizeMb: 128,
|
|
3130
|
+
maxYoungGenerationSizeMb: 32
|
|
3131
|
+
}
|
|
3082
3132
|
});
|
|
3083
3133
|
const pluginLogger = createLogger(`Plugin:${manifest.id || pluginId}`);
|
|
3084
3134
|
worker.stdout.on("data", (chunk) => {
|
|
@@ -3110,7 +3160,8 @@ var init_worker_manager = __esm({
|
|
|
3110
3160
|
// `manifest.id` (NOT the DB UUID `pluginId`) so the prefix
|
|
3111
3161
|
// matches what the frontend `ctx.invokeCommand` produces —
|
|
3112
3162
|
// see packages/core/plugin-host/plugin-namespace.ts.
|
|
3113
|
-
manifest.id
|
|
3163
|
+
manifest.id,
|
|
3164
|
+
pluginId
|
|
3114
3165
|
);
|
|
3115
3166
|
const createdAt = Date.now();
|
|
3116
3167
|
this.registry.register(pluginId, {
|
|
@@ -3283,8 +3334,8 @@ __export(server_exports, {
|
|
|
3283
3334
|
});
|
|
3284
3335
|
module.exports = __toCommonJS(server_exports);
|
|
3285
3336
|
var import_express3 = __toESM(require("express"), 1);
|
|
3286
|
-
var
|
|
3287
|
-
var
|
|
3337
|
+
var import_path15 = __toESM(require("path"), 1);
|
|
3338
|
+
var import_fs14 = __toESM(require("fs"), 1);
|
|
3288
3339
|
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
3289
3340
|
var import_vite = require("vite");
|
|
3290
3341
|
var import_http = require("http");
|
|
@@ -3831,6 +3882,7 @@ function parseRequiresEntry(entry) {
|
|
|
3831
3882
|
// packages/core/db/index.ts
|
|
3832
3883
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
3833
3884
|
var import_path = __toESM(require("path"), 1);
|
|
3885
|
+
var import_os = __toESM(require("os"), 1);
|
|
3834
3886
|
var import_fs = require("fs");
|
|
3835
3887
|
var import_url = require("url");
|
|
3836
3888
|
var import_crypto = __toESM(require("crypto"), 1);
|
|
@@ -3854,6 +3906,11 @@ var dbPath;
|
|
|
3854
3906
|
if (process.env.OPENLEARN_DB_PATH) {
|
|
3855
3907
|
dbPath = process.env.OPENLEARN_DB_PATH;
|
|
3856
3908
|
(0, import_fs.mkdirSync)(import_path.default.dirname(dbPath), { recursive: true });
|
|
3909
|
+
} else if (process.env.VITEST) {
|
|
3910
|
+
const poolId = process.env.VITEST_POOL_ID || process.pid;
|
|
3911
|
+
const testDbDir = import_path.default.join(import_os.default.tmpdir(), "openlearn_test_dbs");
|
|
3912
|
+
(0, import_fs.mkdirSync)(testDbDir, { recursive: true });
|
|
3913
|
+
dbPath = import_path.default.join(testDbDir, `openlearn_test_${poolId}.db`);
|
|
3857
3914
|
} else {
|
|
3858
3915
|
if (typeof import_meta !== "undefined" && import_meta.url) {
|
|
3859
3916
|
const __filename2 = (0, import_url.fileURLToPath)(import_meta.url);
|
|
@@ -3884,6 +3941,7 @@ db.exec(`
|
|
|
3884
3941
|
timeline TEXT,
|
|
3885
3942
|
progress_mode TEXT DEFAULT 'manual',
|
|
3886
3943
|
progress_conditions TEXT,
|
|
3944
|
+
creator_id TEXT,
|
|
3887
3945
|
created_at INTEGER NOT NULL,
|
|
3888
3946
|
updated_at INTEGER NOT NULL
|
|
3889
3947
|
);
|
|
@@ -4221,6 +4279,10 @@ db.exec(`
|
|
|
4221
4279
|
CREATE INDEX IF NOT EXISTS idx_assignments_class ON assignments(class_id);
|
|
4222
4280
|
CREATE INDEX IF NOT EXISTS idx_attendance_schedule ON attendance(schedule_id);
|
|
4223
4281
|
`);
|
|
4282
|
+
try {
|
|
4283
|
+
db.exec("ALTER TABLE lessons ADD COLUMN creator_id TEXT");
|
|
4284
|
+
} catch {
|
|
4285
|
+
}
|
|
4224
4286
|
try {
|
|
4225
4287
|
db.exec(`ALTER TABLE plugins ADD COLUMN execution_mode TEXT DEFAULT 'inline'`);
|
|
4226
4288
|
} catch {
|
|
@@ -4317,6 +4379,9 @@ try {
|
|
|
4317
4379
|
const insertStmt = db.prepare("INSERT INTO users (id, username, password_hash, role, name, created_at) VALUES (?, ?, ?, ?, ?, ?)");
|
|
4318
4380
|
insertStmt.run("usr_admin", "admin", hashPassword("admin"), "administrator", "System Admin", Date.now());
|
|
4319
4381
|
insertStmt.run("usr_teacher", "teacher", hashPassword("teacher"), "teacher", "Regular Teacher", Date.now());
|
|
4382
|
+
console.warn(
|
|
4383
|
+
"[SECURITY WARNING] Default users initialized (admin/admin, teacher/teacher). In production environments, immediately change these passwords via POST /api/auth/change-password!"
|
|
4384
|
+
);
|
|
4320
4385
|
}
|
|
4321
4386
|
} catch (e) {
|
|
4322
4387
|
console.error("Failed to seed default users:", e);
|
|
@@ -5671,13 +5736,15 @@ var BuiltinPlugin = {
|
|
|
5671
5736
|
async execute(command) {
|
|
5672
5737
|
const payload = command.payload;
|
|
5673
5738
|
const lessonId = (0, import_uuid6.v7)();
|
|
5674
|
-
const
|
|
5675
|
-
|
|
5739
|
+
const rawCreatorId = payload.creatorId || command.actorId || "admin";
|
|
5740
|
+
const creatorId = typeof rawCreatorId === "string" && rawCreatorId.startsWith("user:") ? rawCreatorId.split(":")[1] : rawCreatorId;
|
|
5741
|
+
const stmt = db2.prepare("INSERT INTO lessons (id, title, content, creator_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)");
|
|
5742
|
+
stmt.run(lessonId, payload.title, payload.content || "", creatorId, Date.now(), Date.now());
|
|
5676
5743
|
await eventBus.publish({
|
|
5677
5744
|
id: (0, import_uuid6.v7)(),
|
|
5678
5745
|
type: "lesson.created",
|
|
5679
5746
|
source: "builtin.lesson",
|
|
5680
|
-
payload: { id: lessonId, title: payload.title },
|
|
5747
|
+
payload: { id: lessonId, title: payload.title, creatorId },
|
|
5681
5748
|
timestamp: Date.now(),
|
|
5682
5749
|
correlationId: command.id
|
|
5683
5750
|
});
|
|
@@ -6650,12 +6717,27 @@ var BuiltinPlugin = {
|
|
|
6650
6717
|
const base64Content = base64Data.replace(/^data:[^;]+;base64,/, "");
|
|
6651
6718
|
const fileBuffer = Buffer.from(base64Content, "base64");
|
|
6652
6719
|
if (ext === ".html" || ext === ".htm") {
|
|
6653
|
-
const entryName = filename;
|
|
6654
|
-
|
|
6720
|
+
const entryName = import_path3.default.basename(filename.replace(/\\/g, "/"));
|
|
6721
|
+
if (!entryName || entryName === "." || entryName === "..") {
|
|
6722
|
+
throw new Error("Invalid courseware filename");
|
|
6723
|
+
}
|
|
6724
|
+
const destPath = import_path3.default.resolve(storageDir, entryName);
|
|
6725
|
+
if (!destPath.startsWith(storageDir)) {
|
|
6726
|
+
throw new Error(`Path traversal attempt in HTML courseware upload: ${filename}`);
|
|
6727
|
+
}
|
|
6728
|
+
import_fs3.default.writeFileSync(destPath, fileBuffer);
|
|
6655
6729
|
const coursewareId = "cw_" + import_crypto3.default.randomBytes(8).toString("hex");
|
|
6656
6730
|
db2.prepare(
|
|
6657
6731
|
"INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
6658
6732
|
).run(coursewareId, uuid, name, "html", entryName, Date.now());
|
|
6733
|
+
try {
|
|
6734
|
+
const resId = "res_" + import_crypto3.default.randomBytes(8).toString("hex");
|
|
6735
|
+
db2.prepare(
|
|
6736
|
+
"INSERT INTO system_resources (id, name, type, content, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
6737
|
+
).run(resId, name, "html", fileBuffer.toString("utf-8"), Date.now());
|
|
6738
|
+
} catch (resErr) {
|
|
6739
|
+
console.error("[builtin.courseware] \u767B\u8BB0 system_resources \u5931\u8D25:", resErr);
|
|
6740
|
+
}
|
|
6659
6741
|
await eventBus.publish({
|
|
6660
6742
|
id: (0, import_uuid6.v7)(),
|
|
6661
6743
|
type: "courseware.uploaded",
|
|
@@ -6744,6 +6826,23 @@ var BuiltinPlugin = {
|
|
|
6744
6826
|
db2.prepare(
|
|
6745
6827
|
"INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
6746
6828
|
).run(coursewareId, uuid, name, "folder", entry, Date.now());
|
|
6829
|
+
try {
|
|
6830
|
+
const resFiles = [];
|
|
6831
|
+
for (const [relativePath, file] of Object.entries(loadedZip.files)) {
|
|
6832
|
+
const fileObj = file;
|
|
6833
|
+
if (!fileObj.dir) {
|
|
6834
|
+
const buf = await fileObj.async("nodebuffer");
|
|
6835
|
+
const isBinary = /\.(png|jpe?g|gif|webp|ico|mp3|wav|mp4|woff2?|ttf|otf|eot)$/i.test(relativePath);
|
|
6836
|
+
resFiles.push({ path: relativePath, content: isBinary ? buf.toString("base64") : buf.toString("utf-8") });
|
|
6837
|
+
}
|
|
6838
|
+
}
|
|
6839
|
+
const resId = "res_" + import_crypto3.default.randomBytes(8).toString("hex");
|
|
6840
|
+
db2.prepare(
|
|
6841
|
+
"INSERT INTO system_resources (id, name, type, content, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
6842
|
+
).run(resId, name, "folder", JSON.stringify(resFiles), Date.now());
|
|
6843
|
+
} catch (resErr) {
|
|
6844
|
+
console.error("[builtin.courseware] \u767B\u8BB0 system_resources \u5931\u8D25:", resErr);
|
|
6845
|
+
}
|
|
6747
6846
|
await eventBus.publish({
|
|
6748
6847
|
id: (0, import_uuid6.v7)(),
|
|
6749
6848
|
type: "courseware.uploaded",
|
|
@@ -7582,7 +7681,7 @@ var import_node_url = require("node:url");
|
|
|
7582
7681
|
var esbuild = __toESM(require("esbuild"), 1);
|
|
7583
7682
|
var import_jszip = __toESM(require("jszip"), 1);
|
|
7584
7683
|
var fs3 = __toESM(require("fs"), 1);
|
|
7585
|
-
var
|
|
7684
|
+
var os2 = __toESM(require("os"), 1);
|
|
7586
7685
|
var path4 = __toESM(require("path"), 1);
|
|
7587
7686
|
var import_uuid10 = require("uuid");
|
|
7588
7687
|
var MAX_UNCOMPRESSED_SIZE = 300 * 1024 * 1024;
|
|
@@ -7670,7 +7769,7 @@ async function validateAndBundleZip(zipBuffer) {
|
|
|
7670
7769
|
);
|
|
7671
7770
|
}
|
|
7672
7771
|
const entryCode = await entryFile.async("string");
|
|
7673
|
-
const tmpDir = path4.join(
|
|
7772
|
+
const tmpDir = path4.join(os2.tmpdir(), `plugin-build-${(0, import_uuid10.v7)()}`);
|
|
7674
7773
|
try {
|
|
7675
7774
|
fs3.mkdirSync(tmpDir, { recursive: true });
|
|
7676
7775
|
for (const [name, file] of Object.entries(zip.files)) {
|
|
@@ -8460,11 +8559,11 @@ var PluginDistributionManager = class {
|
|
|
8460
8559
|
return;
|
|
8461
8560
|
}
|
|
8462
8561
|
const filePath = this.pluginHost.getPluginFilePath(this.pluginHost.resolvePluginUuid(pluginId));
|
|
8463
|
-
const
|
|
8464
|
-
if (!
|
|
8562
|
+
const fs16 = await import("node:fs");
|
|
8563
|
+
if (!fs16.existsSync(filePath)) {
|
|
8465
8564
|
throw new Error(`Cannot reload plugin "${pluginId}": source file missing at ${filePath}`);
|
|
8466
8565
|
}
|
|
8467
|
-
const code =
|
|
8566
|
+
const code = fs16.readFileSync(filePath, "utf-8");
|
|
8468
8567
|
await this.pluginHost.reloadPlugin(pluginId, code);
|
|
8469
8568
|
}
|
|
8470
8569
|
async updateFromZip(zipBuffer, options = {}) {
|
|
@@ -8511,6 +8610,16 @@ function validatePluginStateTransition(currentState, nextState, pluginId) {
|
|
|
8511
8610
|
var ACTIVATION_TIMEOUT_MS = 5e3;
|
|
8512
8611
|
var DEACTIVATION_TIMEOUT_MS = 5e3;
|
|
8513
8612
|
var OPENLEARN_VERSION = "0.2.5";
|
|
8613
|
+
function createPluginStaticMiddleware(absDir) {
|
|
8614
|
+
return [
|
|
8615
|
+
(_req, res, next) => {
|
|
8616
|
+
res.setHeader("Content-Security-Policy", "sandbox allow-scripts allow-forms allow-downloads; default-src 'self' 'unsafe-inline' blob: data:");
|
|
8617
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
8618
|
+
next();
|
|
8619
|
+
},
|
|
8620
|
+
import_express.default.static(absDir)
|
|
8621
|
+
];
|
|
8622
|
+
}
|
|
8514
8623
|
var PluginHost = class {
|
|
8515
8624
|
constructor(serviceRegistry, esmLoader, db2, pluginsDir) {
|
|
8516
8625
|
this.serviceRegistry = serviceRegistry;
|
|
@@ -8556,7 +8665,7 @@ var PluginHost = class {
|
|
|
8556
8665
|
const pluginDir = this.getPluginDir(p.id);
|
|
8557
8666
|
const absDir = import_path4.default.join(pluginDir, m.deploy.staticDir);
|
|
8558
8667
|
if (import_fs5.default.existsSync(absDir)) {
|
|
8559
|
-
this.expressApp.use(m.deploy.staticRoute,
|
|
8668
|
+
this.expressApp.use(m.deploy.staticRoute, ...createPluginStaticMiddleware(absDir));
|
|
8560
8669
|
this._registeredRoutes.set(m.id, m.deploy.staticRoute);
|
|
8561
8670
|
console.log(`[PluginHost] Restored static route "${m.deploy.staticRoute}" for plugin "${m.id}"`);
|
|
8562
8671
|
}
|
|
@@ -9766,10 +9875,15 @@ var PluginHost = class {
|
|
|
9766
9875
|
import_fs5.default.writeFileSync(import_path4.default.join(pluginDir, "frontend.js"), frontendCode, "utf-8");
|
|
9767
9876
|
}
|
|
9768
9877
|
if (manifest.deploy?.script) {
|
|
9878
|
+
const rawScript = manifest.deploy.script.replace(/\\/g, "/");
|
|
9879
|
+
const resolvedScript = import_path4.default.resolve(pluginDir, rawScript);
|
|
9880
|
+
if (rawScript.includes("..") || !resolvedScript.startsWith(pluginDir + import_path4.default.sep)) {
|
|
9881
|
+
throw new Error(`Security Violation: Zip Slip detected in deploy script path "${manifest.deploy.script}"`);
|
|
9882
|
+
}
|
|
9769
9883
|
const deployFile = zip.file(manifest.deploy.script);
|
|
9770
9884
|
if (deployFile) {
|
|
9771
9885
|
const deployCode = await deployFile.async("string");
|
|
9772
|
-
import_fs5.default.writeFileSync(
|
|
9886
|
+
import_fs5.default.writeFileSync(resolvedScript, deployCode, "utf-8");
|
|
9773
9887
|
}
|
|
9774
9888
|
}
|
|
9775
9889
|
this.emitProgress(manifest.id, "extracting", "Extracting assets...");
|
|
@@ -9779,20 +9893,29 @@ var PluginHost = class {
|
|
|
9779
9893
|
if (storageEntries.length > 0) {
|
|
9780
9894
|
console.log(`[PluginHost] Extracting ${storageEntries.length} static asset files for plugin "${manifest.id}"...`);
|
|
9781
9895
|
const dirs = /* @__PURE__ */ new Set();
|
|
9782
|
-
for (const
|
|
9783
|
-
|
|
9896
|
+
for (const rawName of storageEntries) {
|
|
9897
|
+
const normalized = rawName.replace(/\\/g, "/");
|
|
9898
|
+
const destPath = import_path4.default.resolve(pluginDir, normalized);
|
|
9899
|
+
if (normalized.includes("..") || !destPath.startsWith(pluginDir + import_path4.default.sep)) {
|
|
9900
|
+
throw new Error(`Security Violation: Zip Slip detected in asset path "${rawName}"`);
|
|
9901
|
+
}
|
|
9902
|
+
dirs.add(import_path4.default.dirname(destPath));
|
|
9784
9903
|
}
|
|
9785
9904
|
for (const dir of dirs) {
|
|
9786
|
-
import_fs5.default.mkdirSync(
|
|
9905
|
+
import_fs5.default.mkdirSync(dir, { recursive: true });
|
|
9787
9906
|
}
|
|
9788
9907
|
const BATCH_SIZE = 10;
|
|
9789
9908
|
for (let i = 0; i < storageEntries.length; i += BATCH_SIZE) {
|
|
9790
9909
|
const batch = storageEntries.slice(i, i + BATCH_SIZE);
|
|
9791
9910
|
await Promise.all(batch.map(async (name) => {
|
|
9911
|
+
const normalized = name.replace(/\\/g, "/");
|
|
9912
|
+
const destPath = import_path4.default.resolve(pluginDir, normalized);
|
|
9913
|
+
if (normalized.includes("..") || !destPath.startsWith(pluginDir + import_path4.default.sep)) {
|
|
9914
|
+
throw new Error(`Security Violation: Zip Slip detected in asset path "${name}"`);
|
|
9915
|
+
}
|
|
9792
9916
|
const file = zip.file(name);
|
|
9793
9917
|
if (file) {
|
|
9794
9918
|
const content = await file.async("nodebuffer");
|
|
9795
|
-
const destPath = import_path4.default.join(pluginDir, name);
|
|
9796
9919
|
import_fs5.default.writeFileSync(destPath, content);
|
|
9797
9920
|
}
|
|
9798
9921
|
}));
|
|
@@ -9810,7 +9933,7 @@ var PluginHost = class {
|
|
|
9810
9933
|
};
|
|
9811
9934
|
import_fs5.default.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), "utf-8");
|
|
9812
9935
|
const { execSync: execSync2 } = await import("node:child_process");
|
|
9813
|
-
execSync2("npm install --production --no-audit --no-fund --legacy-peer-deps --registry=https://registry.npmmirror.com", {
|
|
9936
|
+
execSync2("npm install --production --no-audit --no-fund --legacy-peer-deps --ignore-scripts --registry=https://registry.npmmirror.com", {
|
|
9814
9937
|
cwd: pluginDir,
|
|
9815
9938
|
stdio: "ignore"
|
|
9816
9939
|
});
|
|
@@ -9820,30 +9943,36 @@ var PluginHost = class {
|
|
|
9820
9943
|
}
|
|
9821
9944
|
}
|
|
9822
9945
|
if (manifest.deploy?.script) {
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
}
|
|
9828
|
-
if (import_fs5.default.existsSync(deployScriptPath)) {
|
|
9829
|
-
console.log(`[PluginHost] Running deploy script: node ${deployScriptPath}`);
|
|
9830
|
-
try {
|
|
9831
|
-
const { execSync: execSync2 } = await import("node:child_process");
|
|
9832
|
-
execSync2(`node "${deployScriptPath}" "${process.cwd()}"`, { timeout: 12e4 });
|
|
9833
|
-
console.log(`[PluginHost] Deploy script completed for plugin "${manifest.id}"`);
|
|
9834
|
-
} catch (deployErr) {
|
|
9835
|
-
console.error(`[PluginHost] Deploy script failed for plugin "${manifest.id}":`, deployErr.message);
|
|
9836
|
-
throw new Error(`Deploy script "${manifest.deploy.script}" failed: ${deployErr.message}`);
|
|
9837
|
-
}
|
|
9946
|
+
if (process.env.ALLOW_UNSAFE_PLUGIN_SCRIPTS !== "true") {
|
|
9947
|
+
console.warn(
|
|
9948
|
+
`[SECURITY WARNING] Deploy script "${manifest.deploy.script}" for plugin "${manifest.id}" blocked by default security policy. Set ALLOW_UNSAFE_PLUGIN_SCRIPTS=true to enable.`
|
|
9949
|
+
);
|
|
9838
9950
|
} else {
|
|
9839
|
-
|
|
9951
|
+
let deployScriptPath = import_path4.default.join(pluginDir, manifest.deploy.script);
|
|
9952
|
+
if (!import_fs5.default.existsSync(deployScriptPath)) {
|
|
9953
|
+
const altPath = import_path4.default.join(process.cwd(), "v2_plugins", "scratch-editor-deploy", manifest.deploy.script);
|
|
9954
|
+
if (import_fs5.default.existsSync(altPath)) deployScriptPath = altPath;
|
|
9955
|
+
}
|
|
9956
|
+
if (import_fs5.default.existsSync(deployScriptPath)) {
|
|
9957
|
+
console.log(`[PluginHost] Running deploy script: node ${deployScriptPath}`);
|
|
9958
|
+
try {
|
|
9959
|
+
const { execSync: execSync2 } = await import("node:child_process");
|
|
9960
|
+
execSync2(`node "${deployScriptPath}" "${process.cwd()}"`, { timeout: 12e4 });
|
|
9961
|
+
console.log(`[PluginHost] Deploy script completed for plugin "${manifest.id}"`);
|
|
9962
|
+
} catch (deployErr) {
|
|
9963
|
+
console.error(`[PluginHost] Deploy script failed for plugin "${manifest.id}":`, deployErr.message);
|
|
9964
|
+
throw new Error(`Deploy script "${manifest.deploy.script}" failed: ${deployErr.message}`);
|
|
9965
|
+
}
|
|
9966
|
+
} else {
|
|
9967
|
+
console.warn(`[PluginHost] Deploy script "${manifest.deploy.script}" not found for plugin "${manifest.id}"`);
|
|
9968
|
+
}
|
|
9840
9969
|
}
|
|
9841
9970
|
}
|
|
9842
9971
|
if (manifest.deploy?.staticRoute && manifest.deploy?.staticDir && this.expressApp) {
|
|
9843
9972
|
const route = manifest.deploy.staticRoute;
|
|
9844
9973
|
const absDir = import_path4.default.join(pluginDir, manifest.deploy.staticDir);
|
|
9845
9974
|
if (import_fs5.default.existsSync(absDir)) {
|
|
9846
|
-
this.expressApp.use(route,
|
|
9975
|
+
this.expressApp.use(route, ...createPluginStaticMiddleware(absDir));
|
|
9847
9976
|
this._registeredRoutes.set(manifest.id, route);
|
|
9848
9977
|
console.log(`[PluginHost] Registered static route "${route}" for plugin "${manifest.id}"`);
|
|
9849
9978
|
} else {
|
|
@@ -10580,8 +10709,8 @@ var HotReloadController = class {
|
|
|
10580
10709
|
const timer = setTimeout(async () => {
|
|
10581
10710
|
this.debounceTimers.delete(pluginId);
|
|
10582
10711
|
try {
|
|
10583
|
-
const
|
|
10584
|
-
const newSourceCode = await
|
|
10712
|
+
const fs16 = await import("fs/promises");
|
|
10713
|
+
const newSourceCode = await fs16.readFile(filePath, "utf-8");
|
|
10585
10714
|
console.log(
|
|
10586
10715
|
`[HotReload] File changed: ${filePath} \u2192 triggering reload for "${pluginId}"`
|
|
10587
10716
|
);
|
|
@@ -17372,13 +17501,22 @@ function setupPresence({ io, eventBus }) {
|
|
|
17372
17501
|
};
|
|
17373
17502
|
io.on("connection", (socket) => {
|
|
17374
17503
|
let registeredStudentId = null;
|
|
17504
|
+
const session = socket.data?.session;
|
|
17505
|
+
const isTeacherOrAdmin = session?.role === "teacher" || session?.role === "administrator";
|
|
17375
17506
|
socket.on("register-student", (data) => {
|
|
17507
|
+
if (session && !isTeacherOrAdmin && session.userId !== data.studentId) {
|
|
17508
|
+
console.warn(`[Presence Security] Student ${session.userId} attempted to impersonate ${data.studentId}`);
|
|
17509
|
+
return socket.emit("error", { message: "Forbidden: Cannot register presence for another student" });
|
|
17510
|
+
}
|
|
17376
17511
|
registeredStudentId = data.studentId;
|
|
17377
17512
|
onlineStudents.set(data.studentId, { socketId: socket.id, name: data.name });
|
|
17378
17513
|
console.log(`[Presence] Student online: ${data.name} (${data.studentId})`);
|
|
17379
17514
|
broadcastPresence();
|
|
17380
17515
|
});
|
|
17381
17516
|
socket.on("enter-lesson", (data) => {
|
|
17517
|
+
if (session && !isTeacherOrAdmin && session.userId !== data.studentId) {
|
|
17518
|
+
return socket.emit("error", { message: "Forbidden: Cannot enter lesson for another student" });
|
|
17519
|
+
}
|
|
17382
17520
|
activeStudentLessons.set(data.studentId, data.lessonId);
|
|
17383
17521
|
socket.join(data.lessonId);
|
|
17384
17522
|
console.log(`[Presence] Student ${data.studentId} entered lesson ${data.lessonId}`);
|
|
@@ -17392,6 +17530,9 @@ function setupPresence({ io, eventBus }) {
|
|
|
17392
17530
|
}
|
|
17393
17531
|
});
|
|
17394
17532
|
socket.on("leave-lesson", (data) => {
|
|
17533
|
+
if (session && !isTeacherOrAdmin && session.userId !== data.studentId) {
|
|
17534
|
+
return socket.emit("error", { message: "Forbidden: Cannot leave lesson for another student" });
|
|
17535
|
+
}
|
|
17395
17536
|
const oldRoom = activeStudentLessons.get(data.studentId);
|
|
17396
17537
|
if (oldRoom) {
|
|
17397
17538
|
socket.leave(oldRoom);
|
|
@@ -17428,10 +17569,18 @@ function setupPresence({ io, eventBus }) {
|
|
|
17428
17569
|
}
|
|
17429
17570
|
);
|
|
17430
17571
|
socket.on("teacher-broadcast-segment", (data) => {
|
|
17572
|
+
if (session && !isTeacherOrAdmin) {
|
|
17573
|
+
console.warn(`[Presence Security] Unauthorized teacher-broadcast-segment by ${session?.userId}`);
|
|
17574
|
+
return socket.emit("error", { message: "Forbidden: Only teachers or administrators can broadcast segments" });
|
|
17575
|
+
}
|
|
17431
17576
|
lessonActiveSegments.set(data.lessonId, data.activeSegmentId);
|
|
17432
17577
|
io.to(data.lessonId).emit("student-active-segment-changed", data);
|
|
17433
17578
|
});
|
|
17434
17579
|
socket.on("teacher-ping-student", (data) => {
|
|
17580
|
+
if (session && !isTeacherOrAdmin) {
|
|
17581
|
+
console.warn(`[Presence Security] Unauthorized teacher-ping-student by ${session?.userId}`);
|
|
17582
|
+
return socket.emit("error", { message: "Forbidden: Only teachers or administrators can ping students" });
|
|
17583
|
+
}
|
|
17435
17584
|
console.log(`[Ping] Teacher pinged student ${data.studentId} for lesson ${data.lessonId}`);
|
|
17436
17585
|
const studentOnlineInfo = onlineStudents.get(data.studentId);
|
|
17437
17586
|
if (studentOnlineInfo) {
|
|
@@ -17539,6 +17688,83 @@ async function runStartupMigrations(db2) {
|
|
|
17539
17688
|
}
|
|
17540
17689
|
}
|
|
17541
17690
|
|
|
17691
|
+
// server/utils/migrate.ts
|
|
17692
|
+
var import_fs7 = __toESM(require("fs"), 1);
|
|
17693
|
+
var import_path6 = __toESM(require("path"), 1);
|
|
17694
|
+
function splitSqlStatements(sql) {
|
|
17695
|
+
return sql.split(";").map((s) => s.trim()).filter((s) => {
|
|
17696
|
+
const content = s.replace(/--.*$/gm, "").trim();
|
|
17697
|
+
return content.length > 0;
|
|
17698
|
+
});
|
|
17699
|
+
}
|
|
17700
|
+
function parseMigrationSql(name, content) {
|
|
17701
|
+
const upMatch = content.match(/--\s*UP([\s\S]*?)(?=--\s*DOWN|$)/i);
|
|
17702
|
+
const downMatch = content.match(/--\s*DOWN([\s\S]*)$/i);
|
|
17703
|
+
const up = upMatch ? upMatch[1].trim() : content.trim();
|
|
17704
|
+
const down = downMatch ? downMatch[1].trim() : "";
|
|
17705
|
+
return { name, up, down };
|
|
17706
|
+
}
|
|
17707
|
+
function loadMigrationsFromDirectory(dirPath) {
|
|
17708
|
+
if (!import_fs7.default.existsSync(dirPath)) {
|
|
17709
|
+
return [];
|
|
17710
|
+
}
|
|
17711
|
+
const files = import_fs7.default.readdirSync(dirPath).filter((f) => f.endsWith(".sql")).sort((a, b) => a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" }));
|
|
17712
|
+
return files.map((file) => {
|
|
17713
|
+
const fullPath = import_path6.default.join(dirPath, file);
|
|
17714
|
+
const content = import_fs7.default.readFileSync(fullPath, "utf8");
|
|
17715
|
+
const name = import_path6.default.basename(file, ".sql");
|
|
17716
|
+
return parseMigrationSql(name, content);
|
|
17717
|
+
});
|
|
17718
|
+
}
|
|
17719
|
+
function executeSqlStatements(db2, sql) {
|
|
17720
|
+
const statements = splitSqlStatements(sql);
|
|
17721
|
+
for (const stmt of statements) {
|
|
17722
|
+
try {
|
|
17723
|
+
db2.exec(stmt);
|
|
17724
|
+
} catch (err) {
|
|
17725
|
+
if (err?.message && err.message.includes("duplicate column name")) {
|
|
17726
|
+
console.warn(`[Migration] Column already exists, safely continuing: ${stmt.slice(0, 60)}...`);
|
|
17727
|
+
continue;
|
|
17728
|
+
}
|
|
17729
|
+
throw err;
|
|
17730
|
+
}
|
|
17731
|
+
}
|
|
17732
|
+
}
|
|
17733
|
+
function runMigrations(db2, migrations) {
|
|
17734
|
+
db2.exec(`
|
|
17735
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
17736
|
+
name TEXT PRIMARY KEY,
|
|
17737
|
+
applied_at INTEGER NOT NULL,
|
|
17738
|
+
checksum TEXT NOT NULL
|
|
17739
|
+
);
|
|
17740
|
+
`);
|
|
17741
|
+
const applied = new Set(
|
|
17742
|
+
db2.prepare("SELECT name FROM _migrations").all().map((r) => r.name)
|
|
17743
|
+
);
|
|
17744
|
+
for (const migration of migrations) {
|
|
17745
|
+
if (applied.has(migration.name)) continue;
|
|
17746
|
+
console.log(`[Migration] Applying: ${migration.name}`);
|
|
17747
|
+
try {
|
|
17748
|
+
executeSqlStatements(db2, migration.up);
|
|
17749
|
+
const checksum = simpleChecksum(migration.up);
|
|
17750
|
+
db2.prepare("INSERT INTO _migrations (name, applied_at, checksum) VALUES (?, ?, ?)").run(migration.name, Date.now(), checksum);
|
|
17751
|
+
console.log(`[Migration] Applied: ${migration.name}`);
|
|
17752
|
+
} catch (err) {
|
|
17753
|
+
console.error(`[Migration] FAILED: ${migration.name}`, err);
|
|
17754
|
+
throw err;
|
|
17755
|
+
}
|
|
17756
|
+
}
|
|
17757
|
+
}
|
|
17758
|
+
function simpleChecksum(text) {
|
|
17759
|
+
let hash = 0;
|
|
17760
|
+
for (let i = 0; i < text.length; i++) {
|
|
17761
|
+
const char = text.charCodeAt(i);
|
|
17762
|
+
hash = (hash << 5) - hash + char;
|
|
17763
|
+
hash |= 0;
|
|
17764
|
+
}
|
|
17765
|
+
return hash.toString(16);
|
|
17766
|
+
}
|
|
17767
|
+
|
|
17542
17768
|
// server/ai-agent.ts
|
|
17543
17769
|
var import_genai2 = require("@google/genai");
|
|
17544
17770
|
var import_crypto4 = __toESM(require("crypto"), 1);
|
|
@@ -17850,6 +18076,90 @@ var runOpenAIAgentChat = async (provider, request) => {
|
|
|
17850
18076
|
};
|
|
17851
18077
|
};
|
|
17852
18078
|
|
|
18079
|
+
// server/middleware/auth.ts
|
|
18080
|
+
function getValidSession(token) {
|
|
18081
|
+
let sessionRow;
|
|
18082
|
+
try {
|
|
18083
|
+
sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
|
|
18084
|
+
} catch {
|
|
18085
|
+
return null;
|
|
18086
|
+
}
|
|
18087
|
+
if (!sessionRow) return null;
|
|
18088
|
+
const now = Date.now();
|
|
18089
|
+
if (sessionRow.expires_at && sessionRow.expires_at < now) {
|
|
18090
|
+
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
18091
|
+
return null;
|
|
18092
|
+
}
|
|
18093
|
+
const idleTimeout = 24 * 60 * 60 * 1e3;
|
|
18094
|
+
if (sessionRow.updated_at && now - sessionRow.updated_at > idleTimeout) {
|
|
18095
|
+
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
18096
|
+
return null;
|
|
18097
|
+
}
|
|
18098
|
+
kernelContainer.db.prepare("UPDATE client_sessions SET updated_at = ? WHERE id = ?").run(now, token);
|
|
18099
|
+
return JSON.parse(sessionRow.session_data);
|
|
18100
|
+
}
|
|
18101
|
+
function getCookieToken(req) {
|
|
18102
|
+
const rc = req.headers.cookie;
|
|
18103
|
+
if (!rc) return null;
|
|
18104
|
+
const parts = rc.split(";");
|
|
18105
|
+
for (const part of parts) {
|
|
18106
|
+
const trimmed = part.trim();
|
|
18107
|
+
if (trimmed.startsWith("edu_os_token=")) {
|
|
18108
|
+
return trimmed.substring("edu_os_token=".length);
|
|
18109
|
+
}
|
|
18110
|
+
}
|
|
18111
|
+
return null;
|
|
18112
|
+
}
|
|
18113
|
+
function getActorId(req) {
|
|
18114
|
+
const token = getCookieToken(req);
|
|
18115
|
+
if (!token) return "anonymous";
|
|
18116
|
+
try {
|
|
18117
|
+
const session = getValidSession(token);
|
|
18118
|
+
if (!session) return "anonymous";
|
|
18119
|
+
let role = session.subRole || session.role;
|
|
18120
|
+
if (session.username === "admin" || session.userId === "usr_admin" || role === "admin" || role === "administrator") {
|
|
18121
|
+
role = "administrator";
|
|
18122
|
+
}
|
|
18123
|
+
if (role) {
|
|
18124
|
+
return `user:${session.userId || "demo"}:${role}`;
|
|
18125
|
+
}
|
|
18126
|
+
return "anonymous";
|
|
18127
|
+
} catch {
|
|
18128
|
+
return "anonymous";
|
|
18129
|
+
}
|
|
18130
|
+
}
|
|
18131
|
+
function requireAuth(...roles) {
|
|
18132
|
+
return (req, res, next) => {
|
|
18133
|
+
const token = getCookieToken(req);
|
|
18134
|
+
if (!token) return res.status(401).json({ success: false, error: "Authentication required" });
|
|
18135
|
+
const session = getValidSession(token);
|
|
18136
|
+
if (!session) return res.status(401).json({ success: false, error: "Session expired or invalid" });
|
|
18137
|
+
if (roles.length > 0) {
|
|
18138
|
+
let userRole = session.role;
|
|
18139
|
+
if (session.username === "admin" || session.userId === "usr_admin" || userRole === "admin") {
|
|
18140
|
+
userRole = "administrator";
|
|
18141
|
+
}
|
|
18142
|
+
const effectiveRoles = roles.map((r) => r === "admin" ? "administrator" : r);
|
|
18143
|
+
if (!effectiveRoles.includes(userRole)) {
|
|
18144
|
+
return res.status(403).json({ success: false, error: `Role ${session.role} not allowed. Required: ${roles.join(", ")}` });
|
|
18145
|
+
}
|
|
18146
|
+
}
|
|
18147
|
+
req.session = session;
|
|
18148
|
+
next();
|
|
18149
|
+
};
|
|
18150
|
+
}
|
|
18151
|
+
function checkIsTeacherOrAdmin(req) {
|
|
18152
|
+
const token = getCookieToken(req);
|
|
18153
|
+
if (!token) return false;
|
|
18154
|
+
try {
|
|
18155
|
+
const session = getValidSession(token);
|
|
18156
|
+
if (!session) return false;
|
|
18157
|
+
return session.role === "teacher" || session.role === "administrator";
|
|
18158
|
+
} catch {
|
|
18159
|
+
return false;
|
|
18160
|
+
}
|
|
18161
|
+
}
|
|
18162
|
+
|
|
17853
18163
|
// packages/core/bootstrap/adapter/bootstrap-registration.ts
|
|
17854
18164
|
var BootstrapRegistration = class {
|
|
17855
18165
|
static registerConfiguration(builder, context) {
|
|
@@ -18334,28 +18644,28 @@ var IActivityRegistryToken = new Token(
|
|
|
18334
18644
|
);
|
|
18335
18645
|
|
|
18336
18646
|
// server/routes/os.ts
|
|
18337
|
-
var
|
|
18338
|
-
var
|
|
18647
|
+
var import_path9 = __toESM(require("path"), 1);
|
|
18648
|
+
var import_fs9 = __toESM(require("fs"), 1);
|
|
18339
18649
|
var import_child_process = require("child_process");
|
|
18340
18650
|
var import_crypto7 = __toESM(require("crypto"), 1);
|
|
18341
18651
|
var import_xss = require("xss");
|
|
18342
18652
|
|
|
18343
18653
|
// server/utils/crypto.ts
|
|
18344
18654
|
var import_crypto5 = __toESM(require("crypto"), 1);
|
|
18345
|
-
var
|
|
18346
|
-
var
|
|
18655
|
+
var import_fs8 = __toESM(require("fs"), 1);
|
|
18656
|
+
var import_path7 = __toESM(require("path"), 1);
|
|
18347
18657
|
var _encryptionKey = null;
|
|
18348
18658
|
function getEncryptionKey() {
|
|
18349
18659
|
if (_encryptionKey) return _encryptionKey;
|
|
18350
|
-
const envPath =
|
|
18660
|
+
const envPath = import_path7.default.resolve(process.cwd(), ".env");
|
|
18351
18661
|
const keyHex = process.env.ENCRYPTION_KEY;
|
|
18352
18662
|
if (keyHex && keyHex.trim() !== "") {
|
|
18353
18663
|
_encryptionKey = Buffer.from(keyHex.trim(), "hex");
|
|
18354
18664
|
return _encryptionKey;
|
|
18355
18665
|
}
|
|
18356
|
-
if (
|
|
18666
|
+
if (import_fs8.default.existsSync(envPath)) {
|
|
18357
18667
|
try {
|
|
18358
|
-
const content =
|
|
18668
|
+
const content = import_fs8.default.readFileSync(envPath, "utf-8");
|
|
18359
18669
|
const match = content.match(/^ENCRYPTION_KEY=(.+)$/m);
|
|
18360
18670
|
if (match && match[1].trim() !== "") {
|
|
18361
18671
|
const fileKeyHex = match[1].trim();
|
|
@@ -18369,8 +18679,8 @@ function getEncryptionKey() {
|
|
|
18369
18679
|
}
|
|
18370
18680
|
const newKey = import_crypto5.default.randomBytes(32).toString("hex");
|
|
18371
18681
|
try {
|
|
18372
|
-
if (
|
|
18373
|
-
const existingContent =
|
|
18682
|
+
if (import_fs8.default.existsSync(envPath)) {
|
|
18683
|
+
const existingContent = import_fs8.default.readFileSync(envPath, "utf-8");
|
|
18374
18684
|
if (/^ENCRYPTION_KEY=/m.test(existingContent)) {
|
|
18375
18685
|
console.warn(
|
|
18376
18686
|
"[Crypto] ENCRYPTION_KEY missing/empty in .env; NOT overwriting existing entry. Generating ephemeral in-memory key \u2014 please set ENCRYPTION_KEY manually before relying on encrypted storage."
|
|
@@ -18379,12 +18689,12 @@ function getEncryptionKey() {
|
|
|
18379
18689
|
_encryptionKey = Buffer.from(newKey, "hex");
|
|
18380
18690
|
return _encryptionKey;
|
|
18381
18691
|
}
|
|
18382
|
-
|
|
18692
|
+
import_fs8.default.appendFileSync(envPath, `
|
|
18383
18693
|
ENCRYPTION_KEY=${newKey}
|
|
18384
18694
|
`);
|
|
18385
18695
|
console.log("[Crypto] ENCRYPTION_KEY auto-generated and persisted to .env");
|
|
18386
18696
|
} else {
|
|
18387
|
-
|
|
18697
|
+
import_fs8.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
|
|
18388
18698
|
`);
|
|
18389
18699
|
console.log("[Crypto] .env created with auto-generated ENCRYPTION_KEY");
|
|
18390
18700
|
}
|
|
@@ -18437,142 +18747,58 @@ function detectPromptInjection(input) {
|
|
|
18437
18747
|
return PROMPT_INJECTION_PATTERNS.some((p) => p.test(input));
|
|
18438
18748
|
}
|
|
18439
18749
|
|
|
18440
|
-
// server/
|
|
18441
|
-
|
|
18442
|
-
|
|
18750
|
+
// server/routes/shared.ts
|
|
18751
|
+
var import_path8 = __toESM(require("path"), 1);
|
|
18752
|
+
var import_crypto6 = __toESM(require("crypto"), 1);
|
|
18753
|
+
|
|
18754
|
+
// server/utils/bridge-sdk.ts
|
|
18755
|
+
var BRIDGE_SDK_CODE = `(function() {
|
|
18756
|
+
// Mock document.cookie to prevent SecurityError in sandboxed iframes lacking 'allow-same-origin'
|
|
18443
18757
|
try {
|
|
18444
|
-
|
|
18445
|
-
|
|
18446
|
-
|
|
18447
|
-
|
|
18448
|
-
|
|
18449
|
-
|
|
18450
|
-
|
|
18451
|
-
|
|
18452
|
-
|
|
18758
|
+
Object.defineProperty(document, 'cookie', {
|
|
18759
|
+
get: function() { return ""; },
|
|
18760
|
+
set: function(val) {},
|
|
18761
|
+
configurable: true
|
|
18762
|
+
});
|
|
18763
|
+
} catch (e) {
|
|
18764
|
+
try {
|
|
18765
|
+
Object.defineProperty(Document.prototype, 'cookie', {
|
|
18766
|
+
get: function() { return ""; },
|
|
18767
|
+
set: function(val) {},
|
|
18768
|
+
configurable: true
|
|
18769
|
+
});
|
|
18770
|
+
} catch (err) {}
|
|
18453
18771
|
}
|
|
18454
|
-
|
|
18455
|
-
|
|
18456
|
-
|
|
18457
|
-
return
|
|
18458
|
-
|
|
18459
|
-
|
|
18460
|
-
|
|
18461
|
-
|
|
18462
|
-
|
|
18463
|
-
|
|
18464
|
-
|
|
18465
|
-
|
|
18466
|
-
|
|
18467
|
-
|
|
18468
|
-
|
|
18469
|
-
|
|
18470
|
-
|
|
18471
|
-
|
|
18472
|
-
|
|
18473
|
-
}
|
|
18474
|
-
|
|
18475
|
-
|
|
18476
|
-
|
|
18477
|
-
|
|
18478
|
-
|
|
18479
|
-
|
|
18480
|
-
|
|
18481
|
-
|
|
18482
|
-
|
|
18483
|
-
}
|
|
18484
|
-
if (role) {
|
|
18485
|
-
return `user:${session.userId || "demo"}:${role}`;
|
|
18486
|
-
}
|
|
18487
|
-
return "anonymous";
|
|
18488
|
-
} catch {
|
|
18489
|
-
return "anonymous";
|
|
18490
|
-
}
|
|
18491
|
-
}
|
|
18492
|
-
function requireAuth(...roles) {
|
|
18493
|
-
return (req, res, next) => {
|
|
18494
|
-
const token = getCookieToken(req);
|
|
18495
|
-
if (!token) return res.status(401).json({ success: false, error: "Authentication required" });
|
|
18496
|
-
const session = getValidSession(token);
|
|
18497
|
-
if (!session) return res.status(401).json({ success: false, error: "Session expired or invalid" });
|
|
18498
|
-
if (roles.length > 0) {
|
|
18499
|
-
let userRole = session.role;
|
|
18500
|
-
if (session.username === "admin" || session.userId === "usr_admin" || userRole === "admin") {
|
|
18501
|
-
userRole = "administrator";
|
|
18502
|
-
}
|
|
18503
|
-
const effectiveRoles = roles.map((r) => r === "admin" ? "administrator" : r);
|
|
18504
|
-
if (!effectiveRoles.includes(userRole)) {
|
|
18505
|
-
return res.status(403).json({ success: false, error: `Role ${session.role} not allowed. Required: ${roles.join(", ")}` });
|
|
18506
|
-
}
|
|
18507
|
-
}
|
|
18508
|
-
req.session = session;
|
|
18509
|
-
next();
|
|
18510
|
-
};
|
|
18511
|
-
}
|
|
18512
|
-
function checkIsTeacherOrAdmin(req) {
|
|
18513
|
-
const token = getCookieToken(req);
|
|
18514
|
-
if (!token) return false;
|
|
18515
|
-
try {
|
|
18516
|
-
const session = getValidSession(token);
|
|
18517
|
-
if (!session) return false;
|
|
18518
|
-
return session.role === "teacher" || session.role === "administrator";
|
|
18519
|
-
} catch {
|
|
18520
|
-
return false;
|
|
18521
|
-
}
|
|
18522
|
-
}
|
|
18523
|
-
|
|
18524
|
-
// server/routes/shared.ts
|
|
18525
|
-
var import_path7 = __toESM(require("path"), 1);
|
|
18526
|
-
var import_crypto6 = __toESM(require("crypto"), 1);
|
|
18527
|
-
|
|
18528
|
-
// server/utils/bridge-sdk.ts
|
|
18529
|
-
var BRIDGE_SDK_CODE = `(function() {
|
|
18530
|
-
// Mock document.cookie to prevent SecurityError in sandboxed iframes lacking 'allow-same-origin'
|
|
18531
|
-
try {
|
|
18532
|
-
Object.defineProperty(document, 'cookie', {
|
|
18533
|
-
get: function() { return ""; },
|
|
18534
|
-
set: function(val) {},
|
|
18535
|
-
configurable: true
|
|
18536
|
-
});
|
|
18537
|
-
} catch (e) {
|
|
18538
|
-
try {
|
|
18539
|
-
Object.defineProperty(Document.prototype, 'cookie', {
|
|
18540
|
-
get: function() { return ""; },
|
|
18541
|
-
set: function(val) {},
|
|
18542
|
-
configurable: true
|
|
18543
|
-
});
|
|
18544
|
-
} catch (err) {}
|
|
18545
|
-
}
|
|
18546
|
-
|
|
18547
|
-
// \u2500\u2500 Helper: create a safe postMessage wrapper that normalises 'null' \u2192 '*' \u2500\u2500
|
|
18548
|
-
function __makeSafePostMessage(realTarget, label) {
|
|
18549
|
-
return function(message, targetOrigin, transfer) {
|
|
18550
|
-
try {
|
|
18551
|
-
if (message && typeof message === 'object') {
|
|
18552
|
-
if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
|
|
18553
|
-
message.attempt_id = window.__LMS_STUDENT__.attempt_id;
|
|
18554
|
-
}
|
|
18555
|
-
if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
|
|
18556
|
-
message.uuid = window.__LMS_COURSEWARE__.uuid;
|
|
18557
|
-
}
|
|
18558
|
-
}
|
|
18559
|
-
} catch (e) {}
|
|
18560
|
-
|
|
18561
|
-
var origin = targetOrigin;
|
|
18562
|
-
if (origin === 'null' || origin === null || origin === undefined) {
|
|
18563
|
-
console.warn('[LMS Bridge Notice] Normalized invalid targetOrigin "' + origin + '" to "*" for ' + label);
|
|
18564
|
-
origin = '*';
|
|
18565
|
-
}
|
|
18566
|
-
try {
|
|
18567
|
-
return realTarget.postMessage.call(realTarget, message, origin, transfer);
|
|
18568
|
-
} catch (err) {
|
|
18569
|
-
if (err.name === 'SyntaxError' && origin !== '*') {
|
|
18570
|
-
console.warn('[LMS Bridge Notice] Recovered SyntaxError on ' + label + ', fallback to "*"', err);
|
|
18571
|
-
return realTarget.postMessage.call(realTarget, message, '*', transfer);
|
|
18572
|
-
}
|
|
18573
|
-
throw err;
|
|
18574
|
-
}
|
|
18575
|
-
};
|
|
18772
|
+
|
|
18773
|
+
// \u2500\u2500 Helper: create a safe postMessage wrapper that normalises 'null' \u2192 '*' \u2500\u2500
|
|
18774
|
+
function __makeSafePostMessage(realTarget, label) {
|
|
18775
|
+
return function(message, targetOrigin, transfer) {
|
|
18776
|
+
try {
|
|
18777
|
+
if (message && typeof message === 'object') {
|
|
18778
|
+
if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
|
|
18779
|
+
message.attempt_id = window.__LMS_STUDENT__.attempt_id;
|
|
18780
|
+
}
|
|
18781
|
+
if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
|
|
18782
|
+
message.uuid = window.__LMS_COURSEWARE__.uuid;
|
|
18783
|
+
}
|
|
18784
|
+
}
|
|
18785
|
+
} catch (e) {}
|
|
18786
|
+
|
|
18787
|
+
var origin = targetOrigin;
|
|
18788
|
+
if (origin === 'null' || origin === null || origin === undefined) {
|
|
18789
|
+
console.warn('[LMS Bridge Notice] Normalized invalid targetOrigin "' + origin + '" to "*" for ' + label);
|
|
18790
|
+
origin = '*';
|
|
18791
|
+
}
|
|
18792
|
+
try {
|
|
18793
|
+
return realTarget.postMessage.call(realTarget, message, origin, transfer);
|
|
18794
|
+
} catch (err) {
|
|
18795
|
+
if (err.name === 'SyntaxError' && origin !== '*') {
|
|
18796
|
+
console.warn('[LMS Bridge Notice] Recovered SyntaxError on ' + label + ', fallback to "*"', err);
|
|
18797
|
+
return realTarget.postMessage.call(realTarget, message, '*', transfer);
|
|
18798
|
+
}
|
|
18799
|
+
throw err;
|
|
18800
|
+
}
|
|
18801
|
+
};
|
|
18576
18802
|
}
|
|
18577
18803
|
|
|
18578
18804
|
// \u2500\u2500 Helper: create a Proxy wrapper around a cross-origin WindowProxy \u2500\u2500
|
|
@@ -18683,17 +18909,51 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
18683
18909
|
}
|
|
18684
18910
|
} catch (e) {}
|
|
18685
18911
|
|
|
18686
|
-
// \u2500\u2500 Bidirectional host\u2192courseware command bus \u2500\u2500
|
|
18912
|
+
// \u2500\u2500 Bidirectional host\u2192courseware command bus & Theme Bridge \u2500\u2500
|
|
18687
18913
|
var __lmsHandlers = {};
|
|
18688
18914
|
var __lmsPendingRequests = {};
|
|
18915
|
+
|
|
18916
|
+
function __applyThemeTokens(themeData) {
|
|
18917
|
+
if (!themeData || typeof themeData !== 'object') return;
|
|
18918
|
+
try {
|
|
18919
|
+
window.__LMS_THEME__ = themeData;
|
|
18920
|
+
var docEl = document.documentElement;
|
|
18921
|
+
if (docEl) {
|
|
18922
|
+
if (themeData.theme) {
|
|
18923
|
+
docEl.setAttribute('data-theme', themeData.theme);
|
|
18924
|
+
}
|
|
18925
|
+
if (themeData.tokens && typeof themeData.tokens === 'object') {
|
|
18926
|
+
for (var k in themeData.tokens) {
|
|
18927
|
+
if (Object.prototype.hasOwnProperty.call(themeData.tokens, k)) {
|
|
18928
|
+
docEl.style.setProperty(k, themeData.tokens[k]);
|
|
18929
|
+
}
|
|
18930
|
+
}
|
|
18931
|
+
}
|
|
18932
|
+
}
|
|
18933
|
+
} catch (e) {}
|
|
18934
|
+
}
|
|
18935
|
+
|
|
18936
|
+
if (window.__LMS_THEME__) {
|
|
18937
|
+
__applyThemeTokens(window.__LMS_THEME__);
|
|
18938
|
+
}
|
|
18939
|
+
|
|
18689
18940
|
window.addEventListener('message', function(event) {
|
|
18690
18941
|
var d = event.data;
|
|
18691
18942
|
if (!d || typeof d !== 'object') return;
|
|
18692
18943
|
if (d.type === 'LMS_HOST_COMMAND' && d.event) {
|
|
18944
|
+
if (d.event === 'theme:changed' && d.payload) {
|
|
18945
|
+
__applyThemeTokens(d.payload);
|
|
18946
|
+
}
|
|
18693
18947
|
var cbs = __lmsHandlers[d.event] || [];
|
|
18694
18948
|
for (var i = 0; i < cbs.length; i++) {
|
|
18695
18949
|
try { cbs[i](d.payload); } catch (e) {}
|
|
18696
18950
|
}
|
|
18951
|
+
} else if (d.type === 'LMS_THEME_CHANGED' && (d.theme || d.tokens)) {
|
|
18952
|
+
__applyThemeTokens(d);
|
|
18953
|
+
var themeCbs = __lmsHandlers['theme:changed'] || [];
|
|
18954
|
+
for (var j = 0; j < themeCbs.length; j++) {
|
|
18955
|
+
try { themeCbs[j](d); } catch (e) {}
|
|
18956
|
+
}
|
|
18697
18957
|
} else if (d.type === 'LMS_PROGRESS_RESPONSE' && d.requestId) {
|
|
18698
18958
|
var pending = __lmsPendingRequests[d.requestId];
|
|
18699
18959
|
if (pending) {
|
|
@@ -18735,6 +18995,9 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
18735
18995
|
getCourseware() {
|
|
18736
18996
|
return window.__LMS_COURSEWARE__;
|
|
18737
18997
|
},
|
|
18998
|
+
getTheme() {
|
|
18999
|
+
return window.__LMS_THEME__ || null;
|
|
19000
|
+
},
|
|
18738
19001
|
log(event, data) {
|
|
18739
19002
|
window.parent.postMessage({
|
|
18740
19003
|
type: "LMS_LOG",
|
|
@@ -19147,7 +19410,7 @@ function validateMagicBytes(buffer, fileName) {
|
|
|
19147
19410
|
".webp": [[82, 73, 70, 70]]
|
|
19148
19411
|
// RIFF
|
|
19149
19412
|
};
|
|
19150
|
-
const ext =
|
|
19413
|
+
const ext = import_path8.default.extname(fileName || "").toLowerCase();
|
|
19151
19414
|
const signatures = MAGIC_BYTES[ext];
|
|
19152
19415
|
if (!signatures) return true;
|
|
19153
19416
|
return signatures.some(
|
|
@@ -19250,20 +19513,21 @@ var generateStudentNumber = (db2) => {
|
|
|
19250
19513
|
return `S${nextSeq.toString().padStart(3, "0")}`;
|
|
19251
19514
|
};
|
|
19252
19515
|
|
|
19516
|
+
// server/utils/error-handler.ts
|
|
19517
|
+
function sendSafeError(res, err, status = 500, fallbackMessage = "Internal server error") {
|
|
19518
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
19519
|
+
if (process.env.NODE_ENV !== "test") {
|
|
19520
|
+
console.error(`[API Error ${status}]:`, err);
|
|
19521
|
+
}
|
|
19522
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
19523
|
+
const exposedMessage = isProd ? fallbackMessage : message;
|
|
19524
|
+
return res.status(status).json({ success: false, error: exposedMessage });
|
|
19525
|
+
}
|
|
19526
|
+
|
|
19253
19527
|
// server/routes/os.ts
|
|
19254
19528
|
function registerOsRoutes(ctx) {
|
|
19255
19529
|
const {
|
|
19256
19530
|
app,
|
|
19257
|
-
io,
|
|
19258
|
-
loginLimiter,
|
|
19259
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
19260
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
19261
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
19262
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
19263
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
19264
|
-
buildOpenAITools: buildOpenAITools2,
|
|
19265
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
19266
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
19267
19531
|
runGeminiAgentChat: runGeminiAgentChat2,
|
|
19268
19532
|
runOpenAIAgentChat: runOpenAIAgentChat2,
|
|
19269
19533
|
activityRegistry
|
|
@@ -19274,7 +19538,7 @@ function registerOsRoutes(ctx) {
|
|
|
19274
19538
|
if (!filename || !base64Data) {
|
|
19275
19539
|
return res.status(400).json({ error: "Filename and base64Data are required" });
|
|
19276
19540
|
}
|
|
19277
|
-
const ext =
|
|
19541
|
+
const ext = import_path9.default.extname(filename).toLowerCase();
|
|
19278
19542
|
if (BLOCKED_EXTENSIONS.includes(ext)) {
|
|
19279
19543
|
return res.status(400).json({ error: `File type ${ext} is not allowed for security reasons.` });
|
|
19280
19544
|
}
|
|
@@ -19286,13 +19550,13 @@ function registerOsRoutes(ctx) {
|
|
|
19286
19550
|
if (!validateMagicBytes(fileBuffer, filename)) {
|
|
19287
19551
|
return res.status(400).json({ error: "File content does not match the declared file type." });
|
|
19288
19552
|
}
|
|
19289
|
-
const uploadsDir =
|
|
19290
|
-
if (!
|
|
19291
|
-
|
|
19553
|
+
const uploadsDir = import_path9.default.join(process.cwd(), "uploads");
|
|
19554
|
+
if (!import_fs9.default.existsSync(uploadsDir)) {
|
|
19555
|
+
import_fs9.default.mkdirSync(uploadsDir, { recursive: true });
|
|
19292
19556
|
}
|
|
19293
19557
|
const uniqueName = `${Date.now()}-${import_crypto7.default.randomBytes(4).toString("hex")}${ext}`;
|
|
19294
|
-
const filePath =
|
|
19295
|
-
|
|
19558
|
+
const filePath = import_path9.default.join(uploadsDir, uniqueName);
|
|
19559
|
+
import_fs9.default.writeFileSync(filePath, fileBuffer);
|
|
19296
19560
|
let slideCount = 1;
|
|
19297
19561
|
if (ext === ".pdf") {
|
|
19298
19562
|
try {
|
|
@@ -19326,7 +19590,7 @@ function registerOsRoutes(ctx) {
|
|
|
19326
19590
|
});
|
|
19327
19591
|
} catch (e) {
|
|
19328
19592
|
console.error("Upload error:", e);
|
|
19329
|
-
res
|
|
19593
|
+
sendSafeError(res, e);
|
|
19330
19594
|
}
|
|
19331
19595
|
});
|
|
19332
19596
|
app.post("/api/commands", requireAuth(), async (req, res) => {
|
|
@@ -19340,7 +19604,7 @@ function registerOsRoutes(ctx) {
|
|
|
19340
19604
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
19341
19605
|
res.json({ success: true, result });
|
|
19342
19606
|
} catch (err) {
|
|
19343
|
-
res
|
|
19607
|
+
sendSafeError(res, err);
|
|
19344
19608
|
}
|
|
19345
19609
|
});
|
|
19346
19610
|
app.get("/api/commands/registered", (req, res) => {
|
|
@@ -19348,7 +19612,7 @@ function registerOsRoutes(ctx) {
|
|
|
19348
19612
|
const actions = kernelContainer.actionRegistry.getAllActions();
|
|
19349
19613
|
res.json(actions);
|
|
19350
19614
|
} catch (err) {
|
|
19351
|
-
res
|
|
19615
|
+
sendSafeError(res, err);
|
|
19352
19616
|
}
|
|
19353
19617
|
});
|
|
19354
19618
|
app.get("/api/activities", async (req, res) => {
|
|
@@ -19357,7 +19621,7 @@ function registerOsRoutes(ctx) {
|
|
|
19357
19621
|
const list = role === "all" ? activityRegistry.listDescriptors() : activityRegistry.listByRole(role).map((p) => p.descriptor);
|
|
19358
19622
|
res.json({ activities: list });
|
|
19359
19623
|
} catch (err) {
|
|
19360
|
-
res
|
|
19624
|
+
sendSafeError(res, err);
|
|
19361
19625
|
}
|
|
19362
19626
|
});
|
|
19363
19627
|
app.post("/api/activities/:id/start", async (req, res) => {
|
|
@@ -19379,7 +19643,7 @@ function registerOsRoutes(ctx) {
|
|
|
19379
19643
|
res.json({ ok: true, ...result });
|
|
19380
19644
|
} catch (err) {
|
|
19381
19645
|
const status = err?.code === "PERMISSION_DENIED" ? 403 : 500;
|
|
19382
|
-
res
|
|
19646
|
+
sendSafeError(res, err, status);
|
|
19383
19647
|
}
|
|
19384
19648
|
});
|
|
19385
19649
|
app.get("/api/activities/running", (_req, res) => {
|
|
@@ -19395,7 +19659,7 @@ function registerOsRoutes(ctx) {
|
|
|
19395
19659
|
}));
|
|
19396
19660
|
res.json({ activities });
|
|
19397
19661
|
} catch (err) {
|
|
19398
|
-
res
|
|
19662
|
+
sendSafeError(res, err);
|
|
19399
19663
|
}
|
|
19400
19664
|
});
|
|
19401
19665
|
app.post("/api/activities/:id/finish", async (req, res) => {
|
|
@@ -19414,7 +19678,7 @@ function registerOsRoutes(ctx) {
|
|
|
19414
19678
|
await provider.finish(context);
|
|
19415
19679
|
res.json({ ok: true });
|
|
19416
19680
|
} catch (err) {
|
|
19417
|
-
res
|
|
19681
|
+
sendSafeError(res, err);
|
|
19418
19682
|
}
|
|
19419
19683
|
});
|
|
19420
19684
|
app.post("/api/activities/:id/pause", async (req, res) => {
|
|
@@ -19433,7 +19697,7 @@ function registerOsRoutes(ctx) {
|
|
|
19433
19697
|
await provider.pause(context);
|
|
19434
19698
|
res.json({ ok: true });
|
|
19435
19699
|
} catch (err) {
|
|
19436
|
-
res
|
|
19700
|
+
sendSafeError(res, err);
|
|
19437
19701
|
}
|
|
19438
19702
|
});
|
|
19439
19703
|
app.post("/api/activities/:id/resume", async (req, res) => {
|
|
@@ -19452,7 +19716,7 @@ function registerOsRoutes(ctx) {
|
|
|
19452
19716
|
await provider.resume(context);
|
|
19453
19717
|
res.json({ ok: true });
|
|
19454
19718
|
} catch (err) {
|
|
19455
|
-
res
|
|
19719
|
+
sendSafeError(res, err);
|
|
19456
19720
|
}
|
|
19457
19721
|
});
|
|
19458
19722
|
app.post("/api/agent/chat", async (req, res) => {
|
|
@@ -19510,7 +19774,7 @@ function registerOsRoutes(ctx) {
|
|
|
19510
19774
|
});
|
|
19511
19775
|
} catch (err) {
|
|
19512
19776
|
console.error(err);
|
|
19513
|
-
res
|
|
19777
|
+
sendSafeError(res, err);
|
|
19514
19778
|
}
|
|
19515
19779
|
});
|
|
19516
19780
|
app.get("/api/agent/conversations", (req, res) => {
|
|
@@ -19530,7 +19794,7 @@ function registerOsRoutes(ctx) {
|
|
|
19530
19794
|
messages: rows.map((r) => ({ role: r.role, content: r.content, createdAt: r.created_at }))
|
|
19531
19795
|
});
|
|
19532
19796
|
} catch (e) {
|
|
19533
|
-
res
|
|
19797
|
+
sendSafeError(res, e);
|
|
19534
19798
|
}
|
|
19535
19799
|
});
|
|
19536
19800
|
app.delete("/api/agent/conversations", (req, res) => {
|
|
@@ -19546,34 +19810,20 @@ function registerOsRoutes(ctx) {
|
|
|
19546
19810
|
kernelContainer.db.prepare("DELETE FROM agent_conversations WHERE conv_key = ?").run(convKey);
|
|
19547
19811
|
res.json({ success: true });
|
|
19548
19812
|
} catch (e) {
|
|
19549
|
-
res
|
|
19813
|
+
sendSafeError(res, e);
|
|
19550
19814
|
}
|
|
19551
19815
|
});
|
|
19552
19816
|
}
|
|
19553
19817
|
|
|
19554
19818
|
// server/routes/resources.ts
|
|
19555
19819
|
function registerResourcesRoutes(ctx) {
|
|
19556
|
-
const {
|
|
19557
|
-
app,
|
|
19558
|
-
io,
|
|
19559
|
-
loginLimiter,
|
|
19560
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
19561
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
19562
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
19563
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
19564
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
19565
|
-
buildOpenAITools: buildOpenAITools2,
|
|
19566
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
19567
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
19568
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
19569
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
19570
|
-
} = ctx;
|
|
19820
|
+
const { app } = ctx;
|
|
19571
19821
|
app.get("/api/resources", (req, res) => {
|
|
19572
19822
|
try {
|
|
19573
19823
|
const resources = kernelContainer.db.prepare("SELECT id, name, type, created_at FROM system_resources ORDER BY created_at DESC").all();
|
|
19574
19824
|
res.json(resources);
|
|
19575
19825
|
} catch (e) {
|
|
19576
|
-
res
|
|
19826
|
+
sendSafeError(res, e);
|
|
19577
19827
|
}
|
|
19578
19828
|
});
|
|
19579
19829
|
app.get("/api/resources/:id", (req, res) => {
|
|
@@ -19588,6 +19838,7 @@ function registerResourcesRoutes(ctx) {
|
|
|
19588
19838
|
).run(resource.id, resource.id, resource.name, "html", "index.html", resource.created_at || Date.now());
|
|
19589
19839
|
}
|
|
19590
19840
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
19841
|
+
res.setHeader("Content-Security-Policy", "sandbox allow-scripts allow-forms allow-downloads; default-src 'self' 'unsafe-inline' data: blob: https:;");
|
|
19591
19842
|
let html2 = resource.content || "";
|
|
19592
19843
|
const baseTag2 = `<base href="/api/resources/${req.params.id}/">`;
|
|
19593
19844
|
if (html2.toLowerCase().includes("<head>")) {
|
|
@@ -19632,7 +19883,7 @@ function registerResourcesRoutes(ctx) {
|
|
|
19632
19883
|
html = injectLmsSdk(html, req, { id: resource.id, name: resource.name, uuid: resource.id });
|
|
19633
19884
|
return res.send(html);
|
|
19634
19885
|
} catch (e) {
|
|
19635
|
-
res
|
|
19886
|
+
sendSafeError(res, e);
|
|
19636
19887
|
}
|
|
19637
19888
|
});
|
|
19638
19889
|
app.get("/api/resources/:id/*", (req, res) => {
|
|
@@ -19718,15 +19969,16 @@ function registerResourcesRoutes(ctx) {
|
|
|
19718
19969
|
} else {
|
|
19719
19970
|
let content = fileObj.content;
|
|
19720
19971
|
if (contentType.startsWith("text/html")) {
|
|
19972
|
+
res.setHeader("Content-Security-Policy", "sandbox allow-scripts allow-forms allow-downloads; default-src 'self' 'unsafe-inline' data: blob: https:;");
|
|
19721
19973
|
content = injectLmsSdk(content, req, { id: resource.id, name: resource.name, uuid: resource.id });
|
|
19722
19974
|
}
|
|
19723
19975
|
return res.send(content);
|
|
19724
19976
|
}
|
|
19725
19977
|
} catch (e) {
|
|
19726
|
-
res
|
|
19978
|
+
sendSafeError(res, e);
|
|
19727
19979
|
}
|
|
19728
19980
|
});
|
|
19729
|
-
app.post("/api/resources", async (req, res) => {
|
|
19981
|
+
app.post("/api/resources", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
19730
19982
|
try {
|
|
19731
19983
|
const { name, type, content } = req.body;
|
|
19732
19984
|
if (!name || !type) {
|
|
@@ -19783,15 +20035,15 @@ function registerResourcesRoutes(ctx) {
|
|
|
19783
20035
|
}
|
|
19784
20036
|
res.json({ success: true, id, name, type });
|
|
19785
20037
|
} catch (e) {
|
|
19786
|
-
res
|
|
20038
|
+
sendSafeError(res, e);
|
|
19787
20039
|
}
|
|
19788
20040
|
});
|
|
19789
|
-
app.delete("/api/resources/:id", (req, res) => {
|
|
20041
|
+
app.delete("/api/resources/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
19790
20042
|
try {
|
|
19791
20043
|
kernelContainer.db.prepare("DELETE FROM system_resources WHERE id = ?").run(req.params.id);
|
|
19792
20044
|
res.json({ success: true });
|
|
19793
20045
|
} catch (e) {
|
|
19794
|
-
res
|
|
20046
|
+
sendSafeError(res, e);
|
|
19795
20047
|
}
|
|
19796
20048
|
});
|
|
19797
20049
|
const RESOURCE_HANDLERS = {
|
|
@@ -19841,61 +20093,51 @@ function registerResourcesRoutes(ctx) {
|
|
|
19841
20093
|
}
|
|
19842
20094
|
|
|
19843
20095
|
// server/routes/courseware.ts
|
|
19844
|
-
var
|
|
19845
|
-
var
|
|
20096
|
+
var import_path10 = __toESM(require("path"), 1);
|
|
20097
|
+
var import_fs10 = __toESM(require("fs"), 1);
|
|
19846
20098
|
var import_crypto9 = __toESM(require("crypto"), 1);
|
|
19847
20099
|
function registerCoursewareRoutes(ctx) {
|
|
19848
|
-
const {
|
|
19849
|
-
|
|
19850
|
-
io,
|
|
19851
|
-
loginLimiter,
|
|
19852
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
19853
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
19854
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
19855
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
19856
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
19857
|
-
buildOpenAITools: buildOpenAITools2,
|
|
19858
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
19859
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
19860
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
19861
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
19862
|
-
} = ctx;
|
|
19863
|
-
app.post("/api/courseware/upload", async (req, res) => {
|
|
20100
|
+
const { app, io } = ctx;
|
|
20101
|
+
app.post("/api/courseware/upload", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
19864
20102
|
try {
|
|
19865
20103
|
const { name, filename, base64Data } = req.body;
|
|
19866
|
-
const
|
|
20104
|
+
const actorId = getActorId(req) || "teacher";
|
|
20105
|
+
const cmd = kernelContainer.commandBus.createCommand("courseware.upload", { name, filename, base64Data }, actorId);
|
|
19867
20106
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
19868
20107
|
res.json(result);
|
|
19869
20108
|
} catch (e) {
|
|
19870
|
-
res
|
|
20109
|
+
sendSafeError(res, e);
|
|
19871
20110
|
}
|
|
19872
20111
|
});
|
|
19873
|
-
app.post("/api/courseware/confirm", async (req, res) => {
|
|
20112
|
+
app.post("/api/courseware/confirm", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
19874
20113
|
try {
|
|
19875
20114
|
const { uuid, name, entry } = req.body;
|
|
19876
|
-
const
|
|
20115
|
+
const actorId = getActorId(req) || "teacher";
|
|
20116
|
+
const cmd = kernelContainer.commandBus.createCommand("courseware.confirm", { uuid, name, entry }, actorId);
|
|
19877
20117
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
19878
20118
|
res.json(result);
|
|
19879
20119
|
} catch (e) {
|
|
19880
|
-
res
|
|
20120
|
+
sendSafeError(res, e);
|
|
19881
20121
|
}
|
|
19882
20122
|
});
|
|
19883
|
-
app.get("/api/courseware", async (req, res) => {
|
|
20123
|
+
app.get("/api/courseware", requireAuth(), async (req, res) => {
|
|
19884
20124
|
try {
|
|
19885
|
-
const
|
|
20125
|
+
const actorId = getActorId(req) || "system";
|
|
20126
|
+
const cmd = kernelContainer.commandBus.createCommand("courseware.list", {}, actorId);
|
|
19886
20127
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
19887
20128
|
res.json(result);
|
|
19888
20129
|
} catch (e) {
|
|
19889
|
-
res
|
|
20130
|
+
sendSafeError(res, e);
|
|
19890
20131
|
}
|
|
19891
20132
|
});
|
|
19892
|
-
app.delete("/api/courseware/:id", async (req, res) => {
|
|
20133
|
+
app.delete("/api/courseware/:id", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
19893
20134
|
try {
|
|
19894
|
-
const
|
|
20135
|
+
const actorId = getActorId(req) || "teacher";
|
|
20136
|
+
const cmd = kernelContainer.commandBus.createCommand("courseware.delete", { id: req.params.id }, actorId);
|
|
19895
20137
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
19896
20138
|
res.json(result);
|
|
19897
20139
|
} catch (e) {
|
|
19898
|
-
res
|
|
20140
|
+
sendSafeError(res, e);
|
|
19899
20141
|
}
|
|
19900
20142
|
});
|
|
19901
20143
|
function extractScoreCommentCompletion(payload) {
|
|
@@ -19988,6 +20230,19 @@ function registerCoursewareRoutes(ctx) {
|
|
|
19988
20230
|
try {
|
|
19989
20231
|
const { attemptId } = req.params;
|
|
19990
20232
|
const { eventType, payload } = req.body;
|
|
20233
|
+
const token = getCookieToken(req);
|
|
20234
|
+
const session = req.session || (token ? getValidSession(token) : null);
|
|
20235
|
+
if (!session) {
|
|
20236
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
20237
|
+
}
|
|
20238
|
+
if (session.role !== "teacher" && session.role !== "administrator") {
|
|
20239
|
+
const attemptRow = kernelContainer.db.prepare(
|
|
20240
|
+
"SELECT student_id FROM courseware_attempt WHERE id = ?"
|
|
20241
|
+
).get(attemptId);
|
|
20242
|
+
if (attemptRow && attemptRow.student_id !== session.userId) {
|
|
20243
|
+
return res.status(403).json({ error: "Forbidden: Cannot modify logs for another student" });
|
|
20244
|
+
}
|
|
20245
|
+
}
|
|
19991
20246
|
const rawId = "raw_" + import_crypto9.default.randomBytes(8).toString("hex");
|
|
19992
20247
|
kernelContainer.db.prepare(
|
|
19993
20248
|
"INSERT INTO submission_raw (id, attempt_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
@@ -20051,13 +20306,26 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20051
20306
|
});
|
|
20052
20307
|
res.json({ success: true });
|
|
20053
20308
|
} catch (e) {
|
|
20054
|
-
res
|
|
20309
|
+
sendSafeError(res, e);
|
|
20055
20310
|
}
|
|
20056
20311
|
});
|
|
20057
20312
|
app.post("/api/courseware/attempts/:attemptId/submit", async (req, res) => {
|
|
20058
20313
|
try {
|
|
20059
20314
|
const { attemptId } = req.params;
|
|
20060
20315
|
let { score, comment, completion, status, extra = {} } = req.body;
|
|
20316
|
+
const token = getCookieToken(req);
|
|
20317
|
+
const session = req.session || (token ? getValidSession(token) : null);
|
|
20318
|
+
if (!session) {
|
|
20319
|
+
return res.status(401).json({ error: "Authentication required to submit attempt scores" });
|
|
20320
|
+
}
|
|
20321
|
+
if (session.role !== "teacher" && session.role !== "administrator") {
|
|
20322
|
+
const attemptRow = kernelContainer.db.prepare(
|
|
20323
|
+
"SELECT student_id FROM courseware_attempt WHERE id = ?"
|
|
20324
|
+
).get(attemptId);
|
|
20325
|
+
if (attemptRow && attemptRow.student_id !== session.userId) {
|
|
20326
|
+
return res.status(403).json({ error: "Forbidden: Cannot submit scores for another student" });
|
|
20327
|
+
}
|
|
20328
|
+
}
|
|
20061
20329
|
const extracted = extractScoreCommentCompletion({ ...req.body, ...extra });
|
|
20062
20330
|
if (score === void 0 || score === null) score = extracted.score;
|
|
20063
20331
|
if (comment === void 0 || comment === null) comment = extracted.comment;
|
|
@@ -20076,6 +20344,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20076
20344
|
parsedCompletion = num;
|
|
20077
20345
|
}
|
|
20078
20346
|
}
|
|
20347
|
+
const actorId = session.userId || getActorId(req) || "student";
|
|
20079
20348
|
const cmd = kernelContainer.commandBus.createCommand("courseware.submit_attempt", {
|
|
20080
20349
|
attemptId,
|
|
20081
20350
|
score: parsedScore,
|
|
@@ -20083,12 +20352,12 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20083
20352
|
completion: parsedCompletion,
|
|
20084
20353
|
status,
|
|
20085
20354
|
extra
|
|
20086
|
-
},
|
|
20355
|
+
}, actorId);
|
|
20087
20356
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20088
20357
|
io.emit("courseware-attempt-updated", { attemptId, type: "submit" });
|
|
20089
20358
|
res.json(result);
|
|
20090
20359
|
} catch (e) {
|
|
20091
|
-
res
|
|
20360
|
+
sendSafeError(res, e);
|
|
20092
20361
|
}
|
|
20093
20362
|
});
|
|
20094
20363
|
app.get("/api/courseware/attempts", (req, res) => {
|
|
@@ -20113,7 +20382,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20113
20382
|
`).all();
|
|
20114
20383
|
res.json(rows);
|
|
20115
20384
|
} catch (e) {
|
|
20116
|
-
res
|
|
20385
|
+
sendSafeError(res, e);
|
|
20117
20386
|
}
|
|
20118
20387
|
});
|
|
20119
20388
|
app.post("/api/courseware/debug", (req, res) => {
|
|
@@ -20121,12 +20390,12 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20121
20390
|
const { msg, url, student, courseware } = req.body;
|
|
20122
20391
|
const logMsg = `[CLIENT DEBUG] ${msg} | URL: ${url} | Student: ${JSON.stringify(student)} | Courseware: ${JSON.stringify(courseware)}`;
|
|
20123
20392
|
console.log(`\x1B[35m[CLIENT DEBUG]\x1B[0m ${msg}`);
|
|
20124
|
-
const logFile2 =
|
|
20125
|
-
|
|
20393
|
+
const logFile2 = import_path10.default.join(process.cwd(), "client_debug.log");
|
|
20394
|
+
import_fs10.default.appendFileSync(logFile2, `${(/* @__PURE__ */ new Date()).toISOString()} - ${logMsg}
|
|
20126
20395
|
`);
|
|
20127
20396
|
res.json({ success: true });
|
|
20128
20397
|
} catch (e) {
|
|
20129
|
-
res
|
|
20398
|
+
sendSafeError(res, e);
|
|
20130
20399
|
}
|
|
20131
20400
|
});
|
|
20132
20401
|
app.get("/api/courseware/attempts/:attemptId/raw", async (req, res) => {
|
|
@@ -20136,7 +20405,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20136
20405
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20137
20406
|
res.json(result);
|
|
20138
20407
|
} catch (e) {
|
|
20139
|
-
res
|
|
20408
|
+
sendSafeError(res, e);
|
|
20140
20409
|
}
|
|
20141
20410
|
});
|
|
20142
20411
|
app.get("/api/courseware/attempts/:attemptId/progress", (req, res) => {
|
|
@@ -20157,7 +20426,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20157
20426
|
progress: { score: result.score, comment: result.comment, completion: result.completion, extra }
|
|
20158
20427
|
});
|
|
20159
20428
|
} catch (e) {
|
|
20160
|
-
res
|
|
20429
|
+
sendSafeError(res, e);
|
|
20161
20430
|
}
|
|
20162
20431
|
});
|
|
20163
20432
|
app.post("/api/courseware/attempts/:attemptId/promote", async (req, res) => {
|
|
@@ -20248,7 +20517,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20248
20517
|
});
|
|
20249
20518
|
res.json({ success: true, assignmentId, score: finalScore });
|
|
20250
20519
|
} catch (e) {
|
|
20251
|
-
res
|
|
20520
|
+
sendSafeError(res, e);
|
|
20252
20521
|
}
|
|
20253
20522
|
});
|
|
20254
20523
|
app.get("/api/courseware/:id", (req, res) => {
|
|
@@ -20265,30 +20534,16 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20265
20534
|
const html = injectLmsSdk(node.content || "", req, { id: node.id, name: node.name, uuid: node.id });
|
|
20266
20535
|
res.send(html);
|
|
20267
20536
|
} catch (e) {
|
|
20268
|
-
res
|
|
20537
|
+
sendSafeError(res, e);
|
|
20269
20538
|
}
|
|
20270
20539
|
});
|
|
20271
20540
|
}
|
|
20272
20541
|
|
|
20273
20542
|
// server/routes/bridge.ts
|
|
20274
|
-
var
|
|
20275
|
-
var
|
|
20543
|
+
var import_path11 = __toESM(require("path"), 1);
|
|
20544
|
+
var import_fs11 = __toESM(require("fs"), 1);
|
|
20276
20545
|
function registerBridgeRoutes(ctx) {
|
|
20277
|
-
const {
|
|
20278
|
-
app,
|
|
20279
|
-
io,
|
|
20280
|
-
loginLimiter,
|
|
20281
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
20282
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
20283
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
20284
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
20285
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
20286
|
-
buildOpenAITools: buildOpenAITools2,
|
|
20287
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
20288
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
20289
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
20290
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
20291
|
-
} = ctx;
|
|
20546
|
+
const { app } = ctx;
|
|
20292
20547
|
app.get("/bridge.js", (_req, res) => {
|
|
20293
20548
|
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
20294
20549
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -20309,23 +20564,23 @@ function registerBridgeRoutes(ctx) {
|
|
|
20309
20564
|
if (!subpath || subpath === "") {
|
|
20310
20565
|
subpath = courseware.entry;
|
|
20311
20566
|
}
|
|
20312
|
-
const storageDir =
|
|
20313
|
-
const filePath =
|
|
20567
|
+
const storageDir = import_path11.default.resolve(process.cwd(), "storage", "courseware", uuid);
|
|
20568
|
+
const filePath = import_path11.default.resolve(storageDir, subpath);
|
|
20314
20569
|
if (!filePath.startsWith(storageDir)) {
|
|
20315
20570
|
return res.status(403).send("Access denied");
|
|
20316
20571
|
}
|
|
20317
|
-
if (!
|
|
20572
|
+
if (!import_fs11.default.existsSync(filePath)) {
|
|
20318
20573
|
return res.status(404).send(`File not found: ${subpath}`);
|
|
20319
20574
|
}
|
|
20320
|
-
const stat =
|
|
20575
|
+
const stat = import_fs11.default.statSync(filePath);
|
|
20321
20576
|
if (stat.isDirectory()) {
|
|
20322
|
-
const indexHtml =
|
|
20323
|
-
if (
|
|
20577
|
+
const indexHtml = import_path11.default.join(filePath, "index.html");
|
|
20578
|
+
if (import_fs11.default.existsSync(indexHtml)) {
|
|
20324
20579
|
return res.redirect(`/runtime/${uuid}/${subpath.endsWith("/") ? subpath : subpath + "/"}index.html`);
|
|
20325
20580
|
}
|
|
20326
20581
|
return res.status(404).send("Directory index not found");
|
|
20327
20582
|
}
|
|
20328
|
-
const ext =
|
|
20583
|
+
const ext = import_path11.default.extname(filePath).toLowerCase();
|
|
20329
20584
|
let contentType = "text/plain; charset=utf-8";
|
|
20330
20585
|
if (ext === ".html" || ext === ".htm") contentType = "text/html; charset=utf-8";
|
|
20331
20586
|
else if (ext === ".css") contentType = "text/css; charset=utf-8";
|
|
@@ -20341,14 +20596,14 @@ function registerBridgeRoutes(ctx) {
|
|
|
20341
20596
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
20342
20597
|
const isHtml = ext === ".html" || ext === ".htm";
|
|
20343
20598
|
if (isHtml) {
|
|
20344
|
-
let html =
|
|
20599
|
+
let html = import_fs11.default.readFileSync(filePath, "utf8");
|
|
20345
20600
|
html = injectLmsSdk(html, req, { id: courseware.id, name: courseware.name, uuid: courseware.uuid });
|
|
20346
20601
|
return res.send(html);
|
|
20347
20602
|
} else {
|
|
20348
20603
|
return res.sendFile(filePath);
|
|
20349
20604
|
}
|
|
20350
20605
|
} catch (e) {
|
|
20351
|
-
res
|
|
20606
|
+
sendSafeError(res, e, 500, "Failed to load courseware runtime file");
|
|
20352
20607
|
}
|
|
20353
20608
|
});
|
|
20354
20609
|
}
|
|
@@ -20356,27 +20611,89 @@ function registerBridgeRoutes(ctx) {
|
|
|
20356
20611
|
// server/routes/lessons.ts
|
|
20357
20612
|
var import_genai3 = require("@google/genai");
|
|
20358
20613
|
var import_uuid16 = require("uuid");
|
|
20614
|
+
function checkLessonOwnership(req, lessonId) {
|
|
20615
|
+
const token = getCookieToken(req);
|
|
20616
|
+
const session = req.session || (token ? getValidSession(token) : null);
|
|
20617
|
+
if (!session) {
|
|
20618
|
+
return { allowed: false, status: 401, error: "Authentication required" };
|
|
20619
|
+
}
|
|
20620
|
+
const isAdmin = session.username === "admin" || session.userId === "usr_admin" || session.role === "admin" || session.role === "administrator";
|
|
20621
|
+
const isTeacherOrAdmin = isAdmin || session.role === "teacher";
|
|
20622
|
+
if (!isTeacherOrAdmin) {
|
|
20623
|
+
return { allowed: false, status: 403, error: "Forbidden: Only teachers or administrators can modify lessons" };
|
|
20624
|
+
}
|
|
20625
|
+
const lesson = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(lessonId);
|
|
20626
|
+
if (!lesson) {
|
|
20627
|
+
return { allowed: false, status: 404, error: "Lesson not found" };
|
|
20628
|
+
}
|
|
20629
|
+
if (isAdmin) {
|
|
20630
|
+
return { allowed: true, status: 200, lesson };
|
|
20631
|
+
}
|
|
20632
|
+
if (!lesson.creator_id || lesson.creator_id === session.userId || lesson.creator_id === session.username) {
|
|
20633
|
+
return { allowed: true, status: 200, lesson };
|
|
20634
|
+
}
|
|
20635
|
+
return {
|
|
20636
|
+
allowed: false,
|
|
20637
|
+
status: 403,
|
|
20638
|
+
error: "Forbidden: You do not have permission to modify this lesson because it was created by another teacher"
|
|
20639
|
+
};
|
|
20640
|
+
}
|
|
20641
|
+
function requireWhiteboardWriteAccess() {
|
|
20642
|
+
return (req, res, next) => {
|
|
20643
|
+
const { id } = req.params;
|
|
20644
|
+
const token = getCookieToken(req);
|
|
20645
|
+
const session = req.session || (token ? getValidSession(token) : null);
|
|
20646
|
+
if (!session) {
|
|
20647
|
+
return res.status(401).json({ success: false, error: "Authentication required" });
|
|
20648
|
+
}
|
|
20649
|
+
req.session = session;
|
|
20650
|
+
const isAdmin = session.username === "admin" || session.userId === "usr_admin" || session.role === "admin" || session.role === "administrator";
|
|
20651
|
+
if (id.startsWith("assignment-")) {
|
|
20652
|
+
if (isAdmin || session.role === "teacher") {
|
|
20653
|
+
return next();
|
|
20654
|
+
}
|
|
20655
|
+
if (session.role === "student") {
|
|
20656
|
+
const studentMatch = id.match(/-student-([^-]+)$/);
|
|
20657
|
+
const targetStudentId = studentMatch ? studentMatch[1] : null;
|
|
20658
|
+
const currentStudentId = session.studentId || session.userId;
|
|
20659
|
+
if (targetStudentId && currentStudentId && targetStudentId === currentStudentId) {
|
|
20660
|
+
return next();
|
|
20661
|
+
}
|
|
20662
|
+
return res.status(403).json({
|
|
20663
|
+
success: false,
|
|
20664
|
+
error: "Forbidden: You do not have permission to modify another student's assignment whiteboard"
|
|
20665
|
+
});
|
|
20666
|
+
}
|
|
20667
|
+
return res.status(403).json({ success: false, error: "Forbidden: Invalid role for whiteboard write" });
|
|
20668
|
+
}
|
|
20669
|
+
const isTeacherOrAdmin = isAdmin || session.role === "teacher";
|
|
20670
|
+
if (!isTeacherOrAdmin) {
|
|
20671
|
+
return res.status(403).json({
|
|
20672
|
+
success: false,
|
|
20673
|
+
error: "Forbidden: Students cannot modify classroom whiteboards"
|
|
20674
|
+
});
|
|
20675
|
+
}
|
|
20676
|
+
const lesson = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(id);
|
|
20677
|
+
if (!lesson) {
|
|
20678
|
+
return res.status(404).json({ success: false, error: "Lesson not found" });
|
|
20679
|
+
}
|
|
20680
|
+
if (isAdmin || !lesson.creator_id || lesson.creator_id === session.userId || lesson.creator_id === session.username) {
|
|
20681
|
+
return next();
|
|
20682
|
+
}
|
|
20683
|
+
return res.status(403).json({
|
|
20684
|
+
success: false,
|
|
20685
|
+
error: "Forbidden: You do not have permission to modify this lesson whiteboard because it was created by another teacher"
|
|
20686
|
+
});
|
|
20687
|
+
};
|
|
20688
|
+
}
|
|
20359
20689
|
function registerLessonsRoutes(ctx) {
|
|
20360
|
-
const {
|
|
20361
|
-
app,
|
|
20362
|
-
io,
|
|
20363
|
-
loginLimiter,
|
|
20364
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
20365
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
20366
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
20367
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
20368
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
20369
|
-
buildOpenAITools: buildOpenAITools2,
|
|
20370
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
20371
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
20372
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
20373
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
20374
|
-
} = ctx;
|
|
20690
|
+
const { app, io } = ctx;
|
|
20375
20691
|
app.get("/api/lessons", (req, res) => {
|
|
20376
20692
|
const lessons = kernelContainer.db.prepare(`
|
|
20377
|
-
SELECT l.*,
|
|
20693
|
+
SELECT l.*, u.name as creator_name,
|
|
20378
20694
|
(SELECT COUNT(*) FROM student_lesson_progress WHERE lesson_id = l.id) as enrollment_count
|
|
20379
20695
|
FROM lessons l
|
|
20696
|
+
LEFT JOIN users u ON l.creator_id = u.id
|
|
20380
20697
|
ORDER BY l.created_at DESC
|
|
20381
20698
|
`).all();
|
|
20382
20699
|
res.json(lessons);
|
|
@@ -20392,7 +20709,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
20392
20709
|
`).all(lessonId);
|
|
20393
20710
|
res.json(rows);
|
|
20394
20711
|
} catch (err) {
|
|
20395
|
-
res
|
|
20712
|
+
sendSafeError(res, err);
|
|
20396
20713
|
}
|
|
20397
20714
|
});
|
|
20398
20715
|
app.get("/api/lessons/:lessonId/eval-grades", (req, res) => {
|
|
@@ -20436,7 +20753,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
20436
20753
|
}
|
|
20437
20754
|
res.json(result);
|
|
20438
20755
|
} catch (err) {
|
|
20439
|
-
res
|
|
20756
|
+
sendSafeError(res, err);
|
|
20440
20757
|
}
|
|
20441
20758
|
});
|
|
20442
20759
|
app.get("/api/eval-submissions/:submissionId/reviews", (req, res) => {
|
|
@@ -20450,7 +20767,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
20450
20767
|
`).all(submissionId);
|
|
20451
20768
|
res.json(rows);
|
|
20452
20769
|
} catch (err) {
|
|
20453
|
-
res
|
|
20770
|
+
sendSafeError(res, err);
|
|
20454
20771
|
}
|
|
20455
20772
|
});
|
|
20456
20773
|
app.get("/api/lessons/:lessonId/students/:studentId/eval-status", (req, res) => {
|
|
@@ -20487,43 +20804,55 @@ function registerLessonsRoutes(ctx) {
|
|
|
20487
20804
|
grade
|
|
20488
20805
|
});
|
|
20489
20806
|
} catch (err) {
|
|
20490
|
-
res
|
|
20807
|
+
sendSafeError(res, err);
|
|
20491
20808
|
}
|
|
20492
20809
|
});
|
|
20493
|
-
app.post("/api/lessons", async (req, res) => {
|
|
20810
|
+
app.post("/api/lessons", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
20494
20811
|
try {
|
|
20495
20812
|
const { title, content } = req.body;
|
|
20813
|
+
const session = req.session;
|
|
20814
|
+
const creatorId = session?.userId || session?.username || "usr_teacher";
|
|
20815
|
+
const actorId = getActorId(req) || "teacher";
|
|
20496
20816
|
const cmd = kernelContainer.commandBus.createCommand(
|
|
20497
20817
|
"lesson.create",
|
|
20498
|
-
{ title, content },
|
|
20499
|
-
|
|
20818
|
+
{ title, content, creatorId },
|
|
20819
|
+
actorId,
|
|
20500
20820
|
{ approved: true }
|
|
20501
20821
|
);
|
|
20502
20822
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20503
20823
|
res.json({ success: true, result });
|
|
20504
20824
|
} catch (e) {
|
|
20505
|
-
res
|
|
20825
|
+
sendSafeError(res, e);
|
|
20506
20826
|
}
|
|
20507
20827
|
});
|
|
20508
|
-
app.put("/api/lessons/:id/timeline", async (req, res) => {
|
|
20828
|
+
app.put("/api/lessons/:id/timeline", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
20509
20829
|
try {
|
|
20510
20830
|
const { id } = req.params;
|
|
20831
|
+
const ownership = checkLessonOwnership(req, id);
|
|
20832
|
+
if (!ownership.allowed) {
|
|
20833
|
+
return res.status(ownership.status).json({ success: false, error: ownership.error });
|
|
20834
|
+
}
|
|
20511
20835
|
const { timeline } = req.body;
|
|
20836
|
+
const actorId = getActorId(req) || "teacher";
|
|
20512
20837
|
const cmd = kernelContainer.commandBus.createCommand(
|
|
20513
20838
|
"lesson.update_timeline",
|
|
20514
20839
|
{ lessonId: id, timeline },
|
|
20515
|
-
|
|
20840
|
+
actorId,
|
|
20516
20841
|
{ approved: true }
|
|
20517
20842
|
);
|
|
20518
20843
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20519
20844
|
res.json({ success: true, result });
|
|
20520
20845
|
} catch (e) {
|
|
20521
|
-
res
|
|
20846
|
+
sendSafeError(res, e);
|
|
20522
20847
|
}
|
|
20523
20848
|
});
|
|
20524
|
-
app.put("/api/lessons/:id/progress-mode", async (req, res) => {
|
|
20849
|
+
app.put("/api/lessons/:id/progress-mode", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
20525
20850
|
try {
|
|
20526
20851
|
const { id } = req.params;
|
|
20852
|
+
const ownership = checkLessonOwnership(req, id);
|
|
20853
|
+
if (!ownership.allowed) {
|
|
20854
|
+
return res.status(ownership.status).json({ success: false, error: ownership.error });
|
|
20855
|
+
}
|
|
20527
20856
|
const { progressMode, progressConditions } = req.body;
|
|
20528
20857
|
const conditionsStr = typeof progressConditions === "string" ? progressConditions : JSON.stringify(progressConditions || null);
|
|
20529
20858
|
kernelContainer.db.prepare("UPDATE lessons SET progress_mode = ?, progress_conditions = ?, updated_at = ? WHERE id = ?").run(progressMode || "manual", conditionsStr, Date.now(), id);
|
|
@@ -20534,7 +20863,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
20534
20863
|
});
|
|
20535
20864
|
res.json({ success: true });
|
|
20536
20865
|
} catch (e) {
|
|
20537
|
-
res
|
|
20866
|
+
sendSafeError(res, e);
|
|
20538
20867
|
}
|
|
20539
20868
|
});
|
|
20540
20869
|
app.get("/api/lessons/:id/whiteboard", (req, res) => {
|
|
@@ -20560,14 +20889,13 @@ function registerLessonsRoutes(ctx) {
|
|
|
20560
20889
|
}
|
|
20561
20890
|
res.json(elements);
|
|
20562
20891
|
});
|
|
20563
|
-
app.post("/api/lessons/:id/whiteboard/reset", async (req, res) => {
|
|
20892
|
+
app.post("/api/lessons/:id/whiteboard/reset", requireWhiteboardWriteAccess(), async (req, res) => {
|
|
20564
20893
|
try {
|
|
20565
20894
|
const id = req.params.id;
|
|
20566
20895
|
if (id.startsWith("assignment-")) {
|
|
20567
20896
|
const deleteStmt = kernelContainer.db.prepare("DELETE FROM whiteboard_elements WHERE lesson_id = ?");
|
|
20568
20897
|
deleteStmt.run(id);
|
|
20569
|
-
res.json({ success: true, message: "Assignment whiteboard reset to empty" });
|
|
20570
|
-
return;
|
|
20898
|
+
return res.json({ success: true, message: "Assignment whiteboard reset to empty" });
|
|
20571
20899
|
}
|
|
20572
20900
|
const snapshotId = `snapshot-${id}`;
|
|
20573
20901
|
const hasSnapshot = kernelContainer.db.prepare("SELECT count(*) as count FROM whiteboard_elements WHERE lesson_id = ?").get(snapshotId);
|
|
@@ -20590,94 +20918,77 @@ function registerLessonsRoutes(ctx) {
|
|
|
20590
20918
|
res.json({ success: true, message: "Lesson whiteboard cleared (no snapshot)" });
|
|
20591
20919
|
}
|
|
20592
20920
|
} catch (e) {
|
|
20593
|
-
res
|
|
20921
|
+
sendSafeError(res, e);
|
|
20594
20922
|
}
|
|
20595
20923
|
});
|
|
20596
|
-
app.post("/api/lessons/:id/whiteboard", async (req, res) => {
|
|
20924
|
+
app.post("/api/lessons/:id/whiteboard", requireWhiteboardWriteAccess(), async (req, res) => {
|
|
20597
20925
|
try {
|
|
20598
20926
|
const { id } = req.params;
|
|
20599
|
-
if (!id.startsWith("assignment-") && !checkIsTeacherOrAdmin(req)) {
|
|
20600
|
-
return res.status(403).json({ error: "Forbidden: Only teachers can draw on classroom whiteboards" });
|
|
20601
|
-
}
|
|
20602
20927
|
const { type, data } = req.body;
|
|
20928
|
+
const actorId = getActorId(req) || "user-frontend";
|
|
20603
20929
|
const cmd = kernelContainer.commandBus.createCommand("whiteboard.draw", {
|
|
20604
20930
|
lessonId: id,
|
|
20605
20931
|
type,
|
|
20606
20932
|
data: JSON.stringify(data)
|
|
20607
|
-
},
|
|
20933
|
+
}, actorId, { approved: true });
|
|
20608
20934
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20609
20935
|
res.json(result);
|
|
20610
20936
|
} catch (e) {
|
|
20611
|
-
res
|
|
20937
|
+
sendSafeError(res, e);
|
|
20612
20938
|
}
|
|
20613
20939
|
});
|
|
20614
|
-
app.put("/api/lessons/:id/whiteboard/:elementId", async (req, res) => {
|
|
20940
|
+
app.put("/api/lessons/:id/whiteboard/:elementId", requireWhiteboardWriteAccess(), async (req, res) => {
|
|
20615
20941
|
try {
|
|
20616
|
-
const { id } = req.params;
|
|
20617
|
-
if (!id.startsWith("assignment-") && !checkIsTeacherOrAdmin(req)) {
|
|
20618
|
-
return res.status(403).json({ error: "Forbidden: Only teachers can update elements on classroom whiteboards" });
|
|
20619
|
-
}
|
|
20942
|
+
const { id, elementId } = req.params;
|
|
20620
20943
|
const { data } = req.body;
|
|
20944
|
+
const actorId = getActorId(req) || "user-frontend";
|
|
20621
20945
|
const cmd = kernelContainer.commandBus.createCommand("whiteboard.update", {
|
|
20622
20946
|
lessonId: id,
|
|
20623
|
-
elementId
|
|
20947
|
+
elementId,
|
|
20624
20948
|
data: JSON.stringify(data)
|
|
20625
|
-
},
|
|
20949
|
+
}, actorId, { approved: true });
|
|
20626
20950
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20627
20951
|
res.json(result);
|
|
20628
20952
|
} catch (e) {
|
|
20629
|
-
res
|
|
20953
|
+
sendSafeError(res, e);
|
|
20630
20954
|
}
|
|
20631
20955
|
});
|
|
20632
|
-
app.delete("/api/lessons/:id/whiteboard", async (req, res) => {
|
|
20956
|
+
app.delete("/api/lessons/:id/whiteboard", requireWhiteboardWriteAccess(), async (req, res) => {
|
|
20633
20957
|
try {
|
|
20634
20958
|
const { id } = req.params;
|
|
20635
|
-
|
|
20636
|
-
return res.status(403).json({ error: "Forbidden: Only teachers can clear the classroom whiteboard" });
|
|
20637
|
-
}
|
|
20959
|
+
const actorId = getActorId(req) || "user-frontend";
|
|
20638
20960
|
const cmd = kernelContainer.commandBus.createCommand("whiteboard.clear", {
|
|
20639
20961
|
lessonId: id
|
|
20640
|
-
},
|
|
20962
|
+
}, actorId, { approved: true });
|
|
20641
20963
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20642
20964
|
res.json(result);
|
|
20643
20965
|
} catch (e) {
|
|
20644
|
-
res
|
|
20966
|
+
sendSafeError(res, e);
|
|
20645
20967
|
}
|
|
20646
20968
|
});
|
|
20647
|
-
app.delete("/api/lessons/:id/whiteboard/:elementId", async (req, res) => {
|
|
20969
|
+
app.delete("/api/lessons/:id/whiteboard/:elementId", requireWhiteboardWriteAccess(), async (req, res) => {
|
|
20648
20970
|
try {
|
|
20649
|
-
const { id } = req.params;
|
|
20650
|
-
|
|
20651
|
-
return res.status(403).json({ error: "Forbidden: Only teachers can delete elements from classroom whiteboards" });
|
|
20652
|
-
}
|
|
20971
|
+
const { id, elementId } = req.params;
|
|
20972
|
+
const actorId = getActorId(req) || "user-frontend";
|
|
20653
20973
|
const cmd = kernelContainer.commandBus.createCommand("whiteboard.delete", {
|
|
20654
20974
|
lessonId: id,
|
|
20655
|
-
elementId
|
|
20656
|
-
},
|
|
20975
|
+
elementId
|
|
20976
|
+
}, actorId, { approved: true });
|
|
20657
20977
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20658
20978
|
res.json(result);
|
|
20659
20979
|
} catch (e) {
|
|
20660
|
-
res
|
|
20980
|
+
sendSafeError(res, e);
|
|
20661
20981
|
}
|
|
20662
20982
|
});
|
|
20663
|
-
app.post("/api/lessons/:id/quiz-submit", async (req, res) => {
|
|
20983
|
+
app.post("/api/lessons/:id/quiz-submit", requireAuth("student", "teacher", "administrator"), async (req, res) => {
|
|
20664
20984
|
try {
|
|
20665
20985
|
const { id: lessonId } = req.params;
|
|
20666
20986
|
const { elementId, answer } = req.body;
|
|
20667
20987
|
if (!elementId || answer === void 0) {
|
|
20668
20988
|
return res.status(400).json({ error: "Missing elementId or answer" });
|
|
20669
20989
|
}
|
|
20670
|
-
const
|
|
20671
|
-
|
|
20672
|
-
if (token) {
|
|
20673
|
-
const sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
|
|
20674
|
-
if (sessionRow) {
|
|
20675
|
-
const session = JSON.parse(sessionRow.session_data);
|
|
20676
|
-
if (session.role === "student") {
|
|
20677
|
-
studentId = session.studentId || session.userId || "guest";
|
|
20678
|
-
}
|
|
20679
|
-
}
|
|
20680
|
-
}
|
|
20990
|
+
const session = req.session;
|
|
20991
|
+
const studentId = session.studentId || session.userId || "guest";
|
|
20681
20992
|
const row = kernelContainer.db.prepare(
|
|
20682
20993
|
"SELECT data FROM whiteboard_elements WHERE id = ? AND lesson_id = ?"
|
|
20683
20994
|
).get(elementId, lessonId);
|
|
@@ -20701,15 +21012,12 @@ function registerLessonsRoutes(ctx) {
|
|
|
20701
21012
|
io.to(`lesson-${lessonId}`).emit("whiteboard-sync", { type: "element-updated", elementId });
|
|
20702
21013
|
res.json({ success: true, isCorrect, score, studentId });
|
|
20703
21014
|
} catch (e) {
|
|
20704
|
-
res
|
|
21015
|
+
sendSafeError(res, e);
|
|
20705
21016
|
}
|
|
20706
21017
|
});
|
|
20707
|
-
app.get("/api/lessons/:id/quiz-submissions", (req, res) => {
|
|
21018
|
+
app.get("/api/lessons/:id/quiz-submissions", requireAuth("teacher", "administrator"), (req, res) => {
|
|
20708
21019
|
try {
|
|
20709
21020
|
const { id: lessonId } = req.params;
|
|
20710
|
-
if (!checkIsTeacherOrAdmin(req)) {
|
|
20711
|
-
return res.status(403).json({ error: "Forbidden" });
|
|
20712
|
-
}
|
|
20713
21021
|
const elements = kernelContainer.db.prepare(
|
|
20714
21022
|
"SELECT id, type, data FROM whiteboard_elements WHERE lesson_id = ? AND type = ?"
|
|
20715
21023
|
).all(lessonId, "quiz");
|
|
@@ -20730,10 +21038,10 @@ function registerLessonsRoutes(ctx) {
|
|
|
20730
21038
|
});
|
|
20731
21039
|
res.json({ success: true, quizzes });
|
|
20732
21040
|
} catch (e) {
|
|
20733
|
-
res
|
|
21041
|
+
sendSafeError(res, e);
|
|
20734
21042
|
}
|
|
20735
21043
|
});
|
|
20736
|
-
app.post("/api/lessons/:id/ai-tutor", async (req, res) => {
|
|
21044
|
+
app.post("/api/lessons/:id/ai-tutor", requireAuth("student", "teacher", "administrator"), async (req, res) => {
|
|
20737
21045
|
try {
|
|
20738
21046
|
const { elements } = req.body;
|
|
20739
21047
|
const ai = new import_genai3.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
@@ -20762,12 +21070,16 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
20762
21070
|
res.json({ success: true, hint });
|
|
20763
21071
|
} catch (e) {
|
|
20764
21072
|
console.error("AI Tutor error:", e);
|
|
20765
|
-
res
|
|
21073
|
+
sendSafeError(res, e);
|
|
20766
21074
|
}
|
|
20767
21075
|
});
|
|
20768
|
-
app.delete("/api/lessons/:id", async (req, res) => {
|
|
21076
|
+
app.delete("/api/lessons/:id", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
20769
21077
|
try {
|
|
20770
21078
|
const { id } = req.params;
|
|
21079
|
+
const ownership = checkLessonOwnership(req, id);
|
|
21080
|
+
if (!ownership.allowed) {
|
|
21081
|
+
return res.status(ownership.status).json({ success: false, error: ownership.error });
|
|
21082
|
+
}
|
|
20771
21083
|
kernelContainer.db.prepare("DELETE FROM whiteboard_elements WHERE lesson_id = ?").run(id);
|
|
20772
21084
|
kernelContainer.db.prepare("DELETE FROM student_lesson_progress WHERE lesson_id = ?").run(id);
|
|
20773
21085
|
kernelContainer.db.prepare("DELETE FROM schedules WHERE lesson_id = ?").run(id);
|
|
@@ -20778,7 +21090,7 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
20778
21090
|
}
|
|
20779
21091
|
res.json({ success: true });
|
|
20780
21092
|
} catch (e) {
|
|
20781
|
-
res
|
|
21093
|
+
sendSafeError(res, e);
|
|
20782
21094
|
}
|
|
20783
21095
|
});
|
|
20784
21096
|
app.get("/api/lessons/:id/stats", (req, res) => {
|
|
@@ -20798,22 +21110,24 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
20798
21110
|
).get(id).count;
|
|
20799
21111
|
res.json({ whiteboardCount, scheduleCount, enrollmentCount, assignmentCount });
|
|
20800
21112
|
} catch (e) {
|
|
20801
|
-
res
|
|
21113
|
+
sendSafeError(res, e);
|
|
20802
21114
|
}
|
|
20803
21115
|
});
|
|
20804
|
-
app.post("/api/lessons/:id/clone", async (req, res) => {
|
|
21116
|
+
app.post("/api/lessons/:id/clone", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
20805
21117
|
try {
|
|
20806
21118
|
const { id } = req.params;
|
|
20807
21119
|
const original = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(id);
|
|
20808
21120
|
if (!original) {
|
|
20809
21121
|
return res.status(404).json({ error: "Lesson not found" });
|
|
20810
21122
|
}
|
|
21123
|
+
const session = req.session;
|
|
21124
|
+
const creatorId = session?.userId || session?.username || "usr_teacher";
|
|
20811
21125
|
const newId = (0, import_uuid16.v7)();
|
|
20812
21126
|
const now = Date.now();
|
|
20813
21127
|
const newTitle = `\u526F\u672C-${original.title}`;
|
|
20814
21128
|
kernelContainer.db.prepare(
|
|
20815
|
-
"INSERT INTO lessons (id, title, content, timeline, progress_mode, progress_conditions, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
20816
|
-
).run(newId, newTitle, original.content, original.timeline, original.progress_mode, original.progress_conditions, now, now);
|
|
21129
|
+
"INSERT INTO lessons (id, title, content, timeline, progress_mode, progress_conditions, creator_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
21130
|
+
).run(newId, newTitle, original.content, original.timeline, original.progress_mode, original.progress_conditions, creatorId, now, now);
|
|
20817
21131
|
const whiteboardElements = kernelContainer.db.prepare(
|
|
20818
21132
|
"SELECT * FROM whiteboard_elements WHERE lesson_id = ?"
|
|
20819
21133
|
).all(id);
|
|
@@ -20823,40 +21137,29 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
20823
21137
|
for (const el of whiteboardElements) {
|
|
20824
21138
|
insertElement.run((0, import_uuid16.v7)(), newId, el.type, el.data, now);
|
|
20825
21139
|
}
|
|
20826
|
-
const cloned = kernelContainer.db.prepare(
|
|
20827
|
-
|
|
20828
|
-
|
|
21140
|
+
const cloned = kernelContainer.db.prepare(`
|
|
21141
|
+
SELECT l.*, u.name as creator_name, 0 as enrollment_count
|
|
21142
|
+
FROM lessons l
|
|
21143
|
+
LEFT JOIN users u ON l.creator_id = u.id
|
|
21144
|
+
WHERE l.id = ?
|
|
21145
|
+
`).get(newId);
|
|
20829
21146
|
res.json({ success: true, lesson: cloned });
|
|
20830
21147
|
} catch (e) {
|
|
20831
|
-
res
|
|
21148
|
+
sendSafeError(res, e);
|
|
20832
21149
|
}
|
|
20833
21150
|
});
|
|
20834
21151
|
}
|
|
20835
21152
|
|
|
20836
21153
|
// server/routes/workspace.ts
|
|
20837
|
-
var
|
|
21154
|
+
var import_path12 = __toESM(require("path"), 1);
|
|
20838
21155
|
function registerWorkspaceRoutes(ctx) {
|
|
20839
|
-
const {
|
|
20840
|
-
|
|
20841
|
-
io,
|
|
20842
|
-
loginLimiter,
|
|
20843
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
20844
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
20845
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
20846
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
20847
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
20848
|
-
buildOpenAITools: buildOpenAITools2,
|
|
20849
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
20850
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
20851
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
20852
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
20853
|
-
} = ctx;
|
|
20854
|
-
app.get("/api/events", (req, res) => {
|
|
21156
|
+
const { app, MF_REMOTE_CACHE: MF_REMOTE_CACHE2 } = ctx;
|
|
21157
|
+
app.get("/api/events", requireAuth("administrator"), (req, res) => {
|
|
20855
21158
|
try {
|
|
20856
21159
|
const events = kernelContainer.db.prepare("SELECT * FROM events ORDER BY timestamp DESC LIMIT 50").all();
|
|
20857
21160
|
res.json(events);
|
|
20858
21161
|
} catch (e) {
|
|
20859
|
-
res
|
|
21162
|
+
sendSafeError(res, e);
|
|
20860
21163
|
}
|
|
20861
21164
|
});
|
|
20862
21165
|
app.get("/api/mfe/remotes", (req, res) => {
|
|
@@ -20888,12 +21191,15 @@ function registerWorkspaceRoutes(ctx) {
|
|
|
20888
21191
|
MF_REMOTE_CACHE2.set(name, result);
|
|
20889
21192
|
res.json({ success: true, result });
|
|
20890
21193
|
} catch (e) {
|
|
20891
|
-
res
|
|
21194
|
+
sendSafeError(res, e);
|
|
20892
21195
|
}
|
|
20893
21196
|
});
|
|
20894
|
-
app.get("/api/vfs", (req, res) => {
|
|
21197
|
+
app.get("/api/vfs", requireAuth(), (req, res) => {
|
|
20895
21198
|
try {
|
|
20896
21199
|
const parentId = req.query.parentId === "null" ? null : req.query.parentId || null;
|
|
21200
|
+
const isStaff = checkIsTeacherOrAdmin(req);
|
|
21201
|
+
const token = getCookieToken(req);
|
|
21202
|
+
const session = req.session || (token ? getValidSession(token) : null);
|
|
20897
21203
|
let nodes = [];
|
|
20898
21204
|
if (parentId === "virtual-lessons") {
|
|
20899
21205
|
const lessons = kernelContainer.db.prepare("SELECT id, title, content FROM lessons").all();
|
|
@@ -20902,12 +21208,23 @@ function registerWorkspaceRoutes(ctx) {
|
|
|
20902
21208
|
const assignments = kernelContainer.db.prepare("SELECT a.id, a.title, c.name as cname, a.content FROM assignments a JOIN classes c ON a.class_id = c.id").all();
|
|
20903
21209
|
nodes = assignments.map((a) => ({ id: `assgn-${a.id}`, parent_id: "virtual-assignments", type: "file", name: `[${a.cname}] ${a.title}.md`, content: a.content }));
|
|
20904
21210
|
} else if (parentId === "virtual-submissions") {
|
|
20905
|
-
|
|
20906
|
-
|
|
20907
|
-
|
|
20908
|
-
|
|
20909
|
-
|
|
20910
|
-
|
|
21211
|
+
let submissions = [];
|
|
21212
|
+
if (isStaff) {
|
|
21213
|
+
submissions = kernelContainer.db.prepare(`
|
|
21214
|
+
SELECT sub.id, sub.content, a.title, s.name as sname, sub.score
|
|
21215
|
+
FROM assignment_submissions sub
|
|
21216
|
+
JOIN assignments a ON sub.assignment_id = a.id
|
|
21217
|
+
JOIN students s ON sub.student_id = s.id
|
|
21218
|
+
`).all();
|
|
21219
|
+
} else if (session?.userId) {
|
|
21220
|
+
submissions = kernelContainer.db.prepare(`
|
|
21221
|
+
SELECT sub.id, sub.content, a.title, s.name as sname, sub.score
|
|
21222
|
+
FROM assignment_submissions sub
|
|
21223
|
+
JOIN assignments a ON sub.assignment_id = a.id
|
|
21224
|
+
JOIN students s ON sub.student_id = s.id
|
|
21225
|
+
WHERE sub.student_id = ?
|
|
21226
|
+
`).all(session.userId);
|
|
21227
|
+
}
|
|
20911
21228
|
nodes = submissions.map((sub) => ({
|
|
20912
21229
|
id: `sub-${sub.id}`,
|
|
20913
21230
|
parent_id: "virtual-submissions",
|
|
@@ -20934,7 +21251,7 @@ ${sub.content}`
|
|
|
20934
21251
|
}
|
|
20935
21252
|
res.json(nodes);
|
|
20936
21253
|
} catch (e) {
|
|
20937
|
-
res
|
|
21254
|
+
sendSafeError(res, e);
|
|
20938
21255
|
}
|
|
20939
21256
|
});
|
|
20940
21257
|
app.get("/files/*", (req, res) => {
|
|
@@ -20968,7 +21285,7 @@ ${sub.content}`
|
|
|
20968
21285
|
}
|
|
20969
21286
|
const filename = parts[parts.length - 1];
|
|
20970
21287
|
res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"`);
|
|
20971
|
-
const ext =
|
|
21288
|
+
const ext = import_path12.default.extname(filename).toLowerCase();
|
|
20972
21289
|
const binaryExtensions = [".pdf", ".xlsx", ".xls", ".zip", ".png", ".jpg", ".jpeg", ".gif", ".mp4", ".mp3"];
|
|
20973
21290
|
const content = foundNode.content || "";
|
|
20974
21291
|
if (binaryExtensions.includes(ext)) {
|
|
@@ -20980,29 +21297,15 @@ ${sub.content}`
|
|
|
20980
21297
|
}
|
|
20981
21298
|
res.send(content);
|
|
20982
21299
|
} catch (e) {
|
|
20983
|
-
res
|
|
21300
|
+
sendSafeError(res, e);
|
|
20984
21301
|
}
|
|
20985
21302
|
});
|
|
20986
21303
|
}
|
|
20987
21304
|
|
|
20988
21305
|
// server/routes/processes.ts
|
|
20989
21306
|
function registerProcessesRoutes(ctx) {
|
|
20990
|
-
const {
|
|
20991
|
-
|
|
20992
|
-
io,
|
|
20993
|
-
loginLimiter,
|
|
20994
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
20995
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
20996
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
20997
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
20998
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
20999
|
-
buildOpenAITools: buildOpenAITools2,
|
|
21000
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
21001
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
21002
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
21003
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
21004
|
-
} = ctx;
|
|
21005
|
-
app.get("/api/approvals", (req, res) => {
|
|
21307
|
+
const { app } = ctx;
|
|
21308
|
+
app.get("/api/approvals", requireAuth("administrator"), (req, res) => {
|
|
21006
21309
|
try {
|
|
21007
21310
|
const list = kernelContainer.db.prepare("SELECT * FROM pending_commands ORDER BY created_at DESC").all();
|
|
21008
21311
|
res.json(list);
|
|
@@ -21010,71 +21313,54 @@ function registerProcessesRoutes(ctx) {
|
|
|
21010
21313
|
res.status(500).json({ error: e.message });
|
|
21011
21314
|
}
|
|
21012
21315
|
});
|
|
21013
|
-
app.post("/api/approvals/:id/approve", async (req, res) => {
|
|
21316
|
+
app.post("/api/approvals/:id/approve", requireAuth("administrator"), async (req, res) => {
|
|
21014
21317
|
try {
|
|
21015
21318
|
const pending = kernelContainer.db.prepare("SELECT * FROM pending_commands WHERE id = ?").get(req.params.id);
|
|
21016
21319
|
if (!pending) return res.status(404).json({ error: "Not found" });
|
|
21017
|
-
|
|
21018
|
-
|
|
21019
|
-
payload = { ...payload, ...req.body.payloadOverride };
|
|
21020
|
-
}
|
|
21320
|
+
const payload = JSON.parse(pending.payload);
|
|
21321
|
+
const approverId = getActorId(req) || "admin";
|
|
21021
21322
|
const cmd = kernelContainer.commandBus.createCommand(
|
|
21022
21323
|
pending.command_type,
|
|
21023
21324
|
payload,
|
|
21024
|
-
|
|
21325
|
+
approverId,
|
|
21025
21326
|
{ approved: true }
|
|
21026
|
-
// Bypass high risk check now
|
|
21027
21327
|
);
|
|
21028
21328
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
21029
21329
|
kernelContainer.db.prepare("DELETE FROM pending_commands WHERE id = ?").run(pending.id);
|
|
21030
21330
|
res.json({ success: true, result });
|
|
21031
21331
|
} catch (e) {
|
|
21032
|
-
res
|
|
21332
|
+
sendSafeError(res, e);
|
|
21033
21333
|
}
|
|
21034
21334
|
});
|
|
21035
|
-
app.post("/api/approvals/:id/reject", async (req, res) => {
|
|
21335
|
+
app.post("/api/approvals/:id/reject", requireAuth("administrator"), async (req, res) => {
|
|
21036
21336
|
try {
|
|
21037
21337
|
kernelContainer.db.prepare("DELETE FROM pending_commands WHERE id = ?").run(req.params.id);
|
|
21038
21338
|
res.json({ success: true });
|
|
21039
21339
|
} catch (e) {
|
|
21040
|
-
res
|
|
21340
|
+
sendSafeError(res, e);
|
|
21041
21341
|
}
|
|
21042
21342
|
});
|
|
21043
|
-
app.get("/api/processes", (req, res) => {
|
|
21343
|
+
app.get("/api/processes", requireAuth("administrator"), (req, res) => {
|
|
21044
21344
|
try {
|
|
21045
21345
|
const list = kernelContainer.db.prepare("SELECT id, name, status, created_at, updated_at FROM processes WHERE status = 'running' ORDER BY created_at DESC").all();
|
|
21046
21346
|
res.json(list);
|
|
21047
21347
|
} catch (e) {
|
|
21048
|
-
res
|
|
21348
|
+
sendSafeError(res, e);
|
|
21049
21349
|
}
|
|
21050
21350
|
});
|
|
21051
|
-
app.get("/api/processes/:id/logs", (req, res) => {
|
|
21351
|
+
app.get("/api/processes/:id/logs", requireAuth("administrator"), (req, res) => {
|
|
21052
21352
|
try {
|
|
21053
21353
|
const dbRow = kernelContainer.db.prepare("SELECT logs FROM processes WHERE id = ?").get(req.params.id);
|
|
21054
21354
|
res.json(dbRow || { logs: "" });
|
|
21055
21355
|
} catch (e) {
|
|
21056
|
-
res
|
|
21356
|
+
sendSafeError(res, e);
|
|
21057
21357
|
}
|
|
21058
21358
|
});
|
|
21059
21359
|
}
|
|
21060
21360
|
|
|
21061
21361
|
// server/routes/admin.ts
|
|
21062
21362
|
function registerAdminRoutes(ctx) {
|
|
21063
|
-
const {
|
|
21064
|
-
app,
|
|
21065
|
-
io,
|
|
21066
|
-
loginLimiter,
|
|
21067
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
21068
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
21069
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
21070
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
21071
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
21072
|
-
buildOpenAITools: buildOpenAITools2,
|
|
21073
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
21074
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
21075
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
21076
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
21077
|
-
} = ctx;
|
|
21363
|
+
const { app } = ctx;
|
|
21078
21364
|
app.post("/api/admin/seed-demo", requireAuth("administrator"), (req, res) => {
|
|
21079
21365
|
try {
|
|
21080
21366
|
const db2 = kernelContainer.db;
|
|
@@ -21138,7 +21424,7 @@ function registerAdminRoutes(ctx) {
|
|
|
21138
21424
|
}
|
|
21139
21425
|
res.json({ success: true, classId: DEMO_CLASS_ID, scheduleId: DEMO_SCHEDULE_ID, lessonId });
|
|
21140
21426
|
} catch (e) {
|
|
21141
|
-
res
|
|
21427
|
+
sendSafeError(res, e);
|
|
21142
21428
|
}
|
|
21143
21429
|
});
|
|
21144
21430
|
app.post("/api/classes/import", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -21188,7 +21474,7 @@ function registerAdminRoutes(ctx) {
|
|
|
21188
21474
|
}
|
|
21189
21475
|
res.json({ success: true, imported });
|
|
21190
21476
|
} catch (e) {
|
|
21191
|
-
res
|
|
21477
|
+
sendSafeError(res, e);
|
|
21192
21478
|
}
|
|
21193
21479
|
});
|
|
21194
21480
|
app.post("/api/students/import", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -21224,32 +21510,18 @@ function registerAdminRoutes(ctx) {
|
|
|
21224
21510
|
}
|
|
21225
21511
|
res.json({ success: true, imported });
|
|
21226
21512
|
} catch (e) {
|
|
21227
|
-
res
|
|
21513
|
+
sendSafeError(res, e);
|
|
21228
21514
|
}
|
|
21229
21515
|
});
|
|
21230
21516
|
}
|
|
21231
21517
|
|
|
21232
21518
|
// server/routes/roster.ts
|
|
21233
|
-
var
|
|
21234
|
-
var
|
|
21519
|
+
var import_path13 = __toESM(require("path"), 1);
|
|
21520
|
+
var import_fs12 = __toESM(require("fs"), 1);
|
|
21235
21521
|
var import_crypto10 = __toESM(require("crypto"), 1);
|
|
21236
21522
|
var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
|
|
21237
21523
|
function registerRosterRoutes(ctx) {
|
|
21238
|
-
const {
|
|
21239
|
-
app,
|
|
21240
|
-
io,
|
|
21241
|
-
loginLimiter,
|
|
21242
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
21243
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
21244
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
21245
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
21246
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
21247
|
-
buildOpenAITools: buildOpenAITools2,
|
|
21248
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
21249
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
21250
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
21251
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
21252
|
-
} = ctx;
|
|
21524
|
+
const { app, io, loginLimiter } = ctx;
|
|
21253
21525
|
app.get("/api/classes", (req, res) => {
|
|
21254
21526
|
try {
|
|
21255
21527
|
const classes = kernelContainer.db.prepare(`
|
|
@@ -21283,10 +21555,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21283
21555
|
`).all(req.params.id);
|
|
21284
21556
|
res.json(students);
|
|
21285
21557
|
} catch (e) {
|
|
21286
|
-
res
|
|
21558
|
+
sendSafeError(res, e);
|
|
21287
21559
|
}
|
|
21288
21560
|
});
|
|
21289
|
-
app.post("/api/classes", (req, res) => {
|
|
21561
|
+
app.post("/api/classes", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21290
21562
|
try {
|
|
21291
21563
|
const { name, description } = req.body;
|
|
21292
21564
|
const classId = Math.random().toString(36).slice(2);
|
|
@@ -21298,10 +21570,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21298
21570
|
);
|
|
21299
21571
|
res.json({ success: true, id: classId });
|
|
21300
21572
|
} catch (e) {
|
|
21301
|
-
res
|
|
21573
|
+
sendSafeError(res, e);
|
|
21302
21574
|
}
|
|
21303
21575
|
});
|
|
21304
|
-
app.put("/api/classes/:id", (req, res) => {
|
|
21576
|
+
app.put("/api/classes/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21305
21577
|
try {
|
|
21306
21578
|
const { name, description, class_passcode } = req.body;
|
|
21307
21579
|
if (name) kernelContainer.db.prepare("UPDATE classes SET name = ? WHERE id = ?").run(name, req.params.id);
|
|
@@ -21309,10 +21581,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21309
21581
|
if (class_passcode !== void 0) kernelContainer.db.prepare("UPDATE classes SET class_passcode = ? WHERE id = ?").run(class_passcode, req.params.id);
|
|
21310
21582
|
res.json({ success: true });
|
|
21311
21583
|
} catch (e) {
|
|
21312
|
-
res
|
|
21584
|
+
sendSafeError(res, e);
|
|
21313
21585
|
}
|
|
21314
21586
|
});
|
|
21315
|
-
app.delete("/api/classes/:id", (req, res) => {
|
|
21587
|
+
app.delete("/api/classes/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21316
21588
|
try {
|
|
21317
21589
|
const classId = req.params.id;
|
|
21318
21590
|
const db2 = kernelContainer.db;
|
|
@@ -21354,7 +21626,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21354
21626
|
deleteTransaction();
|
|
21355
21627
|
res.json({ success: true });
|
|
21356
21628
|
} catch (e) {
|
|
21357
|
-
res
|
|
21629
|
+
sendSafeError(res, e);
|
|
21358
21630
|
}
|
|
21359
21631
|
});
|
|
21360
21632
|
app.get("/api/db-status", (req, res) => {
|
|
@@ -21419,7 +21691,8 @@ function registerRosterRoutes(ctx) {
|
|
|
21419
21691
|
}
|
|
21420
21692
|
return res.status(500).json({ status: "disconnected", error: "Unexpected response from SQLite" });
|
|
21421
21693
|
} catch (e) {
|
|
21422
|
-
|
|
21694
|
+
const exposed = process.env.NODE_ENV === "production" ? "Database connection error" : e.message;
|
|
21695
|
+
return res.status(500).json({ status: "disconnected", error: exposed });
|
|
21423
21696
|
}
|
|
21424
21697
|
});
|
|
21425
21698
|
app.get("/api/auth/session", (req, res) => {
|
|
@@ -21434,7 +21707,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21434
21707
|
}
|
|
21435
21708
|
res.json({ session });
|
|
21436
21709
|
} catch (e) {
|
|
21437
|
-
res
|
|
21710
|
+
sendSafeError(res, e);
|
|
21438
21711
|
}
|
|
21439
21712
|
});
|
|
21440
21713
|
app.post("/api/auth/logout", (req, res) => {
|
|
@@ -21443,10 +21716,12 @@ function registerRosterRoutes(ctx) {
|
|
|
21443
21716
|
if (token) {
|
|
21444
21717
|
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
21445
21718
|
}
|
|
21446
|
-
|
|
21719
|
+
const isSecure = req.secure || req.headers["x-forwarded-proto"] === "https" || process.env.NODE_ENV === "production" && process.env.ENABLE_HTTPS === "true";
|
|
21720
|
+
const secureFlag = isSecure ? "; Secure" : "";
|
|
21721
|
+
res.setHeader("Set-Cookie", `edu_os_token=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax${secureFlag}`);
|
|
21447
21722
|
res.json({ success: true });
|
|
21448
21723
|
} catch (e) {
|
|
21449
|
-
res
|
|
21724
|
+
sendSafeError(res, e);
|
|
21450
21725
|
}
|
|
21451
21726
|
});
|
|
21452
21727
|
app.post("/api/auth/change-password", (req, res) => {
|
|
@@ -21505,7 +21780,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21505
21780
|
}
|
|
21506
21781
|
res.status(400).json({ error: "Unsupported role" });
|
|
21507
21782
|
} catch (e) {
|
|
21508
|
-
res
|
|
21783
|
+
sendSafeError(res, e);
|
|
21509
21784
|
}
|
|
21510
21785
|
});
|
|
21511
21786
|
app.post("/api/auth/profile", (req, res) => {
|
|
@@ -21536,7 +21811,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21536
21811
|
}
|
|
21537
21812
|
res.json({ success: true, name });
|
|
21538
21813
|
} catch (e) {
|
|
21539
|
-
res
|
|
21814
|
+
sendSafeError(res, e);
|
|
21540
21815
|
}
|
|
21541
21816
|
});
|
|
21542
21817
|
app.post("/api/auth/avatar", (req, res) => {
|
|
@@ -21549,7 +21824,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21549
21824
|
if (!filename || !base64Data) {
|
|
21550
21825
|
return res.status(400).json({ error: "Filename and base64Data are required" });
|
|
21551
21826
|
}
|
|
21552
|
-
const ext =
|
|
21827
|
+
const ext = import_path13.default.extname(filename).toLowerCase();
|
|
21553
21828
|
const ALLOWED_IMG = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
|
|
21554
21829
|
if (BLOCKED_EXTENSIONS.includes(ext) || !ALLOWED_IMG.includes(ext)) {
|
|
21555
21830
|
return res.status(400).json({ error: "Only image files (jpg / png / gif / webp) are allowed" });
|
|
@@ -21562,11 +21837,11 @@ function registerRosterRoutes(ctx) {
|
|
|
21562
21837
|
if (!validateMagicBytes(fileBuffer, filename)) {
|
|
21563
21838
|
return res.status(400).json({ error: "File content does not match the declared image type" });
|
|
21564
21839
|
}
|
|
21565
|
-
const avatarDir =
|
|
21566
|
-
|
|
21840
|
+
const avatarDir = import_path13.default.join(process.cwd(), "uploads", "avatars");
|
|
21841
|
+
import_fs12.default.mkdirSync(avatarDir, { recursive: true });
|
|
21567
21842
|
const uniqueName = `${Date.now()}-${import_crypto10.default.randomBytes(4).toString("hex")}${ext}`;
|
|
21568
|
-
const filePath =
|
|
21569
|
-
|
|
21843
|
+
const filePath = import_path13.default.join(avatarDir, uniqueName);
|
|
21844
|
+
import_fs12.default.writeFileSync(filePath, fileBuffer);
|
|
21570
21845
|
const avatarUrl = `/uploads/avatars/${uniqueName}`;
|
|
21571
21846
|
let oldAvatar = null;
|
|
21572
21847
|
if (session.role === "student") {
|
|
@@ -21580,7 +21855,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21580
21855
|
}
|
|
21581
21856
|
if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
|
|
21582
21857
|
try {
|
|
21583
|
-
|
|
21858
|
+
import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
|
|
21584
21859
|
} catch {
|
|
21585
21860
|
}
|
|
21586
21861
|
}
|
|
@@ -21595,7 +21870,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21595
21870
|
}
|
|
21596
21871
|
res.json({ success: true, avatar: avatarUrl });
|
|
21597
21872
|
} catch (e) {
|
|
21598
|
-
res
|
|
21873
|
+
sendSafeError(res, e);
|
|
21599
21874
|
}
|
|
21600
21875
|
});
|
|
21601
21876
|
app.delete("/api/auth/avatar", (req, res) => {
|
|
@@ -21616,7 +21891,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21616
21891
|
}
|
|
21617
21892
|
if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
|
|
21618
21893
|
try {
|
|
21619
|
-
|
|
21894
|
+
import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
|
|
21620
21895
|
} catch {
|
|
21621
21896
|
}
|
|
21622
21897
|
}
|
|
@@ -21631,7 +21906,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21631
21906
|
}
|
|
21632
21907
|
res.json({ success: true });
|
|
21633
21908
|
} catch (e) {
|
|
21634
|
-
res
|
|
21909
|
+
sendSafeError(res, e);
|
|
21635
21910
|
}
|
|
21636
21911
|
});
|
|
21637
21912
|
app.post("/api/auth/login", loginLimiter, (req, res) => {
|
|
@@ -21726,7 +22001,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21726
22001
|
const now = Date.now();
|
|
21727
22002
|
const expiresAt = now + 7 * 24 * 60 * 60 * 1e3;
|
|
21728
22003
|
kernelContainer.db.prepare("INSERT INTO client_sessions (id, session_data, updated_at, expires_at) VALUES (?, ?, ?, ?)").run(sessionToken, JSON.stringify(sessionData), now, expiresAt);
|
|
21729
|
-
|
|
22004
|
+
const isSecure = req.secure || req.headers["x-forwarded-proto"] === "https" || process.env.NODE_ENV === "production" && process.env.ENABLE_HTTPS === "true";
|
|
22005
|
+
const secureFlag = isSecure ? "; Secure" : "";
|
|
22006
|
+
const maxAgeSeconds = 7 * 24 * 60 * 60;
|
|
22007
|
+
res.setHeader("Set-Cookie", `edu_os_token=${sessionToken}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAgeSeconds}${secureFlag}`);
|
|
21730
22008
|
return res.json({
|
|
21731
22009
|
success: true,
|
|
21732
22010
|
session: sessionData
|
|
@@ -21734,7 +22012,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21734
22012
|
}
|
|
21735
22013
|
res.status(400).json({ error: "Unsupported entry type" });
|
|
21736
22014
|
} catch (e) {
|
|
21737
|
-
res
|
|
22015
|
+
sendSafeError(res, e);
|
|
21738
22016
|
}
|
|
21739
22017
|
});
|
|
21740
22018
|
app.get("/api/students/:id/read_notifications", (req, res) => {
|
|
@@ -21742,10 +22020,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21742
22020
|
const rows = kernelContainer.db.prepare("SELECT notification_id FROM student_read_notifications WHERE student_id = ?").all(req.params.id);
|
|
21743
22021
|
res.json(rows.map((r) => r.notification_id));
|
|
21744
22022
|
} catch (e) {
|
|
21745
|
-
res
|
|
22023
|
+
sendSafeError(res, e);
|
|
21746
22024
|
}
|
|
21747
22025
|
});
|
|
21748
|
-
app.post("/api/students/:id/read_notifications", (req, res) => {
|
|
22026
|
+
app.post("/api/students/:id/read_notifications", requireAuth(), (req, res) => {
|
|
21749
22027
|
try {
|
|
21750
22028
|
const { notificationId } = req.body;
|
|
21751
22029
|
if (!notificationId) {
|
|
@@ -21758,10 +22036,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21758
22036
|
});
|
|
21759
22037
|
res.json({ success: true });
|
|
21760
22038
|
} catch (e) {
|
|
21761
|
-
res
|
|
22039
|
+
sendSafeError(res, e);
|
|
21762
22040
|
}
|
|
21763
22041
|
});
|
|
21764
|
-
app.post("/api/classes/:classId/lock_lesson", (req, res) => {
|
|
22042
|
+
app.post("/api/classes/:classId/lock_lesson", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21765
22043
|
try {
|
|
21766
22044
|
const { lessonId } = req.body;
|
|
21767
22045
|
if (!lessonId) {
|
|
@@ -21778,7 +22056,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21778
22056
|
res.status(550).json({ error: e.message });
|
|
21779
22057
|
}
|
|
21780
22058
|
});
|
|
21781
|
-
app.post("/api/classes/:classId/unlock_lesson", (req, res) => {
|
|
22059
|
+
app.post("/api/classes/:classId/unlock_lesson", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21782
22060
|
try {
|
|
21783
22061
|
kernelContainer.db.prepare("UPDATE students SET locked_lesson_id = NULL WHERE id IN (SELECT student_id FROM class_students WHERE class_id = ?)").run(req.params.classId);
|
|
21784
22062
|
io.emit("class-lock-status-changed", {
|
|
@@ -21848,7 +22126,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21848
22126
|
const labs = kernelContainer.db.prepare("SELECT * FROM computer_labs ORDER BY created_at DESC").all();
|
|
21849
22127
|
res.json(labs);
|
|
21850
22128
|
} catch (e) {
|
|
21851
|
-
res
|
|
22129
|
+
sendSafeError(res, e);
|
|
21852
22130
|
}
|
|
21853
22131
|
});
|
|
21854
22132
|
app.post("/api/labs", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -21860,7 +22138,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21860
22138
|
).run(id, room_number, parseInt(rows), parseInt(cols), Date.now());
|
|
21861
22139
|
res.json({ success: true, id, room_number, rows, cols });
|
|
21862
22140
|
} catch (e) {
|
|
21863
|
-
res
|
|
22141
|
+
sendSafeError(res, e);
|
|
21864
22142
|
}
|
|
21865
22143
|
});
|
|
21866
22144
|
app.put("/api/labs/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -21871,7 +22149,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21871
22149
|
).run(room_number, parseInt(rows), parseInt(cols), req.params.id);
|
|
21872
22150
|
res.json({ success: true });
|
|
21873
22151
|
} catch (e) {
|
|
21874
|
-
res
|
|
22152
|
+
sendSafeError(res, e);
|
|
21875
22153
|
}
|
|
21876
22154
|
});
|
|
21877
22155
|
app.delete("/api/labs/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -21880,7 +22158,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21880
22158
|
kernelContainer.db.prepare("DELETE FROM student_seats WHERE lab_id = ?").run(req.params.id);
|
|
21881
22159
|
res.json({ success: true });
|
|
21882
22160
|
} catch (e) {
|
|
21883
|
-
res
|
|
22161
|
+
sendSafeError(res, e);
|
|
21884
22162
|
}
|
|
21885
22163
|
});
|
|
21886
22164
|
app.get("/api/classes/:classId/seats", (req, res) => {
|
|
@@ -21890,7 +22168,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21890
22168
|
const seats = kernelContainer.db.prepare("SELECT * FROM student_seats WHERE class_id = ?").all(req.params.classId);
|
|
21891
22169
|
res.json({ lab_id: labId, seats });
|
|
21892
22170
|
} catch (e) {
|
|
21893
|
-
res
|
|
22171
|
+
sendSafeError(res, e);
|
|
21894
22172
|
}
|
|
21895
22173
|
});
|
|
21896
22174
|
app.post("/api/classes/:classId/seats", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -21908,10 +22186,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21908
22186
|
}
|
|
21909
22187
|
res.json({ success: true });
|
|
21910
22188
|
} catch (e) {
|
|
21911
|
-
res
|
|
22189
|
+
sendSafeError(res, e);
|
|
21912
22190
|
}
|
|
21913
22191
|
});
|
|
21914
|
-
app.post("/api/students", (req, res) => {
|
|
22192
|
+
app.post("/api/students", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21915
22193
|
try {
|
|
21916
22194
|
const { name, email, password, student_number } = req.body;
|
|
21917
22195
|
const studentId = Math.random().toString(36).slice(2);
|
|
@@ -21930,10 +22208,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21930
22208
|
);
|
|
21931
22209
|
res.json({ success: true, id: studentId, student_number: finalNum, tempPassword: password || "123456" });
|
|
21932
22210
|
} catch (e) {
|
|
21933
|
-
res
|
|
22211
|
+
sendSafeError(res, e);
|
|
21934
22212
|
}
|
|
21935
22213
|
});
|
|
21936
|
-
app.put("/api/students/:id", (req, res) => {
|
|
22214
|
+
app.put("/api/students/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21937
22215
|
try {
|
|
21938
22216
|
const { name, email, password, locked_lesson_id, private_notes, student_number } = req.body;
|
|
21939
22217
|
if (name) kernelContainer.db.prepare("UPDATE students SET name = ? WHERE id = ?").run(name, req.params.id);
|
|
@@ -21947,10 +22225,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21947
22225
|
if (student_number !== void 0) kernelContainer.db.prepare("UPDATE students SET student_number = ? WHERE id = ?").run(student_number, req.params.id);
|
|
21948
22226
|
res.json({ success: true });
|
|
21949
22227
|
} catch (e) {
|
|
21950
|
-
res
|
|
22228
|
+
sendSafeError(res, e);
|
|
21951
22229
|
}
|
|
21952
22230
|
});
|
|
21953
|
-
app.delete("/api/students/:id", (req, res) => {
|
|
22231
|
+
app.delete("/api/students/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21954
22232
|
try {
|
|
21955
22233
|
kernelContainer.db.prepare("DELETE FROM class_students WHERE student_id = ?").run(req.params.id);
|
|
21956
22234
|
kernelContainer.db.prepare("DELETE FROM student_lesson_progress WHERE student_id = ?").run(req.params.id);
|
|
@@ -21966,7 +22244,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21966
22244
|
kernelContainer.db.prepare("DELETE FROM students WHERE id = ?").run(req.params.id);
|
|
21967
22245
|
res.json({ success: true });
|
|
21968
22246
|
} catch (e) {
|
|
21969
|
-
res
|
|
22247
|
+
sendSafeError(res, e);
|
|
21970
22248
|
}
|
|
21971
22249
|
});
|
|
21972
22250
|
app.get("/api/students/:id/export", (req, res) => {
|
|
@@ -22003,14 +22281,11 @@ function registerRosterRoutes(ctx) {
|
|
|
22003
22281
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22004
22282
|
});
|
|
22005
22283
|
} catch (e) {
|
|
22006
|
-
res
|
|
22284
|
+
sendSafeError(res, e);
|
|
22007
22285
|
}
|
|
22008
22286
|
});
|
|
22009
|
-
app.delete("/api/students/:id/gdpr-delete", (req, res) => {
|
|
22287
|
+
app.delete("/api/students/:id/gdpr-delete", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22010
22288
|
try {
|
|
22011
|
-
if (!checkIsTeacherOrAdmin(req)) {
|
|
22012
|
-
return res.status(403).json({ error: "Only teachers and administrators can perform GDPR deletion" });
|
|
22013
|
-
}
|
|
22014
22289
|
const studentId = req.params.id;
|
|
22015
22290
|
const { confirm } = req.body;
|
|
22016
22291
|
if (confirm !== true) {
|
|
@@ -22031,7 +22306,7 @@ function registerRosterRoutes(ctx) {
|
|
|
22031
22306
|
console.log(`[GDPR] Complete data deletion for student ${studentId}`);
|
|
22032
22307
|
res.json({ success: true, message: "All student data has been permanently deleted." });
|
|
22033
22308
|
} catch (e) {
|
|
22034
|
-
res
|
|
22309
|
+
sendSafeError(res, e);
|
|
22035
22310
|
}
|
|
22036
22311
|
});
|
|
22037
22312
|
app.get("/api/students/:id/progress", (req, res) => {
|
|
@@ -22044,11 +22319,16 @@ function registerRosterRoutes(ctx) {
|
|
|
22044
22319
|
`).all(req.params.id);
|
|
22045
22320
|
res.json(progress);
|
|
22046
22321
|
} catch (e) {
|
|
22047
|
-
res
|
|
22322
|
+
sendSafeError(res, e);
|
|
22048
22323
|
}
|
|
22049
22324
|
});
|
|
22050
|
-
app.post("/api/students/:id/progress", (req, res) => {
|
|
22325
|
+
app.post("/api/students/:id/progress", requireAuth(), (req, res) => {
|
|
22051
22326
|
try {
|
|
22327
|
+
const session = req.session;
|
|
22328
|
+
const isPrivileged = session && (session.role === "teacher" || session.role === "administrator");
|
|
22329
|
+
if (!isPrivileged && session?.userId !== req.params.id) {
|
|
22330
|
+
return res.status(403).json({ error: "Cannot update progress for another student" });
|
|
22331
|
+
}
|
|
22052
22332
|
const { lessonId, completed, progressPercent, completedSegments } = req.body;
|
|
22053
22333
|
const completedSegmentsStr = typeof completedSegments === "string" ? completedSegments : JSON.stringify(completedSegments || []);
|
|
22054
22334
|
kernelContainer.db.prepare(`
|
|
@@ -22075,10 +22355,10 @@ function registerRosterRoutes(ctx) {
|
|
|
22075
22355
|
});
|
|
22076
22356
|
res.json({ success: true });
|
|
22077
22357
|
} catch (e) {
|
|
22078
|
-
res
|
|
22358
|
+
sendSafeError(res, e);
|
|
22079
22359
|
}
|
|
22080
22360
|
});
|
|
22081
|
-
app.post("/api/classes/:id/students", (req, res) => {
|
|
22361
|
+
app.post("/api/classes/:id/students", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22082
22362
|
try {
|
|
22083
22363
|
const { studentId } = req.body;
|
|
22084
22364
|
kernelContainer.db.prepare("INSERT OR IGNORE INTO class_students (class_id, student_id, joined_at) VALUES (?, ?, ?)").run(
|
|
@@ -22088,7 +22368,7 @@ function registerRosterRoutes(ctx) {
|
|
|
22088
22368
|
);
|
|
22089
22369
|
res.json({ success: true });
|
|
22090
22370
|
} catch (e) {
|
|
22091
|
-
res
|
|
22371
|
+
sendSafeError(res, e);
|
|
22092
22372
|
}
|
|
22093
22373
|
});
|
|
22094
22374
|
app.post("/api/classes/:id/students/bulk-enroll", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -22130,7 +22410,7 @@ function registerRosterRoutes(ctx) {
|
|
|
22130
22410
|
}
|
|
22131
22411
|
res.json({ success: true, count: results.length, results });
|
|
22132
22412
|
} catch (e) {
|
|
22133
|
-
res
|
|
22413
|
+
sendSafeError(res, e);
|
|
22134
22414
|
}
|
|
22135
22415
|
});
|
|
22136
22416
|
app.get("/api/classes/:id/progress", (req, res) => {
|
|
@@ -22145,7 +22425,7 @@ function registerRosterRoutes(ctx) {
|
|
|
22145
22425
|
`).all(req.params.id);
|
|
22146
22426
|
res.json(progress);
|
|
22147
22427
|
} catch (e) {
|
|
22148
|
-
res
|
|
22428
|
+
sendSafeError(res, e);
|
|
22149
22429
|
}
|
|
22150
22430
|
});
|
|
22151
22431
|
app.get("/api/classes/:classId/lessons/:lessonId/progress", (req, res) => {
|
|
@@ -22165,15 +22445,15 @@ function registerRosterRoutes(ctx) {
|
|
|
22165
22445
|
`).all(req.params.lessonId, req.params.lessonId, req.params.classId);
|
|
22166
22446
|
res.json(progress);
|
|
22167
22447
|
} catch (e) {
|
|
22168
|
-
res
|
|
22448
|
+
sendSafeError(res, e);
|
|
22169
22449
|
}
|
|
22170
22450
|
});
|
|
22171
|
-
app.delete("/api/classes/:classId/students/:studentId", (req, res) => {
|
|
22451
|
+
app.delete("/api/classes/:classId/students/:studentId", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22172
22452
|
try {
|
|
22173
22453
|
kernelContainer.db.prepare("DELETE FROM class_students WHERE class_id = ? AND student_id = ?").run(req.params.classId, req.params.studentId);
|
|
22174
22454
|
res.json({ success: true });
|
|
22175
22455
|
} catch (e) {
|
|
22176
|
-
res
|
|
22456
|
+
sendSafeError(res, e);
|
|
22177
22457
|
}
|
|
22178
22458
|
});
|
|
22179
22459
|
app.get("/api/classes/:classId/dashboard", (req, res) => {
|
|
@@ -22216,11 +22496,16 @@ function registerRosterRoutes(ctx) {
|
|
|
22216
22496
|
`).all(req.params.classId, req.params.classId);
|
|
22217
22497
|
res.json({ assignments, recentSubmissions, performance: performance2, rollcallStats });
|
|
22218
22498
|
} catch (e) {
|
|
22219
|
-
res
|
|
22499
|
+
sendSafeError(res, e);
|
|
22220
22500
|
}
|
|
22221
22501
|
});
|
|
22222
|
-
app.get("/api/students/:id/dashboard", (req, res) => {
|
|
22502
|
+
app.get("/api/students/:id/dashboard", requireAuth(), (req, res) => {
|
|
22223
22503
|
try {
|
|
22504
|
+
const session = req.session;
|
|
22505
|
+
const isPrivileged = session && (session.role === "teacher" || session.role === "administrator");
|
|
22506
|
+
if (!isPrivileged && session?.userId !== req.params.id) {
|
|
22507
|
+
return res.status(403).json({ error: "Cannot access another student dashboard" });
|
|
22508
|
+
}
|
|
22224
22509
|
const studentId = req.params.id;
|
|
22225
22510
|
const studentClasses = kernelContainer.db.prepare(`
|
|
22226
22511
|
SELECT c.*
|
|
@@ -22295,7 +22580,7 @@ function registerRosterRoutes(ctx) {
|
|
|
22295
22580
|
`).get(studentId);
|
|
22296
22581
|
res.json({ classes: studentClasses, schedules, assignments, progress, rollcalls, profile });
|
|
22297
22582
|
} catch (e) {
|
|
22298
|
-
res
|
|
22583
|
+
sendSafeError(res, e);
|
|
22299
22584
|
}
|
|
22300
22585
|
});
|
|
22301
22586
|
}
|
|
@@ -22303,30 +22588,16 @@ function registerRosterRoutes(ctx) {
|
|
|
22303
22588
|
// server/routes/assignments.ts
|
|
22304
22589
|
var import_genai4 = require("@google/genai");
|
|
22305
22590
|
function registerAssignmentsRoutes(ctx) {
|
|
22306
|
-
const {
|
|
22307
|
-
app,
|
|
22308
|
-
io,
|
|
22309
|
-
loginLimiter,
|
|
22310
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
22311
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
22312
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
22313
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
22314
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
22315
|
-
buildOpenAITools: buildOpenAITools2,
|
|
22316
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
22317
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
22318
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
22319
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
22320
|
-
} = ctx;
|
|
22591
|
+
const { app } = ctx;
|
|
22321
22592
|
app.get("/api/classes/:classId/assignments", (req, res) => {
|
|
22322
22593
|
try {
|
|
22323
22594
|
const assignments = kernelContainer.db.prepare("SELECT * FROM assignments WHERE class_id = ? ORDER BY created_at DESC").all(req.params.classId);
|
|
22324
22595
|
res.json(assignments);
|
|
22325
22596
|
} catch (e) {
|
|
22326
|
-
res
|
|
22597
|
+
sendSafeError(res, e);
|
|
22327
22598
|
}
|
|
22328
22599
|
});
|
|
22329
|
-
app.post("/api/classes/:classId/assignments/generate", async (req, res) => {
|
|
22600
|
+
app.post("/api/classes/:classId/assignments/generate", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
22330
22601
|
try {
|
|
22331
22602
|
const { topic, lessonId } = req.body;
|
|
22332
22603
|
const ai = new import_genai4.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
@@ -22351,10 +22622,10 @@ function registerAssignmentsRoutes(ctx) {
|
|
|
22351
22622
|
);
|
|
22352
22623
|
res.json({ success: true, assignment: { id, class_id: req.params.classId, lesson_id: lessonId || null, title: gen.title, description: gen.description, content: gen.content } });
|
|
22353
22624
|
} catch (e) {
|
|
22354
|
-
res
|
|
22625
|
+
sendSafeError(res, e);
|
|
22355
22626
|
}
|
|
22356
22627
|
});
|
|
22357
|
-
app.post("/api/classes/:classId/assignments/suggest", async (req, res) => {
|
|
22628
|
+
app.post("/api/classes/:classId/assignments/suggest", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
22358
22629
|
try {
|
|
22359
22630
|
const { lessonId } = req.body;
|
|
22360
22631
|
const lesson = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(lessonId);
|
|
@@ -22418,10 +22689,10 @@ Generate the response in the specified JSON schema.`;
|
|
|
22418
22689
|
const text = response.text || "{}";
|
|
22419
22690
|
res.json(JSON.parse(text));
|
|
22420
22691
|
} catch (e) {
|
|
22421
|
-
res
|
|
22692
|
+
sendSafeError(res, e);
|
|
22422
22693
|
}
|
|
22423
22694
|
});
|
|
22424
|
-
app.post("/api/classes/:classId/assignments/create-suggested-quiz", async (req, res) => {
|
|
22695
|
+
app.post("/api/classes/:classId/assignments/create-suggested-quiz", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
22425
22696
|
try {
|
|
22426
22697
|
const { title, description, questions, learningObjectives, timeLimit, lessonId } = req.body;
|
|
22427
22698
|
const id = "ast-" + Date.now().toString(36);
|
|
@@ -22442,12 +22713,17 @@ Generate the response in the specified JSON schema.`;
|
|
|
22442
22713
|
);
|
|
22443
22714
|
res.json({ success: true, assignmentId: id });
|
|
22444
22715
|
} catch (e) {
|
|
22445
|
-
res
|
|
22716
|
+
sendSafeError(res, e);
|
|
22446
22717
|
}
|
|
22447
22718
|
});
|
|
22448
|
-
app.post("/api/assignments/:id/submissions", (req, res) => {
|
|
22719
|
+
app.post("/api/assignments/:id/submissions", requireAuth(), (req, res) => {
|
|
22449
22720
|
try {
|
|
22450
22721
|
const { studentId, content } = req.body;
|
|
22722
|
+
const session = req.session;
|
|
22723
|
+
const isPrivileged = session && (session.role === "teacher" || session.role === "administrator");
|
|
22724
|
+
if (!isPrivileged && session?.userId !== studentId) {
|
|
22725
|
+
return res.status(403).json({ error: "Cannot submit assignment on behalf of another student" });
|
|
22726
|
+
}
|
|
22451
22727
|
kernelContainer.db.prepare(`
|
|
22452
22728
|
INSERT INTO assignment_submissions (assignment_id, student_id, content, submitted_at, status)
|
|
22453
22729
|
VALUES (?, ?, ?, ?, 'submitted')
|
|
@@ -22455,7 +22731,7 @@ Generate the response in the specified JSON schema.`;
|
|
|
22455
22731
|
`).run(req.params.id, studentId, content, Date.now());
|
|
22456
22732
|
res.json({ success: true });
|
|
22457
22733
|
} catch (e) {
|
|
22458
|
-
res
|
|
22734
|
+
sendSafeError(res, e);
|
|
22459
22735
|
}
|
|
22460
22736
|
});
|
|
22461
22737
|
app.get("/api/assignments/:id/submissions", (req, res) => {
|
|
@@ -22469,10 +22745,10 @@ Generate the response in the specified JSON schema.`;
|
|
|
22469
22745
|
`).all(req.params.id);
|
|
22470
22746
|
res.json(submissions);
|
|
22471
22747
|
} catch (e) {
|
|
22472
|
-
res
|
|
22748
|
+
sendSafeError(res, e);
|
|
22473
22749
|
}
|
|
22474
22750
|
});
|
|
22475
|
-
app.post("/api/assignments/:id/submissions/:studentId/grade", async (req, res) => {
|
|
22751
|
+
app.post("/api/assignments/:id/submissions/:studentId/grade", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
22476
22752
|
try {
|
|
22477
22753
|
const asb = kernelContainer.db.prepare("SELECT * FROM assignment_submissions WHERE assignment_id = ? AND student_id = ?").get(req.params.id, req.params.studentId);
|
|
22478
22754
|
const ast = kernelContainer.db.prepare("SELECT * FROM assignments WHERE id = ?").get(req.params.id);
|
|
@@ -22554,7 +22830,7 @@ Provide a grade score (0-100) and brief feedback. Ensure you output in this exac
|
|
|
22554
22830
|
});
|
|
22555
22831
|
res.json({ success: true, pendingApproval: true, message: "Grade generated and sent for approval.", score: grade.score, feedback: grade.feedback });
|
|
22556
22832
|
} catch (e) {
|
|
22557
|
-
res
|
|
22833
|
+
sendSafeError(res, e);
|
|
22558
22834
|
}
|
|
22559
22835
|
});
|
|
22560
22836
|
}
|
|
@@ -22562,21 +22838,7 @@ Provide a grade score (0-100) and brief feedback. Ensure you output in this exac
|
|
|
22562
22838
|
// server/routes/schedules.ts
|
|
22563
22839
|
var import_genai5 = require("@google/genai");
|
|
22564
22840
|
function registerSchedulesRoutes(ctx) {
|
|
22565
|
-
const {
|
|
22566
|
-
app,
|
|
22567
|
-
io,
|
|
22568
|
-
loginLimiter,
|
|
22569
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
22570
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
22571
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
22572
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
22573
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
22574
|
-
buildOpenAITools: buildOpenAITools2,
|
|
22575
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
22576
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
22577
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
22578
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
22579
|
-
} = ctx;
|
|
22841
|
+
const { app } = ctx;
|
|
22580
22842
|
app.get("/api/schedules/today", (req, res) => {
|
|
22581
22843
|
try {
|
|
22582
22844
|
const clientDate = req.query.date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -22631,7 +22893,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
22631
22893
|
res.status(500).json({ error: e.message });
|
|
22632
22894
|
}
|
|
22633
22895
|
});
|
|
22634
|
-
app.post("/api/classes/:classId/schedules", (req, res) => {
|
|
22896
|
+
app.post("/api/classes/:classId/schedules", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22635
22897
|
try {
|
|
22636
22898
|
const { lessonId, scheduledDate, timeSlot, status, notes } = req.body;
|
|
22637
22899
|
const id = "sch-" + Date.now().toString(36);
|
|
@@ -22664,7 +22926,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
22664
22926
|
res.status(500).json({ error: e.message });
|
|
22665
22927
|
}
|
|
22666
22928
|
});
|
|
22667
|
-
app.put("/api/classes/:classId/schedules/:scheduleId", (req, res) => {
|
|
22929
|
+
app.put("/api/classes/:classId/schedules/:scheduleId", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22668
22930
|
try {
|
|
22669
22931
|
const { lessonId, scheduledDate, timeSlot, status, notes } = req.body;
|
|
22670
22932
|
kernelContainer.db.prepare(`
|
|
@@ -22685,7 +22947,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
22685
22947
|
res.status(500).json({ error: e.message });
|
|
22686
22948
|
}
|
|
22687
22949
|
});
|
|
22688
|
-
app.delete("/api/classes/:classId/schedules/:scheduleId", (req, res) => {
|
|
22950
|
+
app.delete("/api/classes/:classId/schedules/:scheduleId", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22689
22951
|
try {
|
|
22690
22952
|
kernelContainer.db.prepare("DELETE FROM schedules WHERE id = ? AND class_id = ?").run(req.params.scheduleId, req.params.classId);
|
|
22691
22953
|
kernelContainer.db.prepare("DELETE FROM attendance WHERE schedule_id = ?").run(req.params.scheduleId);
|
|
@@ -22694,7 +22956,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
22694
22956
|
res.status(500).json({ error: e.message });
|
|
22695
22957
|
}
|
|
22696
22958
|
});
|
|
22697
|
-
app.post("/api/classes/:classId/schedules/batch", (req, res) => {
|
|
22959
|
+
app.post("/api/classes/:classId/schedules/batch", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22698
22960
|
try {
|
|
22699
22961
|
const { schedules } = req.body;
|
|
22700
22962
|
const db2 = kernelContainer.db;
|
|
@@ -22723,7 +22985,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
22723
22985
|
res.status(500).json({ error: e.message });
|
|
22724
22986
|
}
|
|
22725
22987
|
});
|
|
22726
|
-
app.post("/api/timetable/ocr", async (req, res) => {
|
|
22988
|
+
app.post("/api/timetable/ocr", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
22727
22989
|
const startTime = Date.now();
|
|
22728
22990
|
console.log(`[OCR Start] Starting timetable OCR. Payload size: ${req.body.imageBase64?.length || 0} bytes. Lang: ${req.body.lang || "zh"}`);
|
|
22729
22991
|
try {
|
|
@@ -22891,24 +23153,10 @@ function registerSchedulesRoutes(ctx) {
|
|
|
22891
23153
|
}
|
|
22892
23154
|
|
|
22893
23155
|
// server/routes/grading.ts
|
|
22894
|
-
init_interfaces();
|
|
22895
23156
|
var import_genai6 = require("@google/genai");
|
|
23157
|
+
init_interfaces();
|
|
22896
23158
|
function registerGradingRoutes(ctx) {
|
|
22897
|
-
const {
|
|
22898
|
-
app,
|
|
22899
|
-
io,
|
|
22900
|
-
loginLimiter,
|
|
22901
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
22902
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
22903
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
22904
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
22905
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
22906
|
-
buildOpenAITools: buildOpenAITools2,
|
|
22907
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
22908
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
22909
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
22910
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
22911
|
-
} = ctx;
|
|
23159
|
+
const { app } = ctx;
|
|
22912
23160
|
app.get("/api/classes/:classId/attendance-summary", (req, res) => {
|
|
22913
23161
|
try {
|
|
22914
23162
|
const classId = req.params.classId;
|
|
@@ -23022,7 +23270,7 @@ function registerGradingRoutes(ctx) {
|
|
|
23022
23270
|
res.status(500).json({ error: e.message });
|
|
23023
23271
|
}
|
|
23024
23272
|
});
|
|
23025
|
-
app.post("/api/schedules/:scheduleId/attendance", (req, res) => {
|
|
23273
|
+
app.post("/api/schedules/:scheduleId/attendance", requireAuth("teacher", "administrator"), (req, res) => {
|
|
23026
23274
|
try {
|
|
23027
23275
|
const { studentId, status } = req.body;
|
|
23028
23276
|
kernelContainer.db.prepare(`
|
|
@@ -23052,7 +23300,7 @@ function registerGradingRoutes(ctx) {
|
|
|
23052
23300
|
res.status(500).json({ error: e.message });
|
|
23053
23301
|
}
|
|
23054
23302
|
});
|
|
23055
|
-
app.post("/api/classes/:classId/grade-weights", (req, res) => {
|
|
23303
|
+
app.post("/api/classes/:classId/grade-weights", requireAuth("teacher", "administrator"), (req, res) => {
|
|
23056
23304
|
try {
|
|
23057
23305
|
const { attendance_weight, progress_weight, assignment_weight, exam_weight } = req.body;
|
|
23058
23306
|
const total = Number(attendance_weight) + Number(progress_weight) + Number(assignment_weight) + Number(exam_weight);
|
|
@@ -23086,7 +23334,7 @@ function registerGradingRoutes(ctx) {
|
|
|
23086
23334
|
res.status(500).json({ error: e.message });
|
|
23087
23335
|
}
|
|
23088
23336
|
});
|
|
23089
|
-
app.post("/api/classes/:classId/exams", (req, res) => {
|
|
23337
|
+
app.post("/api/classes/:classId/exams", requireAuth("teacher", "administrator"), (req, res) => {
|
|
23090
23338
|
try {
|
|
23091
23339
|
const { title, description, max_score } = req.body;
|
|
23092
23340
|
if (!title) return res.status(400).json({ error: "Title is required" });
|
|
@@ -23108,7 +23356,7 @@ function registerGradingRoutes(ctx) {
|
|
|
23108
23356
|
res.status(500).json({ error: e.message });
|
|
23109
23357
|
}
|
|
23110
23358
|
});
|
|
23111
|
-
app.post("/api/exams/:examId/scores", (req, res) => {
|
|
23359
|
+
app.post("/api/exams/:examId/scores", requireAuth("teacher", "administrator"), (req, res) => {
|
|
23112
23360
|
try {
|
|
23113
23361
|
const { scores } = req.body;
|
|
23114
23362
|
if (!Array.isArray(scores)) return res.status(400).json({ error: "Scores array is required" });
|
|
@@ -23264,11 +23512,8 @@ function registerGradingRoutes(ctx) {
|
|
|
23264
23512
|
res.status(500).json({ error: e.message });
|
|
23265
23513
|
}
|
|
23266
23514
|
});
|
|
23267
|
-
app.post("/api/grade-sync", async (req, res) => {
|
|
23515
|
+
app.post("/api/grade-sync", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
23268
23516
|
try {
|
|
23269
|
-
if (!checkIsTeacherOrAdmin(req)) {
|
|
23270
|
-
return res.status(403).json({ success: false, error: "Access Denied: Teachers or Administrators only" });
|
|
23271
|
-
}
|
|
23272
23517
|
const { lessonId, studentId, grade } = req.body;
|
|
23273
23518
|
if (!lessonId || !studentId || grade === void 0) {
|
|
23274
23519
|
return res.status(400).json({ success: false, error: "lessonId, studentId, and grade are required" });
|
|
@@ -23289,11 +23534,8 @@ function registerGradingRoutes(ctx) {
|
|
|
23289
23534
|
res.status(500).json({ success: false, error: e.message });
|
|
23290
23535
|
}
|
|
23291
23536
|
});
|
|
23292
|
-
app.post("/api/students/:studentId/points", async (req, res) => {
|
|
23537
|
+
app.post("/api/students/:studentId/points", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
23293
23538
|
try {
|
|
23294
|
-
if (!checkIsTeacherOrAdmin(req)) {
|
|
23295
|
-
return res.status(403).json({ success: false, error: "Access Denied: Teachers or Administrators only" });
|
|
23296
|
-
}
|
|
23297
23539
|
const { studentId } = req.params;
|
|
23298
23540
|
const { classId, dimensionId, deltaPoints, reason, pluginId } = req.body;
|
|
23299
23541
|
if (!classId || !dimensionId || deltaPoints === void 0 || !reason) {
|
|
@@ -23325,7 +23567,7 @@ function registerGradingRoutes(ctx) {
|
|
|
23325
23567
|
res.status(500).json({ success: false, error: e.message });
|
|
23326
23568
|
}
|
|
23327
23569
|
});
|
|
23328
|
-
app.post("/api/classes/:classId/semester-reports/archive", (req, res) => {
|
|
23570
|
+
app.post("/api/classes/:classId/semester-reports/archive", requireAuth("teacher", "administrator"), (req, res) => {
|
|
23329
23571
|
try {
|
|
23330
23572
|
const { semesterName, reports } = req.body;
|
|
23331
23573
|
if (!semesterName) return res.status(400).json({ error: "semesterName is required" });
|
|
@@ -23378,7 +23620,7 @@ function registerGradingRoutes(ctx) {
|
|
|
23378
23620
|
res.status(500).json({ error: e.message });
|
|
23379
23621
|
}
|
|
23380
23622
|
});
|
|
23381
|
-
app.post("/api/classes/:classId/students/:studentId/semester-ai-evaluation", async (req, res) => {
|
|
23623
|
+
app.post("/api/classes/:classId/students/:studentId/semester-ai-evaluation", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
23382
23624
|
try {
|
|
23383
23625
|
const { classId, studentId } = req.params;
|
|
23384
23626
|
const { semesterName = "2026\u5E74\u6625\u5B63\u5B66\uFFFD?", providerId } = req.body;
|
|
@@ -23478,8 +23720,8 @@ ${examsText}
|
|
|
23478
23720
|
|
|
23479
23721
|
// server/routes/plugins.ts
|
|
23480
23722
|
var import_express2 = __toESM(require("express"), 1);
|
|
23481
|
-
var
|
|
23482
|
-
var
|
|
23723
|
+
var import_path14 = __toESM(require("path"), 1);
|
|
23724
|
+
var import_fs13 = __toESM(require("fs"), 1);
|
|
23483
23725
|
|
|
23484
23726
|
// server/services/version-fetcher.ts
|
|
23485
23727
|
var import_child_process2 = require("child_process");
|
|
@@ -23661,13 +23903,6 @@ function isSafeExternalUrl(urlStr) {
|
|
|
23661
23903
|
function registerPluginsRoutes(ctx) {
|
|
23662
23904
|
const {
|
|
23663
23905
|
app,
|
|
23664
|
-
io,
|
|
23665
|
-
loginLimiter,
|
|
23666
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
23667
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
23668
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
23669
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
23670
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
23671
23906
|
buildOpenAITools: buildOpenAITools2,
|
|
23672
23907
|
executeAgentToolCall: executeAgentToolCall2,
|
|
23673
23908
|
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
@@ -23676,29 +23911,26 @@ function registerPluginsRoutes(ctx) {
|
|
|
23676
23911
|
} = ctx;
|
|
23677
23912
|
app.get("/api/docs/plugin-guide", (req, res) => {
|
|
23678
23913
|
try {
|
|
23679
|
-
const docPath =
|
|
23680
|
-
if (!
|
|
23914
|
+
const docPath = import_path14.default.join(process.cwd(), "docs_plugin_guide.md");
|
|
23915
|
+
if (!import_fs13.default.existsSync(docPath)) {
|
|
23681
23916
|
return res.status(404).json({ success: false, error: "Document not found" });
|
|
23682
23917
|
}
|
|
23683
|
-
const content =
|
|
23918
|
+
const content = import_fs13.default.readFileSync(docPath, "utf-8");
|
|
23684
23919
|
res.json({ success: true, content });
|
|
23685
23920
|
} catch (e) {
|
|
23686
|
-
res
|
|
23921
|
+
sendSafeError(res, e);
|
|
23687
23922
|
}
|
|
23688
23923
|
});
|
|
23689
|
-
app.get("/api/admin/logs", (req, res) => {
|
|
23690
|
-
if (!checkIsTeacherOrAdmin(req)) {
|
|
23691
|
-
return res.status(403).json({ success: false, error: "Access denied: teachers or admins only" });
|
|
23692
|
-
}
|
|
23924
|
+
app.get("/api/admin/logs", requireAuth("administrator"), (req, res) => {
|
|
23693
23925
|
try {
|
|
23694
23926
|
const limit = parseInt(req.query.limit) || 200;
|
|
23695
23927
|
const component = req.query.component;
|
|
23696
23928
|
const levelFilter = req.query.level;
|
|
23697
|
-
const logFile2 =
|
|
23698
|
-
if (!
|
|
23929
|
+
const logFile2 = import_path14.default.resolve(process.cwd(), "logs", "openlearn.log");
|
|
23930
|
+
if (!import_fs13.default.existsSync(logFile2)) {
|
|
23699
23931
|
return res.json({ success: true, logs: [] });
|
|
23700
23932
|
}
|
|
23701
|
-
const content =
|
|
23933
|
+
const content = import_fs13.default.readFileSync(logFile2, "utf-8");
|
|
23702
23934
|
const lines = content.split("\n").filter(Boolean);
|
|
23703
23935
|
let parsedLogs = lines.map((line) => {
|
|
23704
23936
|
try {
|
|
@@ -23729,7 +23961,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23729
23961
|
const sliceStart = Math.max(0, parsedLogs.length - limit);
|
|
23730
23962
|
res.json({ success: true, logs: parsedLogs.slice(sliceStart) });
|
|
23731
23963
|
} catch (e) {
|
|
23732
|
-
res
|
|
23964
|
+
sendSafeError(res, e);
|
|
23733
23965
|
}
|
|
23734
23966
|
});
|
|
23735
23967
|
app.get("/api/plugins", (req, res) => {
|
|
@@ -23754,7 +23986,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23754
23986
|
isSystem: manifestId.startsWith("@openlearn/") || found.pluginId.startsWith("@openlearn/")
|
|
23755
23987
|
});
|
|
23756
23988
|
} catch (e) {
|
|
23757
|
-
res
|
|
23989
|
+
sendSafeError(res, e);
|
|
23758
23990
|
}
|
|
23759
23991
|
});
|
|
23760
23992
|
app.get("/api/plugins/market", async (req, res) => {
|
|
@@ -23780,7 +24012,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23780
24012
|
}
|
|
23781
24013
|
res.json({ success: true, market: results });
|
|
23782
24014
|
} catch (e) {
|
|
23783
|
-
res
|
|
24015
|
+
sendSafeError(res, e);
|
|
23784
24016
|
}
|
|
23785
24017
|
});
|
|
23786
24018
|
app.post("/api/plugins/:id(*)/check-update", async (req, res) => {
|
|
@@ -23802,7 +24034,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23802
24034
|
const result = await checkVersion(src, manifest.version || "0.0.0");
|
|
23803
24035
|
res.json({ success: true, ...result });
|
|
23804
24036
|
} catch (e) {
|
|
23805
|
-
res
|
|
24037
|
+
sendSafeError(res, e);
|
|
23806
24038
|
}
|
|
23807
24039
|
});
|
|
23808
24040
|
app.post("/api/plugins/:id(*)/one-click-update", requireAuth("administrator"), async (req, res) => {
|
|
@@ -23841,9 +24073,9 @@ function registerPluginsRoutes(ctx) {
|
|
|
23841
24073
|
});
|
|
23842
24074
|
}
|
|
23843
24075
|
} else {
|
|
23844
|
-
const zipPath =
|
|
23845
|
-
if (
|
|
23846
|
-
zipBuffer =
|
|
24076
|
+
const zipPath = import_path14.default.resolve(process.cwd(), "v2_plugins/research-workflow/aymwoo-plugin-research-workflow.zip");
|
|
24077
|
+
if (import_fs13.default.existsSync(zipPath)) {
|
|
24078
|
+
zipBuffer = import_fs13.default.readFileSync(zipPath);
|
|
23847
24079
|
} else {
|
|
23848
24080
|
return res.status(404).json({ success: false, error: "\u672A\u627E\u5230\u66F4\u65B0\u5B89\u88C5\u5305" });
|
|
23849
24081
|
}
|
|
@@ -23863,7 +24095,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23863
24095
|
});
|
|
23864
24096
|
} catch (err) {
|
|
23865
24097
|
console.error(err);
|
|
23866
|
-
res
|
|
24098
|
+
sendSafeError(res, err);
|
|
23867
24099
|
}
|
|
23868
24100
|
});
|
|
23869
24101
|
app.get("/api/plugins/:id(*)/contributions", (req, res) => {
|
|
@@ -23872,7 +24104,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23872
24104
|
const summary = kernelContainer.pluginHost.listContributions(rawId);
|
|
23873
24105
|
res.json({ success: true, result: summary });
|
|
23874
24106
|
} catch (e) {
|
|
23875
|
-
res
|
|
24107
|
+
sendSafeError(res, e);
|
|
23876
24108
|
}
|
|
23877
24109
|
});
|
|
23878
24110
|
app.get("/api/plugins/:id(*)/config", (req, res) => {
|
|
@@ -23892,7 +24124,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23892
24124
|
}
|
|
23893
24125
|
});
|
|
23894
24126
|
} catch (e) {
|
|
23895
|
-
res
|
|
24127
|
+
sendSafeError(res, e);
|
|
23896
24128
|
}
|
|
23897
24129
|
});
|
|
23898
24130
|
app.post("/api/plugins/:id(*)/config", requireAuth("administrator"), (req, res) => {
|
|
@@ -23911,7 +24143,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23911
24143
|
kernelContainer.pluginHost.setPluginConfig(pluginId, manifest, updates);
|
|
23912
24144
|
res.json({ success: true });
|
|
23913
24145
|
} catch (e) {
|
|
23914
|
-
res
|
|
24146
|
+
sendSafeError(res, e);
|
|
23915
24147
|
}
|
|
23916
24148
|
});
|
|
23917
24149
|
app.post("/api/plugins/:id(*)/toggle", requireAuth("administrator"), async (req, res) => {
|
|
@@ -23925,7 +24157,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23925
24157
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23926
24158
|
res.json(result);
|
|
23927
24159
|
} catch (err) {
|
|
23928
|
-
res
|
|
24160
|
+
sendSafeError(res, err);
|
|
23929
24161
|
}
|
|
23930
24162
|
});
|
|
23931
24163
|
app.delete("/api/plugins/:id(*)", requireAuth("administrator"), async (req, res) => {
|
|
@@ -23939,7 +24171,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23939
24171
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23940
24172
|
res.json(result);
|
|
23941
24173
|
} catch (err) {
|
|
23942
|
-
res
|
|
24174
|
+
sendSafeError(res, err);
|
|
23943
24175
|
}
|
|
23944
24176
|
});
|
|
23945
24177
|
app.get("/api/plugins/:id(*)", async (req, res) => {
|
|
@@ -23968,7 +24200,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23968
24200
|
res.json(result);
|
|
23969
24201
|
} catch (err) {
|
|
23970
24202
|
console.error(err);
|
|
23971
|
-
res
|
|
24203
|
+
sendSafeError(res, err);
|
|
23972
24204
|
}
|
|
23973
24205
|
});
|
|
23974
24206
|
app.post("/api/plugins/upload-zip", requireAuth("administrator"), async (req, res) => {
|
|
@@ -23983,7 +24215,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23983
24215
|
res.json(result);
|
|
23984
24216
|
} catch (err) {
|
|
23985
24217
|
console.error(err);
|
|
23986
|
-
res
|
|
24218
|
+
sendSafeError(res, err);
|
|
23987
24219
|
}
|
|
23988
24220
|
});
|
|
23989
24221
|
app.post("/api/plugins/upload-zip-raw", requireAuth("administrator"), import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }), async (req, res) => {
|
|
@@ -24025,7 +24257,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24025
24257
|
});
|
|
24026
24258
|
} catch (err) {
|
|
24027
24259
|
console.error(err);
|
|
24028
|
-
res
|
|
24260
|
+
sendSafeError(res, err);
|
|
24029
24261
|
}
|
|
24030
24262
|
});
|
|
24031
24263
|
app.post(
|
|
@@ -24058,7 +24290,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24058
24290
|
});
|
|
24059
24291
|
} catch (err) {
|
|
24060
24292
|
console.error(err);
|
|
24061
|
-
res
|
|
24293
|
+
sendSafeError(res, err);
|
|
24062
24294
|
}
|
|
24063
24295
|
}
|
|
24064
24296
|
);
|
|
@@ -24102,10 +24334,10 @@ function registerPluginsRoutes(ctx) {
|
|
|
24102
24334
|
res.json({ success: true, result });
|
|
24103
24335
|
} catch (err) {
|
|
24104
24336
|
console.error("[execute-command]", err.message);
|
|
24105
|
-
res
|
|
24337
|
+
sendSafeError(res, err);
|
|
24106
24338
|
}
|
|
24107
24339
|
});
|
|
24108
|
-
app.get("/api/ai-providers", (req, res) => {
|
|
24340
|
+
app.get("/api/ai-providers", requireAuth("teacher", "administrator"), (req, res) => {
|
|
24109
24341
|
try {
|
|
24110
24342
|
const providers = kernelContainer.db.prepare("SELECT * FROM ai_providers ORDER BY created_at DESC").all();
|
|
24111
24343
|
const masked = providers.map((p) => ({
|
|
@@ -24114,7 +24346,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24114
24346
|
}));
|
|
24115
24347
|
res.json(masked);
|
|
24116
24348
|
} catch (e) {
|
|
24117
|
-
res
|
|
24349
|
+
sendSafeError(res, e);
|
|
24118
24350
|
}
|
|
24119
24351
|
});
|
|
24120
24352
|
app.post("/api/ai-providers", requireAuth("administrator"), (req, res) => {
|
|
@@ -24129,7 +24361,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24129
24361
|
kernelContainer.db.prepare("INSERT INTO ai_providers (id, name, api_url, api_key, model_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, name, api_url, encryptedKey, model_name, now, now);
|
|
24130
24362
|
res.json({ success: true, id });
|
|
24131
24363
|
} catch (e) {
|
|
24132
|
-
res
|
|
24364
|
+
sendSafeError(res, e);
|
|
24133
24365
|
}
|
|
24134
24366
|
});
|
|
24135
24367
|
app.put("/api/ai-providers/:id", requireAuth("administrator"), (req, res) => {
|
|
@@ -24149,7 +24381,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24149
24381
|
kernelContainer.db.prepare("UPDATE ai_providers SET name = ?, api_url = ?, api_key = ?, model_name = ?, updated_at = ? WHERE id = ?").run(name, api_url, finalKey, model_name, now, req.params.id);
|
|
24150
24382
|
res.json({ success: true });
|
|
24151
24383
|
} catch (e) {
|
|
24152
|
-
res
|
|
24384
|
+
sendSafeError(res, e);
|
|
24153
24385
|
}
|
|
24154
24386
|
});
|
|
24155
24387
|
app.delete("/api/ai-providers/:id", requireAuth("administrator"), (req, res) => {
|
|
@@ -24157,7 +24389,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24157
24389
|
kernelContainer.db.prepare("DELETE FROM ai_providers WHERE id = ?").run(req.params.id);
|
|
24158
24390
|
res.json({ success: true });
|
|
24159
24391
|
} catch (e) {
|
|
24160
|
-
res
|
|
24392
|
+
sendSafeError(res, e);
|
|
24161
24393
|
}
|
|
24162
24394
|
});
|
|
24163
24395
|
app.get("/api/site-settings", (req, res) => {
|
|
@@ -24169,7 +24401,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24169
24401
|
logoUrl: row?.logo_url || null
|
|
24170
24402
|
});
|
|
24171
24403
|
} catch (e) {
|
|
24172
|
-
res
|
|
24404
|
+
sendSafeError(res, e);
|
|
24173
24405
|
}
|
|
24174
24406
|
});
|
|
24175
24407
|
app.put("/api/site-settings", requireAuth("administrator"), (req, res) => {
|
|
@@ -24184,7 +24416,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24184
24416
|
siteInfo: { siteName: siteName || "", slogan: slogan || "", logoUrl: logoUrl || null }
|
|
24185
24417
|
});
|
|
24186
24418
|
} catch (e) {
|
|
24187
|
-
res
|
|
24419
|
+
sendSafeError(res, e);
|
|
24188
24420
|
}
|
|
24189
24421
|
});
|
|
24190
24422
|
app.post("/api/ai-providers/test", requireAuth("administrator"), async (req, res) => {
|
|
@@ -24233,7 +24465,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24233
24465
|
res.status(response.status).json({ success: false, error: `API responded with status ${response.status}: ${responseText.slice(0, 200)}` });
|
|
24234
24466
|
}
|
|
24235
24467
|
} catch (e) {
|
|
24236
|
-
res
|
|
24468
|
+
sendSafeError(res, e, 500, "Connection failed");
|
|
24237
24469
|
}
|
|
24238
24470
|
});
|
|
24239
24471
|
}
|
|
@@ -24253,6 +24485,15 @@ async function startServer() {
|
|
|
24253
24485
|
environment: process.env.NODE_ENV || "development",
|
|
24254
24486
|
config: { port: Number(process.env.PORT) || 9e3 }
|
|
24255
24487
|
});
|
|
24488
|
+
try {
|
|
24489
|
+
const migrationsDir = import_path15.default.join(process.cwd(), "migrations");
|
|
24490
|
+
const migrations = loadMigrationsFromDirectory(migrationsDir);
|
|
24491
|
+
if (migrations.length > 0) {
|
|
24492
|
+
runMigrations(kernelContainer.db, migrations);
|
|
24493
|
+
}
|
|
24494
|
+
} catch (err) {
|
|
24495
|
+
console.error("[Migration] Failed to run database migrations:", err);
|
|
24496
|
+
}
|
|
24256
24497
|
await runStartupMigrations(kernelContainer.db);
|
|
24257
24498
|
await kernelContainer.ready;
|
|
24258
24499
|
const activityRegistry = new ActivityRegistry2();
|
|
@@ -24263,23 +24504,27 @@ async function startServer() {
|
|
|
24263
24504
|
kernelContainer.pluginHost.setExpressApp(app);
|
|
24264
24505
|
const PORT = parseInt(process.env.PORT || "9000", 10);
|
|
24265
24506
|
app.set("trust proxy", 1);
|
|
24507
|
+
const frameAllowedOrigins = process.env.ALLOWED_FRAME_ORIGINS ? process.env.ALLOWED_FRAME_ORIGINS.split(",").map((s) => s.trim()) : ["http://localhost:*", "http://127.0.0.1:*"];
|
|
24266
24508
|
app.use((0, import_helmet.default)({
|
|
24267
24509
|
contentSecurityPolicy: {
|
|
24268
24510
|
directives: {
|
|
24269
24511
|
defaultSrc: ["'self'"],
|
|
24270
|
-
|
|
24271
|
-
|
|
24512
|
+
// 移除通配 https: 与 data:,禁止加载全网任意第三方未授权脚本
|
|
24513
|
+
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "blob:"],
|
|
24514
|
+
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
|
24272
24515
|
imgSrc: ["'self'", "data:", "blob:", "https:"],
|
|
24273
24516
|
connectSrc: ["'self'", "ws:", "wss:", "https:"],
|
|
24274
|
-
fontSrc: ["'self'", "data:", "https
|
|
24275
|
-
|
|
24276
|
-
|
|
24517
|
+
fontSrc: ["'self'", "data:", "https://fonts.gstatic.com"],
|
|
24518
|
+
// 移除通配 http: 和 https:,限制 iframe 仅能加载本地、沙箱或受信任课件源
|
|
24519
|
+
frameSrc: ["'self'", "blob:", "data:", ...frameAllowedOrigins],
|
|
24520
|
+
objectSrc: ["'none'"],
|
|
24521
|
+
baseUri: ["'self'"]
|
|
24277
24522
|
}
|
|
24278
24523
|
},
|
|
24279
24524
|
crossOriginOpenerPolicy: false,
|
|
24280
24525
|
crossOriginEmbedderPolicy: false,
|
|
24281
24526
|
crossOriginResourcePolicy: { policy: "cross-origin" },
|
|
24282
|
-
// 允许沙箱 iframe(opaque origin
|
|
24527
|
+
// 允许沙箱 iframe(opaque origin)加载静态资?
|
|
24283
24528
|
originAgentCluster: false,
|
|
24284
24529
|
strictTransportSecurity: false
|
|
24285
24530
|
// �? HTTP 部署,禁�? HSTS(否则浏览器缓存后强�? HTTPS,导�? ERR_CONNECTION_REFUSED�?
|
|
@@ -24294,8 +24539,8 @@ async function startServer() {
|
|
|
24294
24539
|
});
|
|
24295
24540
|
app.use(import_express3.default.json({ limit: "10mb" }));
|
|
24296
24541
|
app.use(import_express3.default.urlencoded({ limit: "10mb", extended: true }));
|
|
24297
|
-
app.use("/uploads", import_express3.default.static(
|
|
24298
|
-
app.use("/plugins", import_express3.default.static(
|
|
24542
|
+
app.use("/uploads", import_express3.default.static(import_path15.default.join(process.cwd(), "uploads")));
|
|
24543
|
+
app.use("/plugins", import_express3.default.static(import_path15.default.join(process.cwd(), "plugins")));
|
|
24299
24544
|
app.use((req, res, next) => {
|
|
24300
24545
|
const origin = req.headers.origin;
|
|
24301
24546
|
if (origin === "null" || origin === void 0) {
|
|
@@ -24325,14 +24570,58 @@ async function startServer() {
|
|
|
24325
24570
|
activityRegistry
|
|
24326
24571
|
};
|
|
24327
24572
|
const httpServer = (0, import_http.createServer)(app);
|
|
24328
|
-
const allowedOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(",") : process.env.NODE_ENV === "production" ? [] : ["http://localhost:5173", "http://localhost:9000", "http://localhost:4173"];
|
|
24573
|
+
const allowedOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(",").map((s) => s.trim()) : process.env.NODE_ENV === "production" ? [] : ["http://localhost:5173", "http://localhost:9000", "http://localhost:4173"];
|
|
24329
24574
|
const io = new import_socket.Server(httpServer, {
|
|
24330
24575
|
cors: {
|
|
24331
|
-
origin:
|
|
24576
|
+
origin: (origin, callback) => {
|
|
24577
|
+
if (!origin) return callback(null, true);
|
|
24578
|
+
if (allowedOrigins.length === 0) {
|
|
24579
|
+
return callback(new Error("CORS not allowed"));
|
|
24580
|
+
}
|
|
24581
|
+
if (allowedOrigins.includes(origin) || allowedOrigins.includes("*")) {
|
|
24582
|
+
return callback(null, true);
|
|
24583
|
+
}
|
|
24584
|
+
return callback(new Error("Not allowed by CORS"));
|
|
24585
|
+
},
|
|
24332
24586
|
methods: ["GET", "POST"],
|
|
24333
24587
|
credentials: true
|
|
24334
24588
|
}
|
|
24335
24589
|
});
|
|
24590
|
+
io.use((socket, next) => {
|
|
24591
|
+
try {
|
|
24592
|
+
const cookieHeader = socket.handshake.headers.cookie;
|
|
24593
|
+
let token = null;
|
|
24594
|
+
if (cookieHeader) {
|
|
24595
|
+
const parts = cookieHeader.split(";");
|
|
24596
|
+
for (const part of parts) {
|
|
24597
|
+
const trimmed = part.trim();
|
|
24598
|
+
if (trimmed.startsWith("edu_os_token=")) {
|
|
24599
|
+
token = trimmed.substring("edu_os_token=".length);
|
|
24600
|
+
break;
|
|
24601
|
+
}
|
|
24602
|
+
}
|
|
24603
|
+
}
|
|
24604
|
+
if (!token && socket.handshake.auth?.token) {
|
|
24605
|
+
token = socket.handshake.auth.token;
|
|
24606
|
+
}
|
|
24607
|
+
if (process.env.NODE_ENV === "test" && !token) {
|
|
24608
|
+
return next();
|
|
24609
|
+
}
|
|
24610
|
+
if (!token) {
|
|
24611
|
+
return next(new Error("Authentication required: missing edu_os_token"));
|
|
24612
|
+
}
|
|
24613
|
+
const session = getValidSession(token);
|
|
24614
|
+
if (!session) {
|
|
24615
|
+
return next(new Error("Authentication required: session expired or invalid"));
|
|
24616
|
+
}
|
|
24617
|
+
socket.data.session = session;
|
|
24618
|
+
socket.data.userId = session.userId;
|
|
24619
|
+
socket.data.role = session.role;
|
|
24620
|
+
next();
|
|
24621
|
+
} catch (err) {
|
|
24622
|
+
next(new Error(`Authentication error: ${err.message}`));
|
|
24623
|
+
}
|
|
24624
|
+
});
|
|
24336
24625
|
kernelContainer.pluginHost.setSocketIO(io);
|
|
24337
24626
|
ctx.io = io;
|
|
24338
24627
|
registerOsRoutes(ctx);
|
|
@@ -24361,10 +24650,10 @@ async function startServer() {
|
|
|
24361
24650
|
});
|
|
24362
24651
|
app.use(vite.middlewares);
|
|
24363
24652
|
} else {
|
|
24364
|
-
const distPath = __dirname ||
|
|
24653
|
+
const distPath = __dirname || import_path15.default.join(process.cwd(), "dist");
|
|
24365
24654
|
app.use(import_express3.default.static(distPath));
|
|
24366
24655
|
app.get("*", (req, res) => {
|
|
24367
|
-
res.sendFile(
|
|
24656
|
+
res.sendFile(import_path15.default.join(distPath, "index.html"));
|
|
24368
24657
|
});
|
|
24369
24658
|
}
|
|
24370
24659
|
httpServer.on("error", (err) => {
|
|
@@ -24384,8 +24673,8 @@ async function startServer() {
|
|
|
24384
24673
|
const startTime = Date.now();
|
|
24385
24674
|
let platformVersion = "0.0.0";
|
|
24386
24675
|
try {
|
|
24387
|
-
const pkgPath =
|
|
24388
|
-
const pkg = JSON.parse(
|
|
24676
|
+
const pkgPath = import_path15.default.join(process.cwd(), "package.json");
|
|
24677
|
+
const pkg = JSON.parse(import_fs14.default.readFileSync(pkgPath, "utf-8"));
|
|
24389
24678
|
platformVersion = pkg.version ?? platformVersion;
|
|
24390
24679
|
} catch {
|
|
24391
24680
|
}
|
|
@@ -24401,7 +24690,7 @@ async function startServer() {
|
|
|
24401
24690
|
res.status(503).json({ status: "not_ready", error: e.message });
|
|
24402
24691
|
}
|
|
24403
24692
|
});
|
|
24404
|
-
app.get("/metrics", (_req, res) => {
|
|
24693
|
+
app.get("/metrics", requireAuth("administrator"), (_req, res) => {
|
|
24405
24694
|
const mem = process.memoryUsage();
|
|
24406
24695
|
res.json({
|
|
24407
24696
|
uptime: Math.floor((Date.now() - startTime) / 1e3),
|