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