openlearn-next 0.2.8 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/AdminDirectoryView-0XV86Ues.js +1 -0
- package/dist/assets/BatchPickerModal-of9a5EJz.js +1 -0
- package/dist/assets/ClassesView-CefnsjZ8.js +19 -0
- package/dist/assets/CloudDriveModal-DgVQLbO5.js +1 -0
- package/dist/assets/ComputerLabView-BId0ZarM.js +1 -0
- package/dist/assets/CourseManagement-Cxhc784Q.js +1 -0
- package/dist/assets/CourseWizardModal-Da6PKhnU.js +3 -0
- package/dist/assets/CoursewareHubPanel-LpUgjqbH.js +1 -0
- package/dist/assets/{Dashboard-DQ91dQio.js → Dashboard-LWbFbup4.js} +2 -2
- package/dist/assets/ExportWeightModal-Cyg1zMEd.js +1 -0
- package/dist/assets/HelpTour-CHPIK2ae.js +1 -0
- package/dist/assets/{HelpView-DLAKVpwf.js → HelpView-CvpgBpdr.js} +5 -5
- package/dist/assets/ImportLessonsModal-CfWIrW1K.js +1 -0
- package/dist/assets/ImportModal-BTfia0Ah.js +1 -0
- package/dist/assets/InteractiveCoursewareViewer-CS5BqfJZ.js +1 -0
- package/dist/assets/InteractiveWhiteboard-U23CsqGo.js +78 -0
- package/dist/assets/{LazyCourseware-g7b8EXR_.js → LazyCourseware-DXL9IxZO.js} +1 -1
- package/dist/assets/{LazyWhiteboard-B6rDWBFb.js → LazyWhiteboard-CtkzSFtN.js} +2 -2
- package/dist/assets/LessonEditorView-Bfxf4VVB.js +1 -0
- package/dist/assets/LiveClassroomView-DHzCM4qQ.js +1 -0
- package/dist/assets/NotificationDetailModal-BxrpnlWV.js +1 -0
- package/dist/assets/{PluginView-DCY4yi2h.js → PluginView-12qYzmnq.js} +3 -3
- package/dist/assets/{ProcessLogsModal-B-PY780w.js → ProcessLogsModal-01S7zsw0.js} +1 -1
- package/dist/assets/QuizGeneratorModal-DfZI40Ii.js +1 -0
- package/dist/assets/StudentAssignmentView-BP6zbHuM.js +1 -0
- package/dist/assets/StudentLessonView-DEpMzrH2.js +1 -0
- package/dist/assets/StudentPreviewModal-DCmai53s.js +1 -0
- package/dist/assets/StudentView-CcGlfW25.js +2 -0
- package/dist/assets/SystemResourceLibraryModal-BzHw1edT.js +1 -0
- package/dist/assets/TeacherView-Dw6mKwcv.js +2 -0
- package/dist/assets/{TimetableView-BMsjwi-s.js → TimetableView-DJgOlGSf.js} +3 -3
- package/dist/assets/index-B-YxO39A.css +1 -0
- package/dist/assets/index-C8sCKzmN.js +2 -0
- package/dist/assets/index-CYzNsYZB.js +193 -0
- package/dist/assets/{index.es-DZ6UeYE-.js → index.es-CfxBkm_z.js} +1 -1
- package/dist/assets/{vendor-charts-B7ViBL9w.js → vendor-charts-CRXa4AlU.js} +1 -1
- package/dist/assets/vendor-content-CYR4BTuT.js +3 -0
- package/dist/assets/{vendor-icons-_c3xpwjZ.js → vendor-icons-Dq6MhgH9.js} +1 -1
- package/dist/assets/{vendor-pdf-CVyNPGVr.js → vendor-pdf-Cp-cYteE.js} +2 -2
- package/dist/assets/{vendor-pptx-B0hlo0eh.js → vendor-pptx-B_NheIKl.js} +5 -5
- package/dist/assets/vendor-utils-BEhuP0Zq.js +1 -0
- package/dist/index.html +2 -2
- package/dist/server.cjs +1079 -699
- package/package.json +9 -6
- package/dist/assets/AdminDirectoryView-CD7tW4mc.js +0 -1
- package/dist/assets/BatchPickerModal-Bt8Qx0Av.js +0 -1
- package/dist/assets/ClassesView-B1B3wlkx.js +0 -19
- package/dist/assets/CloudDriveModal-BSZaWIex.js +0 -1
- package/dist/assets/ComputerLabView-B6-twB0u.js +0 -1
- package/dist/assets/CourseManagement-Bnezm9Vv.js +0 -1
- package/dist/assets/CourseWizardModal-BvfTvcQT.js +0 -3
- package/dist/assets/CoursewareHubPanel-Cb1mAqHS.js +0 -1
- package/dist/assets/ExportWeightModal-al1iIVxb.js +0 -1
- package/dist/assets/HelpTour-DwM9UBiA.js +0 -1
- package/dist/assets/ImportLessonsModal-DwhvqzY_.js +0 -1
- package/dist/assets/ImportModal-CmUt1YeX.js +0 -1
- package/dist/assets/InteractiveCoursewareViewer-DWnzq20c.js +0 -1
- package/dist/assets/InteractiveWhiteboard-CQ-4meYj.js +0 -102
- package/dist/assets/LessonEditorView-DixwX1Kw.js +0 -1
- package/dist/assets/LiveClassroomView-j93YOxSh.js +0 -1
- package/dist/assets/NotificationDetailModal-CQ9PIqkD.js +0 -1
- package/dist/assets/QuizGeneratorModal-BvTcmD68.js +0 -1
- package/dist/assets/StudentAssignmentView-4R0WxStx.js +0 -1
- package/dist/assets/StudentLessonView-DZ0i9-tX.js +0 -1
- package/dist/assets/StudentPreviewModal-CfsB1dPY.js +0 -1
- package/dist/assets/StudentView-D7GtUiYR.js +0 -2
- package/dist/assets/SystemResourceLibraryModal-Ilnthwpw.js +0 -1
- package/dist/assets/TeacherView-DjnjsQ_3.js +0 -2
- package/dist/assets/index-CATki2cp.css +0 -1
- package/dist/assets/index-D8x6bUeG.js +0 -150
- 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
|
}
|
|
@@ -2605,7 +2651,9 @@ parentPort.on('message', async function(msg) {
|
|
|
2605
2651
|
}
|
|
2606
2652
|
|
|
2607
2653
|
// \u6784\u5EFA\u63D2\u4EF6\u81EA\u5EFA\u8868 API (dbApi)
|
|
2608
|
-
|
|
2654
|
+
// \u8868\u524D\u7F00\u5FC5\u987B\u7528 manifestId\uFF08\u4E0E\u547D\u4EE4\u547D\u540D\u7A7A\u95F4 L525 \u53CA ServiceHost \u7684 DDL \u5B88\u536B\u4E00\u81F4\uFF09\uFF0C
|
|
2655
|
+
// \u5426\u5219 Worker \u63D2\u4EF6\u5728\u81EA\u5DF1\u547D\u540D\u7A7A\u95F4\u5EFA\u8868\u4F1A\u88AB\u8BEF\u5224\u4E3A\u8D8A\u6743 DDL\u3002
|
|
2656
|
+
var tablePrefix = 'plugin_' + (workerData.manifestId || workerData.pluginId).replace(/[^a-zA-Z0-9_]/g, '_') + '_';
|
|
2609
2657
|
var dbService = rawServices['@openlearn/core:IDatabase'];
|
|
2610
2658
|
var dbApi = dbService ? {
|
|
2611
2659
|
ensureTable: function(tableName, schema) {
|
|
@@ -3076,7 +3124,11 @@ var init_worker_manager = __esm({
|
|
|
3076
3124
|
workerData: { pluginId, manifestId: manifest.id, serviceTokens, pluginDir },
|
|
3077
3125
|
eval: false,
|
|
3078
3126
|
stdout: true,
|
|
3079
|
-
stderr: true
|
|
3127
|
+
stderr: true,
|
|
3128
|
+
resourceLimits: {
|
|
3129
|
+
maxOldGenerationSizeMb: 128,
|
|
3130
|
+
maxYoungGenerationSizeMb: 32
|
|
3131
|
+
}
|
|
3080
3132
|
});
|
|
3081
3133
|
const pluginLogger = createLogger(`Plugin:${manifest.id || pluginId}`);
|
|
3082
3134
|
worker.stdout.on("data", (chunk) => {
|
|
@@ -3108,7 +3160,8 @@ var init_worker_manager = __esm({
|
|
|
3108
3160
|
// `manifest.id` (NOT the DB UUID `pluginId`) so the prefix
|
|
3109
3161
|
// matches what the frontend `ctx.invokeCommand` produces —
|
|
3110
3162
|
// see packages/core/plugin-host/plugin-namespace.ts.
|
|
3111
|
-
manifest.id
|
|
3163
|
+
manifest.id,
|
|
3164
|
+
pluginId
|
|
3112
3165
|
);
|
|
3113
3166
|
const createdAt = Date.now();
|
|
3114
3167
|
this.registry.register(pluginId, {
|
|
@@ -3281,8 +3334,8 @@ __export(server_exports, {
|
|
|
3281
3334
|
});
|
|
3282
3335
|
module.exports = __toCommonJS(server_exports);
|
|
3283
3336
|
var import_express3 = __toESM(require("express"), 1);
|
|
3284
|
-
var
|
|
3285
|
-
var
|
|
3337
|
+
var import_path15 = __toESM(require("path"), 1);
|
|
3338
|
+
var import_fs14 = __toESM(require("fs"), 1);
|
|
3286
3339
|
var import_dotenv = __toESM(require("dotenv"), 1);
|
|
3287
3340
|
var import_vite = require("vite");
|
|
3288
3341
|
var import_http = require("http");
|
|
@@ -3829,6 +3882,7 @@ function parseRequiresEntry(entry) {
|
|
|
3829
3882
|
// packages/core/db/index.ts
|
|
3830
3883
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
3831
3884
|
var import_path = __toESM(require("path"), 1);
|
|
3885
|
+
var import_os = __toESM(require("os"), 1);
|
|
3832
3886
|
var import_fs = require("fs");
|
|
3833
3887
|
var import_url = require("url");
|
|
3834
3888
|
var import_crypto = __toESM(require("crypto"), 1);
|
|
@@ -3852,6 +3906,11 @@ var dbPath;
|
|
|
3852
3906
|
if (process.env.OPENLEARN_DB_PATH) {
|
|
3853
3907
|
dbPath = process.env.OPENLEARN_DB_PATH;
|
|
3854
3908
|
(0, import_fs.mkdirSync)(import_path.default.dirname(dbPath), { recursive: true });
|
|
3909
|
+
} else if (process.env.VITEST) {
|
|
3910
|
+
const poolId = process.env.VITEST_POOL_ID || process.pid;
|
|
3911
|
+
const testDbDir = import_path.default.join(import_os.default.tmpdir(), "openlearn_test_dbs");
|
|
3912
|
+
(0, import_fs.mkdirSync)(testDbDir, { recursive: true });
|
|
3913
|
+
dbPath = import_path.default.join(testDbDir, `openlearn_test_${poolId}.db`);
|
|
3855
3914
|
} else {
|
|
3856
3915
|
if (typeof import_meta !== "undefined" && import_meta.url) {
|
|
3857
3916
|
const __filename2 = (0, import_url.fileURLToPath)(import_meta.url);
|
|
@@ -3882,6 +3941,7 @@ db.exec(`
|
|
|
3882
3941
|
timeline TEXT,
|
|
3883
3942
|
progress_mode TEXT DEFAULT 'manual',
|
|
3884
3943
|
progress_conditions TEXT,
|
|
3944
|
+
creator_id TEXT,
|
|
3885
3945
|
created_at INTEGER NOT NULL,
|
|
3886
3946
|
updated_at INTEGER NOT NULL
|
|
3887
3947
|
);
|
|
@@ -4219,6 +4279,10 @@ db.exec(`
|
|
|
4219
4279
|
CREATE INDEX IF NOT EXISTS idx_assignments_class ON assignments(class_id);
|
|
4220
4280
|
CREATE INDEX IF NOT EXISTS idx_attendance_schedule ON attendance(schedule_id);
|
|
4221
4281
|
`);
|
|
4282
|
+
try {
|
|
4283
|
+
db.exec("ALTER TABLE lessons ADD COLUMN creator_id TEXT");
|
|
4284
|
+
} catch {
|
|
4285
|
+
}
|
|
4222
4286
|
try {
|
|
4223
4287
|
db.exec(`ALTER TABLE plugins ADD COLUMN execution_mode TEXT DEFAULT 'inline'`);
|
|
4224
4288
|
} catch {
|
|
@@ -4315,6 +4379,9 @@ try {
|
|
|
4315
4379
|
const insertStmt = db.prepare("INSERT INTO users (id, username, password_hash, role, name, created_at) VALUES (?, ?, ?, ?, ?, ?)");
|
|
4316
4380
|
insertStmt.run("usr_admin", "admin", hashPassword("admin"), "administrator", "System Admin", Date.now());
|
|
4317
4381
|
insertStmt.run("usr_teacher", "teacher", hashPassword("teacher"), "teacher", "Regular Teacher", Date.now());
|
|
4382
|
+
console.warn(
|
|
4383
|
+
"[SECURITY WARNING] Default users initialized (admin/admin, teacher/teacher). In production environments, immediately change these passwords via POST /api/auth/change-password!"
|
|
4384
|
+
);
|
|
4318
4385
|
}
|
|
4319
4386
|
} catch (e) {
|
|
4320
4387
|
console.error("Failed to seed default users:", e);
|
|
@@ -5669,13 +5736,15 @@ var BuiltinPlugin = {
|
|
|
5669
5736
|
async execute(command) {
|
|
5670
5737
|
const payload = command.payload;
|
|
5671
5738
|
const lessonId = (0, import_uuid6.v7)();
|
|
5672
|
-
const
|
|
5673
|
-
|
|
5739
|
+
const rawCreatorId = payload.creatorId || command.actorId || "admin";
|
|
5740
|
+
const creatorId = typeof rawCreatorId === "string" && rawCreatorId.startsWith("user:") ? rawCreatorId.split(":")[1] : rawCreatorId;
|
|
5741
|
+
const stmt = db2.prepare("INSERT INTO lessons (id, title, content, creator_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)");
|
|
5742
|
+
stmt.run(lessonId, payload.title, payload.content || "", creatorId, Date.now(), Date.now());
|
|
5674
5743
|
await eventBus.publish({
|
|
5675
5744
|
id: (0, import_uuid6.v7)(),
|
|
5676
5745
|
type: "lesson.created",
|
|
5677
5746
|
source: "builtin.lesson",
|
|
5678
|
-
payload: { id: lessonId, title: payload.title },
|
|
5747
|
+
payload: { id: lessonId, title: payload.title, creatorId },
|
|
5679
5748
|
timestamp: Date.now(),
|
|
5680
5749
|
correlationId: command.id
|
|
5681
5750
|
});
|
|
@@ -6648,12 +6717,27 @@ var BuiltinPlugin = {
|
|
|
6648
6717
|
const base64Content = base64Data.replace(/^data:[^;]+;base64,/, "");
|
|
6649
6718
|
const fileBuffer = Buffer.from(base64Content, "base64");
|
|
6650
6719
|
if (ext === ".html" || ext === ".htm") {
|
|
6651
|
-
const entryName = filename;
|
|
6652
|
-
|
|
6720
|
+
const entryName = import_path3.default.basename(filename.replace(/\\/g, "/"));
|
|
6721
|
+
if (!entryName || entryName === "." || entryName === "..") {
|
|
6722
|
+
throw new Error("Invalid courseware filename");
|
|
6723
|
+
}
|
|
6724
|
+
const destPath = import_path3.default.resolve(storageDir, entryName);
|
|
6725
|
+
if (!destPath.startsWith(storageDir)) {
|
|
6726
|
+
throw new Error(`Path traversal attempt in HTML courseware upload: ${filename}`);
|
|
6727
|
+
}
|
|
6728
|
+
import_fs3.default.writeFileSync(destPath, fileBuffer);
|
|
6653
6729
|
const coursewareId = "cw_" + import_crypto3.default.randomBytes(8).toString("hex");
|
|
6654
6730
|
db2.prepare(
|
|
6655
6731
|
"INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
6656
6732
|
).run(coursewareId, uuid, name, "html", entryName, Date.now());
|
|
6733
|
+
try {
|
|
6734
|
+
const resId = "res_" + import_crypto3.default.randomBytes(8).toString("hex");
|
|
6735
|
+
db2.prepare(
|
|
6736
|
+
"INSERT INTO system_resources (id, name, type, content, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
6737
|
+
).run(resId, name, "html", fileBuffer.toString("utf-8"), Date.now());
|
|
6738
|
+
} catch (resErr) {
|
|
6739
|
+
console.error("[builtin.courseware] \u767B\u8BB0 system_resources \u5931\u8D25:", resErr);
|
|
6740
|
+
}
|
|
6657
6741
|
await eventBus.publish({
|
|
6658
6742
|
id: (0, import_uuid6.v7)(),
|
|
6659
6743
|
type: "courseware.uploaded",
|
|
@@ -6742,6 +6826,23 @@ var BuiltinPlugin = {
|
|
|
6742
6826
|
db2.prepare(
|
|
6743
6827
|
"INSERT INTO courseware (id, uuid, name, type, entry, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
6744
6828
|
).run(coursewareId, uuid, name, "folder", entry, Date.now());
|
|
6829
|
+
try {
|
|
6830
|
+
const resFiles = [];
|
|
6831
|
+
for (const [relativePath, file] of Object.entries(loadedZip.files)) {
|
|
6832
|
+
const fileObj = file;
|
|
6833
|
+
if (!fileObj.dir) {
|
|
6834
|
+
const buf = await fileObj.async("nodebuffer");
|
|
6835
|
+
const isBinary = /\.(png|jpe?g|gif|webp|ico|mp3|wav|mp4|woff2?|ttf|otf|eot)$/i.test(relativePath);
|
|
6836
|
+
resFiles.push({ path: relativePath, content: isBinary ? buf.toString("base64") : buf.toString("utf-8") });
|
|
6837
|
+
}
|
|
6838
|
+
}
|
|
6839
|
+
const resId = "res_" + import_crypto3.default.randomBytes(8).toString("hex");
|
|
6840
|
+
db2.prepare(
|
|
6841
|
+
"INSERT INTO system_resources (id, name, type, content, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
6842
|
+
).run(resId, name, "folder", JSON.stringify(resFiles), Date.now());
|
|
6843
|
+
} catch (resErr) {
|
|
6844
|
+
console.error("[builtin.courseware] \u767B\u8BB0 system_resources \u5931\u8D25:", resErr);
|
|
6845
|
+
}
|
|
6745
6846
|
await eventBus.publish({
|
|
6746
6847
|
id: (0, import_uuid6.v7)(),
|
|
6747
6848
|
type: "courseware.uploaded",
|
|
@@ -7580,7 +7681,7 @@ var import_node_url = require("node:url");
|
|
|
7580
7681
|
var esbuild = __toESM(require("esbuild"), 1);
|
|
7581
7682
|
var import_jszip = __toESM(require("jszip"), 1);
|
|
7582
7683
|
var fs3 = __toESM(require("fs"), 1);
|
|
7583
|
-
var
|
|
7684
|
+
var os2 = __toESM(require("os"), 1);
|
|
7584
7685
|
var path4 = __toESM(require("path"), 1);
|
|
7585
7686
|
var import_uuid10 = require("uuid");
|
|
7586
7687
|
var MAX_UNCOMPRESSED_SIZE = 300 * 1024 * 1024;
|
|
@@ -7668,7 +7769,7 @@ async function validateAndBundleZip(zipBuffer) {
|
|
|
7668
7769
|
);
|
|
7669
7770
|
}
|
|
7670
7771
|
const entryCode = await entryFile.async("string");
|
|
7671
|
-
const tmpDir = path4.join(
|
|
7772
|
+
const tmpDir = path4.join(os2.tmpdir(), `plugin-build-${(0, import_uuid10.v7)()}`);
|
|
7672
7773
|
try {
|
|
7673
7774
|
fs3.mkdirSync(tmpDir, { recursive: true });
|
|
7674
7775
|
for (const [name, file] of Object.entries(zip.files)) {
|
|
@@ -8458,11 +8559,11 @@ var PluginDistributionManager = class {
|
|
|
8458
8559
|
return;
|
|
8459
8560
|
}
|
|
8460
8561
|
const filePath = this.pluginHost.getPluginFilePath(this.pluginHost.resolvePluginUuid(pluginId));
|
|
8461
|
-
const
|
|
8462
|
-
if (!
|
|
8562
|
+
const fs16 = await import("node:fs");
|
|
8563
|
+
if (!fs16.existsSync(filePath)) {
|
|
8463
8564
|
throw new Error(`Cannot reload plugin "${pluginId}": source file missing at ${filePath}`);
|
|
8464
8565
|
}
|
|
8465
|
-
const code =
|
|
8566
|
+
const code = fs16.readFileSync(filePath, "utf-8");
|
|
8466
8567
|
await this.pluginHost.reloadPlugin(pluginId, code);
|
|
8467
8568
|
}
|
|
8468
8569
|
async updateFromZip(zipBuffer, options = {}) {
|
|
@@ -8509,6 +8610,16 @@ function validatePluginStateTransition(currentState, nextState, pluginId) {
|
|
|
8509
8610
|
var ACTIVATION_TIMEOUT_MS = 5e3;
|
|
8510
8611
|
var DEACTIVATION_TIMEOUT_MS = 5e3;
|
|
8511
8612
|
var OPENLEARN_VERSION = "0.2.5";
|
|
8613
|
+
function createPluginStaticMiddleware(absDir) {
|
|
8614
|
+
return [
|
|
8615
|
+
(_req, res, next) => {
|
|
8616
|
+
res.setHeader("Content-Security-Policy", "sandbox allow-scripts allow-forms allow-downloads; default-src 'self' 'unsafe-inline' blob: data:");
|
|
8617
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
8618
|
+
next();
|
|
8619
|
+
},
|
|
8620
|
+
import_express.default.static(absDir)
|
|
8621
|
+
];
|
|
8622
|
+
}
|
|
8512
8623
|
var PluginHost = class {
|
|
8513
8624
|
constructor(serviceRegistry, esmLoader, db2, pluginsDir) {
|
|
8514
8625
|
this.serviceRegistry = serviceRegistry;
|
|
@@ -8554,7 +8665,7 @@ var PluginHost = class {
|
|
|
8554
8665
|
const pluginDir = this.getPluginDir(p.id);
|
|
8555
8666
|
const absDir = import_path4.default.join(pluginDir, m.deploy.staticDir);
|
|
8556
8667
|
if (import_fs5.default.existsSync(absDir)) {
|
|
8557
|
-
this.expressApp.use(m.deploy.staticRoute,
|
|
8668
|
+
this.expressApp.use(m.deploy.staticRoute, ...createPluginStaticMiddleware(absDir));
|
|
8558
8669
|
this._registeredRoutes.set(m.id, m.deploy.staticRoute);
|
|
8559
8670
|
console.log(`[PluginHost] Restored static route "${m.deploy.staticRoute}" for plugin "${m.id}"`);
|
|
8560
8671
|
}
|
|
@@ -9764,10 +9875,15 @@ var PluginHost = class {
|
|
|
9764
9875
|
import_fs5.default.writeFileSync(import_path4.default.join(pluginDir, "frontend.js"), frontendCode, "utf-8");
|
|
9765
9876
|
}
|
|
9766
9877
|
if (manifest.deploy?.script) {
|
|
9878
|
+
const rawScript = manifest.deploy.script.replace(/\\/g, "/");
|
|
9879
|
+
const resolvedScript = import_path4.default.resolve(pluginDir, rawScript);
|
|
9880
|
+
if (rawScript.includes("..") || !resolvedScript.startsWith(pluginDir + import_path4.default.sep)) {
|
|
9881
|
+
throw new Error(`Security Violation: Zip Slip detected in deploy script path "${manifest.deploy.script}"`);
|
|
9882
|
+
}
|
|
9767
9883
|
const deployFile = zip.file(manifest.deploy.script);
|
|
9768
9884
|
if (deployFile) {
|
|
9769
9885
|
const deployCode = await deployFile.async("string");
|
|
9770
|
-
import_fs5.default.writeFileSync(
|
|
9886
|
+
import_fs5.default.writeFileSync(resolvedScript, deployCode, "utf-8");
|
|
9771
9887
|
}
|
|
9772
9888
|
}
|
|
9773
9889
|
this.emitProgress(manifest.id, "extracting", "Extracting assets...");
|
|
@@ -9777,20 +9893,29 @@ var PluginHost = class {
|
|
|
9777
9893
|
if (storageEntries.length > 0) {
|
|
9778
9894
|
console.log(`[PluginHost] Extracting ${storageEntries.length} static asset files for plugin "${manifest.id}"...`);
|
|
9779
9895
|
const dirs = /* @__PURE__ */ new Set();
|
|
9780
|
-
for (const
|
|
9781
|
-
|
|
9896
|
+
for (const rawName of storageEntries) {
|
|
9897
|
+
const normalized = rawName.replace(/\\/g, "/");
|
|
9898
|
+
const destPath = import_path4.default.resolve(pluginDir, normalized);
|
|
9899
|
+
if (normalized.includes("..") || !destPath.startsWith(pluginDir + import_path4.default.sep)) {
|
|
9900
|
+
throw new Error(`Security Violation: Zip Slip detected in asset path "${rawName}"`);
|
|
9901
|
+
}
|
|
9902
|
+
dirs.add(import_path4.default.dirname(destPath));
|
|
9782
9903
|
}
|
|
9783
9904
|
for (const dir of dirs) {
|
|
9784
|
-
import_fs5.default.mkdirSync(
|
|
9905
|
+
import_fs5.default.mkdirSync(dir, { recursive: true });
|
|
9785
9906
|
}
|
|
9786
9907
|
const BATCH_SIZE = 10;
|
|
9787
9908
|
for (let i = 0; i < storageEntries.length; i += BATCH_SIZE) {
|
|
9788
9909
|
const batch = storageEntries.slice(i, i + BATCH_SIZE);
|
|
9789
9910
|
await Promise.all(batch.map(async (name) => {
|
|
9911
|
+
const normalized = name.replace(/\\/g, "/");
|
|
9912
|
+
const destPath = import_path4.default.resolve(pluginDir, normalized);
|
|
9913
|
+
if (normalized.includes("..") || !destPath.startsWith(pluginDir + import_path4.default.sep)) {
|
|
9914
|
+
throw new Error(`Security Violation: Zip Slip detected in asset path "${name}"`);
|
|
9915
|
+
}
|
|
9790
9916
|
const file = zip.file(name);
|
|
9791
9917
|
if (file) {
|
|
9792
9918
|
const content = await file.async("nodebuffer");
|
|
9793
|
-
const destPath = import_path4.default.join(pluginDir, name);
|
|
9794
9919
|
import_fs5.default.writeFileSync(destPath, content);
|
|
9795
9920
|
}
|
|
9796
9921
|
}));
|
|
@@ -9808,7 +9933,7 @@ var PluginHost = class {
|
|
|
9808
9933
|
};
|
|
9809
9934
|
import_fs5.default.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2), "utf-8");
|
|
9810
9935
|
const { execSync: execSync2 } = await import("node:child_process");
|
|
9811
|
-
execSync2("npm install --production --no-audit --no-fund --legacy-peer-deps --registry=https://registry.npmmirror.com", {
|
|
9936
|
+
execSync2("npm install --production --no-audit --no-fund --legacy-peer-deps --ignore-scripts --registry=https://registry.npmmirror.com", {
|
|
9812
9937
|
cwd: pluginDir,
|
|
9813
9938
|
stdio: "ignore"
|
|
9814
9939
|
});
|
|
@@ -9818,30 +9943,36 @@ var PluginHost = class {
|
|
|
9818
9943
|
}
|
|
9819
9944
|
}
|
|
9820
9945
|
if (manifest.deploy?.script) {
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
}
|
|
9826
|
-
if (import_fs5.default.existsSync(deployScriptPath)) {
|
|
9827
|
-
console.log(`[PluginHost] Running deploy script: node ${deployScriptPath}`);
|
|
9828
|
-
try {
|
|
9829
|
-
const { execSync: execSync2 } = await import("node:child_process");
|
|
9830
|
-
execSync2(`node "${deployScriptPath}" "${process.cwd()}"`, { timeout: 12e4 });
|
|
9831
|
-
console.log(`[PluginHost] Deploy script completed for plugin "${manifest.id}"`);
|
|
9832
|
-
} catch (deployErr) {
|
|
9833
|
-
console.error(`[PluginHost] Deploy script failed for plugin "${manifest.id}":`, deployErr.message);
|
|
9834
|
-
throw new Error(`Deploy script "${manifest.deploy.script}" failed: ${deployErr.message}`);
|
|
9835
|
-
}
|
|
9946
|
+
if (process.env.ALLOW_UNSAFE_PLUGIN_SCRIPTS !== "true") {
|
|
9947
|
+
console.warn(
|
|
9948
|
+
`[SECURITY WARNING] Deploy script "${manifest.deploy.script}" for plugin "${manifest.id}" blocked by default security policy. Set ALLOW_UNSAFE_PLUGIN_SCRIPTS=true to enable.`
|
|
9949
|
+
);
|
|
9836
9950
|
} else {
|
|
9837
|
-
|
|
9951
|
+
let deployScriptPath = import_path4.default.join(pluginDir, manifest.deploy.script);
|
|
9952
|
+
if (!import_fs5.default.existsSync(deployScriptPath)) {
|
|
9953
|
+
const altPath = import_path4.default.join(process.cwd(), "v2_plugins", "scratch-editor-deploy", manifest.deploy.script);
|
|
9954
|
+
if (import_fs5.default.existsSync(altPath)) deployScriptPath = altPath;
|
|
9955
|
+
}
|
|
9956
|
+
if (import_fs5.default.existsSync(deployScriptPath)) {
|
|
9957
|
+
console.log(`[PluginHost] Running deploy script: node ${deployScriptPath}`);
|
|
9958
|
+
try {
|
|
9959
|
+
const { execSync: execSync2 } = await import("node:child_process");
|
|
9960
|
+
execSync2(`node "${deployScriptPath}" "${process.cwd()}"`, { timeout: 12e4 });
|
|
9961
|
+
console.log(`[PluginHost] Deploy script completed for plugin "${manifest.id}"`);
|
|
9962
|
+
} catch (deployErr) {
|
|
9963
|
+
console.error(`[PluginHost] Deploy script failed for plugin "${manifest.id}":`, deployErr.message);
|
|
9964
|
+
throw new Error(`Deploy script "${manifest.deploy.script}" failed: ${deployErr.message}`);
|
|
9965
|
+
}
|
|
9966
|
+
} else {
|
|
9967
|
+
console.warn(`[PluginHost] Deploy script "${manifest.deploy.script}" not found for plugin "${manifest.id}"`);
|
|
9968
|
+
}
|
|
9838
9969
|
}
|
|
9839
9970
|
}
|
|
9840
9971
|
if (manifest.deploy?.staticRoute && manifest.deploy?.staticDir && this.expressApp) {
|
|
9841
9972
|
const route = manifest.deploy.staticRoute;
|
|
9842
9973
|
const absDir = import_path4.default.join(pluginDir, manifest.deploy.staticDir);
|
|
9843
9974
|
if (import_fs5.default.existsSync(absDir)) {
|
|
9844
|
-
this.expressApp.use(route,
|
|
9975
|
+
this.expressApp.use(route, ...createPluginStaticMiddleware(absDir));
|
|
9845
9976
|
this._registeredRoutes.set(manifest.id, route);
|
|
9846
9977
|
console.log(`[PluginHost] Registered static route "${route}" for plugin "${manifest.id}"`);
|
|
9847
9978
|
} else {
|
|
@@ -10578,8 +10709,8 @@ var HotReloadController = class {
|
|
|
10578
10709
|
const timer = setTimeout(async () => {
|
|
10579
10710
|
this.debounceTimers.delete(pluginId);
|
|
10580
10711
|
try {
|
|
10581
|
-
const
|
|
10582
|
-
const newSourceCode = await
|
|
10712
|
+
const fs16 = await import("fs/promises");
|
|
10713
|
+
const newSourceCode = await fs16.readFile(filePath, "utf-8");
|
|
10583
10714
|
console.log(
|
|
10584
10715
|
`[HotReload] File changed: ${filePath} \u2192 triggering reload for "${pluginId}"`
|
|
10585
10716
|
);
|
|
@@ -17370,13 +17501,22 @@ function setupPresence({ io, eventBus }) {
|
|
|
17370
17501
|
};
|
|
17371
17502
|
io.on("connection", (socket) => {
|
|
17372
17503
|
let registeredStudentId = null;
|
|
17504
|
+
const session = socket.data?.session;
|
|
17505
|
+
const isTeacherOrAdmin = session?.role === "teacher" || session?.role === "administrator";
|
|
17373
17506
|
socket.on("register-student", (data) => {
|
|
17507
|
+
if (session && !isTeacherOrAdmin && session.userId !== data.studentId) {
|
|
17508
|
+
console.warn(`[Presence Security] Student ${session.userId} attempted to impersonate ${data.studentId}`);
|
|
17509
|
+
return socket.emit("error", { message: "Forbidden: Cannot register presence for another student" });
|
|
17510
|
+
}
|
|
17374
17511
|
registeredStudentId = data.studentId;
|
|
17375
17512
|
onlineStudents.set(data.studentId, { socketId: socket.id, name: data.name });
|
|
17376
17513
|
console.log(`[Presence] Student online: ${data.name} (${data.studentId})`);
|
|
17377
17514
|
broadcastPresence();
|
|
17378
17515
|
});
|
|
17379
17516
|
socket.on("enter-lesson", (data) => {
|
|
17517
|
+
if (session && !isTeacherOrAdmin && session.userId !== data.studentId) {
|
|
17518
|
+
return socket.emit("error", { message: "Forbidden: Cannot enter lesson for another student" });
|
|
17519
|
+
}
|
|
17380
17520
|
activeStudentLessons.set(data.studentId, data.lessonId);
|
|
17381
17521
|
socket.join(data.lessonId);
|
|
17382
17522
|
console.log(`[Presence] Student ${data.studentId} entered lesson ${data.lessonId}`);
|
|
@@ -17390,6 +17530,9 @@ function setupPresence({ io, eventBus }) {
|
|
|
17390
17530
|
}
|
|
17391
17531
|
});
|
|
17392
17532
|
socket.on("leave-lesson", (data) => {
|
|
17533
|
+
if (session && !isTeacherOrAdmin && session.userId !== data.studentId) {
|
|
17534
|
+
return socket.emit("error", { message: "Forbidden: Cannot leave lesson for another student" });
|
|
17535
|
+
}
|
|
17393
17536
|
const oldRoom = activeStudentLessons.get(data.studentId);
|
|
17394
17537
|
if (oldRoom) {
|
|
17395
17538
|
socket.leave(oldRoom);
|
|
@@ -17426,10 +17569,18 @@ function setupPresence({ io, eventBus }) {
|
|
|
17426
17569
|
}
|
|
17427
17570
|
);
|
|
17428
17571
|
socket.on("teacher-broadcast-segment", (data) => {
|
|
17572
|
+
if (session && !isTeacherOrAdmin) {
|
|
17573
|
+
console.warn(`[Presence Security] Unauthorized teacher-broadcast-segment by ${session?.userId}`);
|
|
17574
|
+
return socket.emit("error", { message: "Forbidden: Only teachers or administrators can broadcast segments" });
|
|
17575
|
+
}
|
|
17429
17576
|
lessonActiveSegments.set(data.lessonId, data.activeSegmentId);
|
|
17430
17577
|
io.to(data.lessonId).emit("student-active-segment-changed", data);
|
|
17431
17578
|
});
|
|
17432
17579
|
socket.on("teacher-ping-student", (data) => {
|
|
17580
|
+
if (session && !isTeacherOrAdmin) {
|
|
17581
|
+
console.warn(`[Presence Security] Unauthorized teacher-ping-student by ${session?.userId}`);
|
|
17582
|
+
return socket.emit("error", { message: "Forbidden: Only teachers or administrators can ping students" });
|
|
17583
|
+
}
|
|
17433
17584
|
console.log(`[Ping] Teacher pinged student ${data.studentId} for lesson ${data.lessonId}`);
|
|
17434
17585
|
const studentOnlineInfo = onlineStudents.get(data.studentId);
|
|
17435
17586
|
if (studentOnlineInfo) {
|
|
@@ -17537,6 +17688,83 @@ async function runStartupMigrations(db2) {
|
|
|
17537
17688
|
}
|
|
17538
17689
|
}
|
|
17539
17690
|
|
|
17691
|
+
// server/utils/migrate.ts
|
|
17692
|
+
var import_fs7 = __toESM(require("fs"), 1);
|
|
17693
|
+
var import_path6 = __toESM(require("path"), 1);
|
|
17694
|
+
function splitSqlStatements(sql) {
|
|
17695
|
+
return sql.split(";").map((s) => s.trim()).filter((s) => {
|
|
17696
|
+
const content = s.replace(/--.*$/gm, "").trim();
|
|
17697
|
+
return content.length > 0;
|
|
17698
|
+
});
|
|
17699
|
+
}
|
|
17700
|
+
function parseMigrationSql(name, content) {
|
|
17701
|
+
const upMatch = content.match(/--\s*UP([\s\S]*?)(?=--\s*DOWN|$)/i);
|
|
17702
|
+
const downMatch = content.match(/--\s*DOWN([\s\S]*)$/i);
|
|
17703
|
+
const up = upMatch ? upMatch[1].trim() : content.trim();
|
|
17704
|
+
const down = downMatch ? downMatch[1].trim() : "";
|
|
17705
|
+
return { name, up, down };
|
|
17706
|
+
}
|
|
17707
|
+
function loadMigrationsFromDirectory(dirPath) {
|
|
17708
|
+
if (!import_fs7.default.existsSync(dirPath)) {
|
|
17709
|
+
return [];
|
|
17710
|
+
}
|
|
17711
|
+
const files = import_fs7.default.readdirSync(dirPath).filter((f) => f.endsWith(".sql")).sort((a, b) => a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" }));
|
|
17712
|
+
return files.map((file) => {
|
|
17713
|
+
const fullPath = import_path6.default.join(dirPath, file);
|
|
17714
|
+
const content = import_fs7.default.readFileSync(fullPath, "utf8");
|
|
17715
|
+
const name = import_path6.default.basename(file, ".sql");
|
|
17716
|
+
return parseMigrationSql(name, content);
|
|
17717
|
+
});
|
|
17718
|
+
}
|
|
17719
|
+
function executeSqlStatements(db2, sql) {
|
|
17720
|
+
const statements = splitSqlStatements(sql);
|
|
17721
|
+
for (const stmt of statements) {
|
|
17722
|
+
try {
|
|
17723
|
+
db2.exec(stmt);
|
|
17724
|
+
} catch (err) {
|
|
17725
|
+
if (err?.message && err.message.includes("duplicate column name")) {
|
|
17726
|
+
console.warn(`[Migration] Column already exists, safely continuing: ${stmt.slice(0, 60)}...`);
|
|
17727
|
+
continue;
|
|
17728
|
+
}
|
|
17729
|
+
throw err;
|
|
17730
|
+
}
|
|
17731
|
+
}
|
|
17732
|
+
}
|
|
17733
|
+
function runMigrations(db2, migrations) {
|
|
17734
|
+
db2.exec(`
|
|
17735
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
17736
|
+
name TEXT PRIMARY KEY,
|
|
17737
|
+
applied_at INTEGER NOT NULL,
|
|
17738
|
+
checksum TEXT NOT NULL
|
|
17739
|
+
);
|
|
17740
|
+
`);
|
|
17741
|
+
const applied = new Set(
|
|
17742
|
+
db2.prepare("SELECT name FROM _migrations").all().map((r) => r.name)
|
|
17743
|
+
);
|
|
17744
|
+
for (const migration of migrations) {
|
|
17745
|
+
if (applied.has(migration.name)) continue;
|
|
17746
|
+
console.log(`[Migration] Applying: ${migration.name}`);
|
|
17747
|
+
try {
|
|
17748
|
+
executeSqlStatements(db2, migration.up);
|
|
17749
|
+
const checksum = simpleChecksum(migration.up);
|
|
17750
|
+
db2.prepare("INSERT INTO _migrations (name, applied_at, checksum) VALUES (?, ?, ?)").run(migration.name, Date.now(), checksum);
|
|
17751
|
+
console.log(`[Migration] Applied: ${migration.name}`);
|
|
17752
|
+
} catch (err) {
|
|
17753
|
+
console.error(`[Migration] FAILED: ${migration.name}`, err);
|
|
17754
|
+
throw err;
|
|
17755
|
+
}
|
|
17756
|
+
}
|
|
17757
|
+
}
|
|
17758
|
+
function simpleChecksum(text) {
|
|
17759
|
+
let hash = 0;
|
|
17760
|
+
for (let i = 0; i < text.length; i++) {
|
|
17761
|
+
const char = text.charCodeAt(i);
|
|
17762
|
+
hash = (hash << 5) - hash + char;
|
|
17763
|
+
hash |= 0;
|
|
17764
|
+
}
|
|
17765
|
+
return hash.toString(16);
|
|
17766
|
+
}
|
|
17767
|
+
|
|
17540
17768
|
// server/ai-agent.ts
|
|
17541
17769
|
var import_genai2 = require("@google/genai");
|
|
17542
17770
|
var import_crypto4 = __toESM(require("crypto"), 1);
|
|
@@ -17848,6 +18076,90 @@ var runOpenAIAgentChat = async (provider, request) => {
|
|
|
17848
18076
|
};
|
|
17849
18077
|
};
|
|
17850
18078
|
|
|
18079
|
+
// server/middleware/auth.ts
|
|
18080
|
+
function getValidSession(token) {
|
|
18081
|
+
let sessionRow;
|
|
18082
|
+
try {
|
|
18083
|
+
sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
|
|
18084
|
+
} catch {
|
|
18085
|
+
return null;
|
|
18086
|
+
}
|
|
18087
|
+
if (!sessionRow) return null;
|
|
18088
|
+
const now = Date.now();
|
|
18089
|
+
if (sessionRow.expires_at && sessionRow.expires_at < now) {
|
|
18090
|
+
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
18091
|
+
return null;
|
|
18092
|
+
}
|
|
18093
|
+
const idleTimeout = 24 * 60 * 60 * 1e3;
|
|
18094
|
+
if (sessionRow.updated_at && now - sessionRow.updated_at > idleTimeout) {
|
|
18095
|
+
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
18096
|
+
return null;
|
|
18097
|
+
}
|
|
18098
|
+
kernelContainer.db.prepare("UPDATE client_sessions SET updated_at = ? WHERE id = ?").run(now, token);
|
|
18099
|
+
return JSON.parse(sessionRow.session_data);
|
|
18100
|
+
}
|
|
18101
|
+
function getCookieToken(req) {
|
|
18102
|
+
const rc = req.headers.cookie;
|
|
18103
|
+
if (!rc) return null;
|
|
18104
|
+
const parts = rc.split(";");
|
|
18105
|
+
for (const part of parts) {
|
|
18106
|
+
const trimmed = part.trim();
|
|
18107
|
+
if (trimmed.startsWith("edu_os_token=")) {
|
|
18108
|
+
return trimmed.substring("edu_os_token=".length);
|
|
18109
|
+
}
|
|
18110
|
+
}
|
|
18111
|
+
return null;
|
|
18112
|
+
}
|
|
18113
|
+
function getActorId(req) {
|
|
18114
|
+
const token = getCookieToken(req);
|
|
18115
|
+
if (!token) return "anonymous";
|
|
18116
|
+
try {
|
|
18117
|
+
const session = getValidSession(token);
|
|
18118
|
+
if (!session) return "anonymous";
|
|
18119
|
+
let role = session.subRole || session.role;
|
|
18120
|
+
if (session.username === "admin" || session.userId === "usr_admin" || role === "admin" || role === "administrator") {
|
|
18121
|
+
role = "administrator";
|
|
18122
|
+
}
|
|
18123
|
+
if (role) {
|
|
18124
|
+
return `user:${session.userId || "demo"}:${role}`;
|
|
18125
|
+
}
|
|
18126
|
+
return "anonymous";
|
|
18127
|
+
} catch {
|
|
18128
|
+
return "anonymous";
|
|
18129
|
+
}
|
|
18130
|
+
}
|
|
18131
|
+
function requireAuth(...roles) {
|
|
18132
|
+
return (req, res, next) => {
|
|
18133
|
+
const token = getCookieToken(req);
|
|
18134
|
+
if (!token) return res.status(401).json({ success: false, error: "Authentication required" });
|
|
18135
|
+
const session = getValidSession(token);
|
|
18136
|
+
if (!session) return res.status(401).json({ success: false, error: "Session expired or invalid" });
|
|
18137
|
+
if (roles.length > 0) {
|
|
18138
|
+
let userRole = session.role;
|
|
18139
|
+
if (session.username === "admin" || session.userId === "usr_admin" || userRole === "admin") {
|
|
18140
|
+
userRole = "administrator";
|
|
18141
|
+
}
|
|
18142
|
+
const effectiveRoles = roles.map((r) => r === "admin" ? "administrator" : r);
|
|
18143
|
+
if (!effectiveRoles.includes(userRole)) {
|
|
18144
|
+
return res.status(403).json({ success: false, error: `Role ${session.role} not allowed. Required: ${roles.join(", ")}` });
|
|
18145
|
+
}
|
|
18146
|
+
}
|
|
18147
|
+
req.session = session;
|
|
18148
|
+
next();
|
|
18149
|
+
};
|
|
18150
|
+
}
|
|
18151
|
+
function checkIsTeacherOrAdmin(req) {
|
|
18152
|
+
const token = getCookieToken(req);
|
|
18153
|
+
if (!token) return false;
|
|
18154
|
+
try {
|
|
18155
|
+
const session = getValidSession(token);
|
|
18156
|
+
if (!session) return false;
|
|
18157
|
+
return session.role === "teacher" || session.role === "administrator";
|
|
18158
|
+
} catch {
|
|
18159
|
+
return false;
|
|
18160
|
+
}
|
|
18161
|
+
}
|
|
18162
|
+
|
|
17851
18163
|
// packages/core/bootstrap/adapter/bootstrap-registration.ts
|
|
17852
18164
|
var BootstrapRegistration = class {
|
|
17853
18165
|
static registerConfiguration(builder, context) {
|
|
@@ -18332,28 +18644,28 @@ var IActivityRegistryToken = new Token(
|
|
|
18332
18644
|
);
|
|
18333
18645
|
|
|
18334
18646
|
// server/routes/os.ts
|
|
18335
|
-
var
|
|
18336
|
-
var
|
|
18647
|
+
var import_path9 = __toESM(require("path"), 1);
|
|
18648
|
+
var import_fs9 = __toESM(require("fs"), 1);
|
|
18337
18649
|
var import_child_process = require("child_process");
|
|
18338
18650
|
var import_crypto7 = __toESM(require("crypto"), 1);
|
|
18339
18651
|
var import_xss = require("xss");
|
|
18340
18652
|
|
|
18341
18653
|
// server/utils/crypto.ts
|
|
18342
18654
|
var import_crypto5 = __toESM(require("crypto"), 1);
|
|
18343
|
-
var
|
|
18344
|
-
var
|
|
18655
|
+
var import_fs8 = __toESM(require("fs"), 1);
|
|
18656
|
+
var import_path7 = __toESM(require("path"), 1);
|
|
18345
18657
|
var _encryptionKey = null;
|
|
18346
18658
|
function getEncryptionKey() {
|
|
18347
18659
|
if (_encryptionKey) return _encryptionKey;
|
|
18348
|
-
const envPath =
|
|
18660
|
+
const envPath = import_path7.default.resolve(process.cwd(), ".env");
|
|
18349
18661
|
const keyHex = process.env.ENCRYPTION_KEY;
|
|
18350
18662
|
if (keyHex && keyHex.trim() !== "") {
|
|
18351
18663
|
_encryptionKey = Buffer.from(keyHex.trim(), "hex");
|
|
18352
18664
|
return _encryptionKey;
|
|
18353
18665
|
}
|
|
18354
|
-
if (
|
|
18666
|
+
if (import_fs8.default.existsSync(envPath)) {
|
|
18355
18667
|
try {
|
|
18356
|
-
const content =
|
|
18668
|
+
const content = import_fs8.default.readFileSync(envPath, "utf-8");
|
|
18357
18669
|
const match = content.match(/^ENCRYPTION_KEY=(.+)$/m);
|
|
18358
18670
|
if (match && match[1].trim() !== "") {
|
|
18359
18671
|
const fileKeyHex = match[1].trim();
|
|
@@ -18367,8 +18679,8 @@ function getEncryptionKey() {
|
|
|
18367
18679
|
}
|
|
18368
18680
|
const newKey = import_crypto5.default.randomBytes(32).toString("hex");
|
|
18369
18681
|
try {
|
|
18370
|
-
if (
|
|
18371
|
-
const existingContent =
|
|
18682
|
+
if (import_fs8.default.existsSync(envPath)) {
|
|
18683
|
+
const existingContent = import_fs8.default.readFileSync(envPath, "utf-8");
|
|
18372
18684
|
if (/^ENCRYPTION_KEY=/m.test(existingContent)) {
|
|
18373
18685
|
console.warn(
|
|
18374
18686
|
"[Crypto] ENCRYPTION_KEY missing/empty in .env; NOT overwriting existing entry. Generating ephemeral in-memory key \u2014 please set ENCRYPTION_KEY manually before relying on encrypted storage."
|
|
@@ -18377,12 +18689,12 @@ function getEncryptionKey() {
|
|
|
18377
18689
|
_encryptionKey = Buffer.from(newKey, "hex");
|
|
18378
18690
|
return _encryptionKey;
|
|
18379
18691
|
}
|
|
18380
|
-
|
|
18692
|
+
import_fs8.default.appendFileSync(envPath, `
|
|
18381
18693
|
ENCRYPTION_KEY=${newKey}
|
|
18382
18694
|
`);
|
|
18383
18695
|
console.log("[Crypto] ENCRYPTION_KEY auto-generated and persisted to .env");
|
|
18384
18696
|
} else {
|
|
18385
|
-
|
|
18697
|
+
import_fs8.default.writeFileSync(envPath, `ENCRYPTION_KEY=${newKey}
|
|
18386
18698
|
`);
|
|
18387
18699
|
console.log("[Crypto] .env created with auto-generated ENCRYPTION_KEY");
|
|
18388
18700
|
}
|
|
@@ -18435,142 +18747,58 @@ function detectPromptInjection(input) {
|
|
|
18435
18747
|
return PROMPT_INJECTION_PATTERNS.some((p) => p.test(input));
|
|
18436
18748
|
}
|
|
18437
18749
|
|
|
18438
|
-
// server/
|
|
18439
|
-
|
|
18440
|
-
|
|
18750
|
+
// server/routes/shared.ts
|
|
18751
|
+
var import_path8 = __toESM(require("path"), 1);
|
|
18752
|
+
var import_crypto6 = __toESM(require("crypto"), 1);
|
|
18753
|
+
|
|
18754
|
+
// server/utils/bridge-sdk.ts
|
|
18755
|
+
var BRIDGE_SDK_CODE = `(function() {
|
|
18756
|
+
// Mock document.cookie to prevent SecurityError in sandboxed iframes lacking 'allow-same-origin'
|
|
18441
18757
|
try {
|
|
18442
|
-
|
|
18443
|
-
|
|
18444
|
-
|
|
18445
|
-
|
|
18446
|
-
|
|
18447
|
-
|
|
18448
|
-
|
|
18449
|
-
|
|
18450
|
-
|
|
18758
|
+
Object.defineProperty(document, 'cookie', {
|
|
18759
|
+
get: function() { return ""; },
|
|
18760
|
+
set: function(val) {},
|
|
18761
|
+
configurable: true
|
|
18762
|
+
});
|
|
18763
|
+
} catch (e) {
|
|
18764
|
+
try {
|
|
18765
|
+
Object.defineProperty(Document.prototype, 'cookie', {
|
|
18766
|
+
get: function() { return ""; },
|
|
18767
|
+
set: function(val) {},
|
|
18768
|
+
configurable: true
|
|
18769
|
+
});
|
|
18770
|
+
} catch (err) {}
|
|
18451
18771
|
}
|
|
18452
|
-
|
|
18453
|
-
|
|
18454
|
-
|
|
18455
|
-
return
|
|
18456
|
-
|
|
18457
|
-
|
|
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
|
-
if (role) {
|
|
18483
|
-
return `user:${session.userId || "demo"}:${role}`;
|
|
18484
|
-
}
|
|
18485
|
-
return "anonymous";
|
|
18486
|
-
} catch {
|
|
18487
|
-
return "anonymous";
|
|
18488
|
-
}
|
|
18489
|
-
}
|
|
18490
|
-
function requireAuth(...roles) {
|
|
18491
|
-
return (req, res, next) => {
|
|
18492
|
-
const token = getCookieToken(req);
|
|
18493
|
-
if (!token) return res.status(401).json({ success: false, error: "Authentication required" });
|
|
18494
|
-
const session = getValidSession(token);
|
|
18495
|
-
if (!session) return res.status(401).json({ success: false, error: "Session expired or invalid" });
|
|
18496
|
-
if (roles.length > 0) {
|
|
18497
|
-
let userRole = session.role;
|
|
18498
|
-
if (session.username === "admin" || session.userId === "usr_admin" || userRole === "admin") {
|
|
18499
|
-
userRole = "administrator";
|
|
18500
|
-
}
|
|
18501
|
-
const effectiveRoles = roles.map((r) => r === "admin" ? "administrator" : r);
|
|
18502
|
-
if (!effectiveRoles.includes(userRole)) {
|
|
18503
|
-
return res.status(403).json({ success: false, error: `Role ${session.role} not allowed. Required: ${roles.join(", ")}` });
|
|
18504
|
-
}
|
|
18505
|
-
}
|
|
18506
|
-
req.session = session;
|
|
18507
|
-
next();
|
|
18508
|
-
};
|
|
18509
|
-
}
|
|
18510
|
-
function checkIsTeacherOrAdmin(req) {
|
|
18511
|
-
const token = getCookieToken(req);
|
|
18512
|
-
if (!token) return false;
|
|
18513
|
-
try {
|
|
18514
|
-
const session = getValidSession(token);
|
|
18515
|
-
if (!session) return false;
|
|
18516
|
-
return session.role === "teacher" || session.role === "administrator";
|
|
18517
|
-
} catch {
|
|
18518
|
-
return false;
|
|
18519
|
-
}
|
|
18520
|
-
}
|
|
18521
|
-
|
|
18522
|
-
// server/routes/shared.ts
|
|
18523
|
-
var import_path7 = __toESM(require("path"), 1);
|
|
18524
|
-
var import_crypto6 = __toESM(require("crypto"), 1);
|
|
18525
|
-
|
|
18526
|
-
// server/utils/bridge-sdk.ts
|
|
18527
|
-
var BRIDGE_SDK_CODE = `(function() {
|
|
18528
|
-
// Mock document.cookie to prevent SecurityError in sandboxed iframes lacking 'allow-same-origin'
|
|
18529
|
-
try {
|
|
18530
|
-
Object.defineProperty(document, 'cookie', {
|
|
18531
|
-
get: function() { return ""; },
|
|
18532
|
-
set: function(val) {},
|
|
18533
|
-
configurable: true
|
|
18534
|
-
});
|
|
18535
|
-
} catch (e) {
|
|
18536
|
-
try {
|
|
18537
|
-
Object.defineProperty(Document.prototype, 'cookie', {
|
|
18538
|
-
get: function() { return ""; },
|
|
18539
|
-
set: function(val) {},
|
|
18540
|
-
configurable: true
|
|
18541
|
-
});
|
|
18542
|
-
} catch (err) {}
|
|
18543
|
-
}
|
|
18544
|
-
|
|
18545
|
-
// \u2500\u2500 Helper: create a safe postMessage wrapper that normalises 'null' \u2192 '*' \u2500\u2500
|
|
18546
|
-
function __makeSafePostMessage(realTarget, label) {
|
|
18547
|
-
return function(message, targetOrigin, transfer) {
|
|
18548
|
-
try {
|
|
18549
|
-
if (message && typeof message === 'object') {
|
|
18550
|
-
if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
|
|
18551
|
-
message.attempt_id = window.__LMS_STUDENT__.attempt_id;
|
|
18552
|
-
}
|
|
18553
|
-
if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
|
|
18554
|
-
message.uuid = window.__LMS_COURSEWARE__.uuid;
|
|
18555
|
-
}
|
|
18556
|
-
}
|
|
18557
|
-
} catch (e) {}
|
|
18558
|
-
|
|
18559
|
-
var origin = targetOrigin;
|
|
18560
|
-
if (origin === 'null' || origin === null || origin === undefined) {
|
|
18561
|
-
console.warn('[LMS Bridge Notice] Normalized invalid targetOrigin "' + origin + '" to "*" for ' + label);
|
|
18562
|
-
origin = '*';
|
|
18563
|
-
}
|
|
18564
|
-
try {
|
|
18565
|
-
return realTarget.postMessage.call(realTarget, message, origin, transfer);
|
|
18566
|
-
} catch (err) {
|
|
18567
|
-
if (err.name === 'SyntaxError' && origin !== '*') {
|
|
18568
|
-
console.warn('[LMS Bridge Notice] Recovered SyntaxError on ' + label + ', fallback to "*"', err);
|
|
18569
|
-
return realTarget.postMessage.call(realTarget, message, '*', transfer);
|
|
18570
|
-
}
|
|
18571
|
-
throw err;
|
|
18572
|
-
}
|
|
18573
|
-
};
|
|
18772
|
+
|
|
18773
|
+
// \u2500\u2500 Helper: create a safe postMessage wrapper that normalises 'null' \u2192 '*' \u2500\u2500
|
|
18774
|
+
function __makeSafePostMessage(realTarget, label) {
|
|
18775
|
+
return function(message, targetOrigin, transfer) {
|
|
18776
|
+
try {
|
|
18777
|
+
if (message && typeof message === 'object') {
|
|
18778
|
+
if (!message.attempt_id && window.__LMS_STUDENT__?.attempt_id) {
|
|
18779
|
+
message.attempt_id = window.__LMS_STUDENT__.attempt_id;
|
|
18780
|
+
}
|
|
18781
|
+
if (!message.uuid && window.__LMS_COURSEWARE__?.uuid) {
|
|
18782
|
+
message.uuid = window.__LMS_COURSEWARE__.uuid;
|
|
18783
|
+
}
|
|
18784
|
+
}
|
|
18785
|
+
} catch (e) {}
|
|
18786
|
+
|
|
18787
|
+
var origin = targetOrigin;
|
|
18788
|
+
if (origin === 'null' || origin === null || origin === undefined) {
|
|
18789
|
+
console.warn('[LMS Bridge Notice] Normalized invalid targetOrigin "' + origin + '" to "*" for ' + label);
|
|
18790
|
+
origin = '*';
|
|
18791
|
+
}
|
|
18792
|
+
try {
|
|
18793
|
+
return realTarget.postMessage.call(realTarget, message, origin, transfer);
|
|
18794
|
+
} catch (err) {
|
|
18795
|
+
if (err.name === 'SyntaxError' && origin !== '*') {
|
|
18796
|
+
console.warn('[LMS Bridge Notice] Recovered SyntaxError on ' + label + ', fallback to "*"', err);
|
|
18797
|
+
return realTarget.postMessage.call(realTarget, message, '*', transfer);
|
|
18798
|
+
}
|
|
18799
|
+
throw err;
|
|
18800
|
+
}
|
|
18801
|
+
};
|
|
18574
18802
|
}
|
|
18575
18803
|
|
|
18576
18804
|
// \u2500\u2500 Helper: create a Proxy wrapper around a cross-origin WindowProxy \u2500\u2500
|
|
@@ -18681,6 +18909,61 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
18681
18909
|
}
|
|
18682
18910
|
} catch (e) {}
|
|
18683
18911
|
|
|
18912
|
+
// \u2500\u2500 Bidirectional host\u2192courseware command bus & Theme Bridge \u2500\u2500
|
|
18913
|
+
var __lmsHandlers = {};
|
|
18914
|
+
var __lmsPendingRequests = {};
|
|
18915
|
+
|
|
18916
|
+
function __applyThemeTokens(themeData) {
|
|
18917
|
+
if (!themeData || typeof themeData !== 'object') return;
|
|
18918
|
+
try {
|
|
18919
|
+
window.__LMS_THEME__ = themeData;
|
|
18920
|
+
var docEl = document.documentElement;
|
|
18921
|
+
if (docEl) {
|
|
18922
|
+
if (themeData.theme) {
|
|
18923
|
+
docEl.setAttribute('data-theme', themeData.theme);
|
|
18924
|
+
}
|
|
18925
|
+
if (themeData.tokens && typeof themeData.tokens === 'object') {
|
|
18926
|
+
for (var k in themeData.tokens) {
|
|
18927
|
+
if (Object.prototype.hasOwnProperty.call(themeData.tokens, k)) {
|
|
18928
|
+
docEl.style.setProperty(k, themeData.tokens[k]);
|
|
18929
|
+
}
|
|
18930
|
+
}
|
|
18931
|
+
}
|
|
18932
|
+
}
|
|
18933
|
+
} catch (e) {}
|
|
18934
|
+
}
|
|
18935
|
+
|
|
18936
|
+
if (window.__LMS_THEME__) {
|
|
18937
|
+
__applyThemeTokens(window.__LMS_THEME__);
|
|
18938
|
+
}
|
|
18939
|
+
|
|
18940
|
+
window.addEventListener('message', function(event) {
|
|
18941
|
+
var d = event.data;
|
|
18942
|
+
if (!d || typeof d !== 'object') return;
|
|
18943
|
+
if (d.type === 'LMS_HOST_COMMAND' && d.event) {
|
|
18944
|
+
if (d.event === 'theme:changed' && d.payload) {
|
|
18945
|
+
__applyThemeTokens(d.payload);
|
|
18946
|
+
}
|
|
18947
|
+
var cbs = __lmsHandlers[d.event] || [];
|
|
18948
|
+
for (var i = 0; i < cbs.length; i++) {
|
|
18949
|
+
try { cbs[i](d.payload); } catch (e) {}
|
|
18950
|
+
}
|
|
18951
|
+
} else if (d.type === 'LMS_THEME_CHANGED' && (d.theme || d.tokens)) {
|
|
18952
|
+
__applyThemeTokens(d);
|
|
18953
|
+
var themeCbs = __lmsHandlers['theme:changed'] || [];
|
|
18954
|
+
for (var j = 0; j < themeCbs.length; j++) {
|
|
18955
|
+
try { themeCbs[j](d); } catch (e) {}
|
|
18956
|
+
}
|
|
18957
|
+
} else if (d.type === 'LMS_PROGRESS_RESPONSE' && d.requestId) {
|
|
18958
|
+
var pending = __lmsPendingRequests[d.requestId];
|
|
18959
|
+
if (pending) {
|
|
18960
|
+
delete __lmsPendingRequests[d.requestId];
|
|
18961
|
+
clearTimeout(pending.timer);
|
|
18962
|
+
pending.resolve(d.progress || null);
|
|
18963
|
+
}
|
|
18964
|
+
}
|
|
18965
|
+
});
|
|
18966
|
+
|
|
18684
18967
|
window.LMS = {
|
|
18685
18968
|
submit(data) {
|
|
18686
18969
|
window.parent.postMessage({
|
|
@@ -18712,6 +18995,9 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
18712
18995
|
getCourseware() {
|
|
18713
18996
|
return window.__LMS_COURSEWARE__;
|
|
18714
18997
|
},
|
|
18998
|
+
getTheme() {
|
|
18999
|
+
return window.__LMS_THEME__ || null;
|
|
19000
|
+
},
|
|
18715
19001
|
log(event, data) {
|
|
18716
19002
|
window.parent.postMessage({
|
|
18717
19003
|
type: "LMS_LOG",
|
|
@@ -18720,6 +19006,43 @@ var BRIDGE_SDK_CODE = `(function() {
|
|
|
18720
19006
|
event: event,
|
|
18721
19007
|
payload: data
|
|
18722
19008
|
}, "*");
|
|
19009
|
+
},
|
|
19010
|
+
on(event, callback) {
|
|
19011
|
+
(__lmsHandlers[event] = __lmsHandlers[event] || []).push(callback);
|
|
19012
|
+
return function() {
|
|
19013
|
+
var arr = __lmsHandlers[event] || [];
|
|
19014
|
+
var idx = arr.indexOf(callback);
|
|
19015
|
+
if (idx >= 0) arr.splice(idx, 1);
|
|
19016
|
+
};
|
|
19017
|
+
},
|
|
19018
|
+
off(event, callback) {
|
|
19019
|
+
var arr = __lmsHandlers[event] || [];
|
|
19020
|
+
var idx = arr.indexOf(callback);
|
|
19021
|
+
if (idx >= 0) arr.splice(idx, 1);
|
|
19022
|
+
},
|
|
19023
|
+
setConfig(config) {
|
|
19024
|
+
window.parent.postMessage({
|
|
19025
|
+
type: "LMS_CONFIG",
|
|
19026
|
+
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
19027
|
+
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
19028
|
+
config: config
|
|
19029
|
+
}, "*");
|
|
19030
|
+
},
|
|
19031
|
+
getProgress() {
|
|
19032
|
+
return new Promise(function(resolve) {
|
|
19033
|
+
var requestId = 'prog_' + Math.random().toString(36).slice(2) + '_' + Date.now();
|
|
19034
|
+
var timer = setTimeout(function() {
|
|
19035
|
+
delete __lmsPendingRequests[requestId];
|
|
19036
|
+
resolve(null);
|
|
19037
|
+
}, 5000);
|
|
19038
|
+
__lmsPendingRequests[requestId] = { resolve: resolve, timer: timer };
|
|
19039
|
+
window.parent.postMessage({
|
|
19040
|
+
type: "LMS_GET_PROGRESS",
|
|
19041
|
+
uuid: window.__LMS_COURSEWARE__?.uuid,
|
|
19042
|
+
attempt_id: window.__LMS_STUDENT__?.attempt_id,
|
|
19043
|
+
requestId: requestId
|
|
19044
|
+
}, "*");
|
|
19045
|
+
});
|
|
18723
19046
|
}
|
|
18724
19047
|
};
|
|
18725
19048
|
|
|
@@ -19087,7 +19410,7 @@ function validateMagicBytes(buffer, fileName) {
|
|
|
19087
19410
|
".webp": [[82, 73, 70, 70]]
|
|
19088
19411
|
// RIFF
|
|
19089
19412
|
};
|
|
19090
|
-
const ext =
|
|
19413
|
+
const ext = import_path8.default.extname(fileName || "").toLowerCase();
|
|
19091
19414
|
const signatures = MAGIC_BYTES[ext];
|
|
19092
19415
|
if (!signatures) return true;
|
|
19093
19416
|
return signatures.some(
|
|
@@ -19162,6 +19485,8 @@ window.__LMS_COURSEWARE__ = {
|
|
|
19162
19485
|
html = html.replace(/<script[^>]*src='[^']*frog-sdk\.js'[^>]*><\/script>/gi, "<!-- Removed frog-sdk.js to prevent sandboxed iframe crash -->");
|
|
19163
19486
|
html = html.replace(/<script[^>]*src="[^"]*init-frog\.js"[^>]*><\/script>/gi, "<!-- Removed init-frog.js to prevent sandboxed iframe crash -->");
|
|
19164
19487
|
html = html.replace(/<script[^>]*src='[^']*init-frog\.js'[^>]*><\/script>/gi, "<!-- Removed init-frog.js to prevent sandboxed iframe crash -->");
|
|
19488
|
+
html = html.replace(/<base\b[^>]*>/gi, "<!-- Removed base tag -->");
|
|
19489
|
+
html = html.replace(/<meta[^>]*http-equiv\s*=\s*["']?refresh["']?[^>]*>/gi, "<!-- Removed meta refresh -->");
|
|
19165
19490
|
if (html.toLowerCase().includes("<head>")) {
|
|
19166
19491
|
html = html.replace(/<head>/i, `<head>${injection}`);
|
|
19167
19492
|
} else if (html.toLowerCase().includes("<html>")) {
|
|
@@ -19188,20 +19513,21 @@ var generateStudentNumber = (db2) => {
|
|
|
19188
19513
|
return `S${nextSeq.toString().padStart(3, "0")}`;
|
|
19189
19514
|
};
|
|
19190
19515
|
|
|
19516
|
+
// server/utils/error-handler.ts
|
|
19517
|
+
function sendSafeError(res, err, status = 500, fallbackMessage = "Internal server error") {
|
|
19518
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
19519
|
+
if (process.env.NODE_ENV !== "test") {
|
|
19520
|
+
console.error(`[API Error ${status}]:`, err);
|
|
19521
|
+
}
|
|
19522
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
19523
|
+
const exposedMessage = isProd ? fallbackMessage : message;
|
|
19524
|
+
return res.status(status).json({ success: false, error: exposedMessage });
|
|
19525
|
+
}
|
|
19526
|
+
|
|
19191
19527
|
// server/routes/os.ts
|
|
19192
19528
|
function registerOsRoutes(ctx) {
|
|
19193
19529
|
const {
|
|
19194
19530
|
app,
|
|
19195
|
-
io,
|
|
19196
|
-
loginLimiter,
|
|
19197
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
19198
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
19199
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
19200
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
19201
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
19202
|
-
buildOpenAITools: buildOpenAITools2,
|
|
19203
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
19204
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
19205
19531
|
runGeminiAgentChat: runGeminiAgentChat2,
|
|
19206
19532
|
runOpenAIAgentChat: runOpenAIAgentChat2,
|
|
19207
19533
|
activityRegistry
|
|
@@ -19212,7 +19538,7 @@ function registerOsRoutes(ctx) {
|
|
|
19212
19538
|
if (!filename || !base64Data) {
|
|
19213
19539
|
return res.status(400).json({ error: "Filename and base64Data are required" });
|
|
19214
19540
|
}
|
|
19215
|
-
const ext =
|
|
19541
|
+
const ext = import_path9.default.extname(filename).toLowerCase();
|
|
19216
19542
|
if (BLOCKED_EXTENSIONS.includes(ext)) {
|
|
19217
19543
|
return res.status(400).json({ error: `File type ${ext} is not allowed for security reasons.` });
|
|
19218
19544
|
}
|
|
@@ -19224,13 +19550,13 @@ function registerOsRoutes(ctx) {
|
|
|
19224
19550
|
if (!validateMagicBytes(fileBuffer, filename)) {
|
|
19225
19551
|
return res.status(400).json({ error: "File content does not match the declared file type." });
|
|
19226
19552
|
}
|
|
19227
|
-
const uploadsDir =
|
|
19228
|
-
if (!
|
|
19229
|
-
|
|
19553
|
+
const uploadsDir = import_path9.default.join(process.cwd(), "uploads");
|
|
19554
|
+
if (!import_fs9.default.existsSync(uploadsDir)) {
|
|
19555
|
+
import_fs9.default.mkdirSync(uploadsDir, { recursive: true });
|
|
19230
19556
|
}
|
|
19231
19557
|
const uniqueName = `${Date.now()}-${import_crypto7.default.randomBytes(4).toString("hex")}${ext}`;
|
|
19232
|
-
const filePath =
|
|
19233
|
-
|
|
19558
|
+
const filePath = import_path9.default.join(uploadsDir, uniqueName);
|
|
19559
|
+
import_fs9.default.writeFileSync(filePath, fileBuffer);
|
|
19234
19560
|
let slideCount = 1;
|
|
19235
19561
|
if (ext === ".pdf") {
|
|
19236
19562
|
try {
|
|
@@ -19264,7 +19590,7 @@ function registerOsRoutes(ctx) {
|
|
|
19264
19590
|
});
|
|
19265
19591
|
} catch (e) {
|
|
19266
19592
|
console.error("Upload error:", e);
|
|
19267
|
-
res
|
|
19593
|
+
sendSafeError(res, e);
|
|
19268
19594
|
}
|
|
19269
19595
|
});
|
|
19270
19596
|
app.post("/api/commands", requireAuth(), async (req, res) => {
|
|
@@ -19278,7 +19604,7 @@ function registerOsRoutes(ctx) {
|
|
|
19278
19604
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
19279
19605
|
res.json({ success: true, result });
|
|
19280
19606
|
} catch (err) {
|
|
19281
|
-
res
|
|
19607
|
+
sendSafeError(res, err);
|
|
19282
19608
|
}
|
|
19283
19609
|
});
|
|
19284
19610
|
app.get("/api/commands/registered", (req, res) => {
|
|
@@ -19286,7 +19612,7 @@ function registerOsRoutes(ctx) {
|
|
|
19286
19612
|
const actions = kernelContainer.actionRegistry.getAllActions();
|
|
19287
19613
|
res.json(actions);
|
|
19288
19614
|
} catch (err) {
|
|
19289
|
-
res
|
|
19615
|
+
sendSafeError(res, err);
|
|
19290
19616
|
}
|
|
19291
19617
|
});
|
|
19292
19618
|
app.get("/api/activities", async (req, res) => {
|
|
@@ -19295,7 +19621,7 @@ function registerOsRoutes(ctx) {
|
|
|
19295
19621
|
const list = role === "all" ? activityRegistry.listDescriptors() : activityRegistry.listByRole(role).map((p) => p.descriptor);
|
|
19296
19622
|
res.json({ activities: list });
|
|
19297
19623
|
} catch (err) {
|
|
19298
|
-
res
|
|
19624
|
+
sendSafeError(res, err);
|
|
19299
19625
|
}
|
|
19300
19626
|
});
|
|
19301
19627
|
app.post("/api/activities/:id/start", async (req, res) => {
|
|
@@ -19317,7 +19643,7 @@ function registerOsRoutes(ctx) {
|
|
|
19317
19643
|
res.json({ ok: true, ...result });
|
|
19318
19644
|
} catch (err) {
|
|
19319
19645
|
const status = err?.code === "PERMISSION_DENIED" ? 403 : 500;
|
|
19320
|
-
res
|
|
19646
|
+
sendSafeError(res, err, status);
|
|
19321
19647
|
}
|
|
19322
19648
|
});
|
|
19323
19649
|
app.get("/api/activities/running", (_req, res) => {
|
|
@@ -19333,7 +19659,7 @@ function registerOsRoutes(ctx) {
|
|
|
19333
19659
|
}));
|
|
19334
19660
|
res.json({ activities });
|
|
19335
19661
|
} catch (err) {
|
|
19336
|
-
res
|
|
19662
|
+
sendSafeError(res, err);
|
|
19337
19663
|
}
|
|
19338
19664
|
});
|
|
19339
19665
|
app.post("/api/activities/:id/finish", async (req, res) => {
|
|
@@ -19352,7 +19678,7 @@ function registerOsRoutes(ctx) {
|
|
|
19352
19678
|
await provider.finish(context);
|
|
19353
19679
|
res.json({ ok: true });
|
|
19354
19680
|
} catch (err) {
|
|
19355
|
-
res
|
|
19681
|
+
sendSafeError(res, err);
|
|
19356
19682
|
}
|
|
19357
19683
|
});
|
|
19358
19684
|
app.post("/api/activities/:id/pause", async (req, res) => {
|
|
@@ -19371,7 +19697,7 @@ function registerOsRoutes(ctx) {
|
|
|
19371
19697
|
await provider.pause(context);
|
|
19372
19698
|
res.json({ ok: true });
|
|
19373
19699
|
} catch (err) {
|
|
19374
|
-
res
|
|
19700
|
+
sendSafeError(res, err);
|
|
19375
19701
|
}
|
|
19376
19702
|
});
|
|
19377
19703
|
app.post("/api/activities/:id/resume", async (req, res) => {
|
|
@@ -19390,7 +19716,7 @@ function registerOsRoutes(ctx) {
|
|
|
19390
19716
|
await provider.resume(context);
|
|
19391
19717
|
res.json({ ok: true });
|
|
19392
19718
|
} catch (err) {
|
|
19393
|
-
res
|
|
19719
|
+
sendSafeError(res, err);
|
|
19394
19720
|
}
|
|
19395
19721
|
});
|
|
19396
19722
|
app.post("/api/agent/chat", async (req, res) => {
|
|
@@ -19448,7 +19774,7 @@ function registerOsRoutes(ctx) {
|
|
|
19448
19774
|
});
|
|
19449
19775
|
} catch (err) {
|
|
19450
19776
|
console.error(err);
|
|
19451
|
-
res
|
|
19777
|
+
sendSafeError(res, err);
|
|
19452
19778
|
}
|
|
19453
19779
|
});
|
|
19454
19780
|
app.get("/api/agent/conversations", (req, res) => {
|
|
@@ -19468,7 +19794,7 @@ function registerOsRoutes(ctx) {
|
|
|
19468
19794
|
messages: rows.map((r) => ({ role: r.role, content: r.content, createdAt: r.created_at }))
|
|
19469
19795
|
});
|
|
19470
19796
|
} catch (e) {
|
|
19471
|
-
res
|
|
19797
|
+
sendSafeError(res, e);
|
|
19472
19798
|
}
|
|
19473
19799
|
});
|
|
19474
19800
|
app.delete("/api/agent/conversations", (req, res) => {
|
|
@@ -19484,34 +19810,20 @@ function registerOsRoutes(ctx) {
|
|
|
19484
19810
|
kernelContainer.db.prepare("DELETE FROM agent_conversations WHERE conv_key = ?").run(convKey);
|
|
19485
19811
|
res.json({ success: true });
|
|
19486
19812
|
} catch (e) {
|
|
19487
|
-
res
|
|
19813
|
+
sendSafeError(res, e);
|
|
19488
19814
|
}
|
|
19489
19815
|
});
|
|
19490
19816
|
}
|
|
19491
19817
|
|
|
19492
19818
|
// server/routes/resources.ts
|
|
19493
19819
|
function registerResourcesRoutes(ctx) {
|
|
19494
|
-
const {
|
|
19495
|
-
app,
|
|
19496
|
-
io,
|
|
19497
|
-
loginLimiter,
|
|
19498
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
19499
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
19500
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
19501
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
19502
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
19503
|
-
buildOpenAITools: buildOpenAITools2,
|
|
19504
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
19505
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
19506
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
19507
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
19508
|
-
} = ctx;
|
|
19820
|
+
const { app } = ctx;
|
|
19509
19821
|
app.get("/api/resources", (req, res) => {
|
|
19510
19822
|
try {
|
|
19511
19823
|
const resources = kernelContainer.db.prepare("SELECT id, name, type, created_at FROM system_resources ORDER BY created_at DESC").all();
|
|
19512
19824
|
res.json(resources);
|
|
19513
19825
|
} catch (e) {
|
|
19514
|
-
res
|
|
19826
|
+
sendSafeError(res, e);
|
|
19515
19827
|
}
|
|
19516
19828
|
});
|
|
19517
19829
|
app.get("/api/resources/:id", (req, res) => {
|
|
@@ -19526,6 +19838,7 @@ function registerResourcesRoutes(ctx) {
|
|
|
19526
19838
|
).run(resource.id, resource.id, resource.name, "html", "index.html", resource.created_at || Date.now());
|
|
19527
19839
|
}
|
|
19528
19840
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
19841
|
+
res.setHeader("Content-Security-Policy", "sandbox allow-scripts allow-forms allow-downloads; default-src 'self' 'unsafe-inline' data: blob: https:;");
|
|
19529
19842
|
let html2 = resource.content || "";
|
|
19530
19843
|
const baseTag2 = `<base href="/api/resources/${req.params.id}/">`;
|
|
19531
19844
|
if (html2.toLowerCase().includes("<head>")) {
|
|
@@ -19570,7 +19883,7 @@ function registerResourcesRoutes(ctx) {
|
|
|
19570
19883
|
html = injectLmsSdk(html, req, { id: resource.id, name: resource.name, uuid: resource.id });
|
|
19571
19884
|
return res.send(html);
|
|
19572
19885
|
} catch (e) {
|
|
19573
|
-
res
|
|
19886
|
+
sendSafeError(res, e);
|
|
19574
19887
|
}
|
|
19575
19888
|
});
|
|
19576
19889
|
app.get("/api/resources/:id/*", (req, res) => {
|
|
@@ -19656,15 +19969,16 @@ function registerResourcesRoutes(ctx) {
|
|
|
19656
19969
|
} else {
|
|
19657
19970
|
let content = fileObj.content;
|
|
19658
19971
|
if (contentType.startsWith("text/html")) {
|
|
19972
|
+
res.setHeader("Content-Security-Policy", "sandbox allow-scripts allow-forms allow-downloads; default-src 'self' 'unsafe-inline' data: blob: https:;");
|
|
19659
19973
|
content = injectLmsSdk(content, req, { id: resource.id, name: resource.name, uuid: resource.id });
|
|
19660
19974
|
}
|
|
19661
19975
|
return res.send(content);
|
|
19662
19976
|
}
|
|
19663
19977
|
} catch (e) {
|
|
19664
|
-
res
|
|
19978
|
+
sendSafeError(res, e);
|
|
19665
19979
|
}
|
|
19666
19980
|
});
|
|
19667
|
-
app.post("/api/resources", async (req, res) => {
|
|
19981
|
+
app.post("/api/resources", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
19668
19982
|
try {
|
|
19669
19983
|
const { name, type, content } = req.body;
|
|
19670
19984
|
if (!name || !type) {
|
|
@@ -19721,15 +20035,15 @@ function registerResourcesRoutes(ctx) {
|
|
|
19721
20035
|
}
|
|
19722
20036
|
res.json({ success: true, id, name, type });
|
|
19723
20037
|
} catch (e) {
|
|
19724
|
-
res
|
|
20038
|
+
sendSafeError(res, e);
|
|
19725
20039
|
}
|
|
19726
20040
|
});
|
|
19727
|
-
app.delete("/api/resources/:id", (req, res) => {
|
|
20041
|
+
app.delete("/api/resources/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
19728
20042
|
try {
|
|
19729
20043
|
kernelContainer.db.prepare("DELETE FROM system_resources WHERE id = ?").run(req.params.id);
|
|
19730
20044
|
res.json({ success: true });
|
|
19731
20045
|
} catch (e) {
|
|
19732
|
-
res
|
|
20046
|
+
sendSafeError(res, e);
|
|
19733
20047
|
}
|
|
19734
20048
|
});
|
|
19735
20049
|
const RESOURCE_HANDLERS = {
|
|
@@ -19779,61 +20093,51 @@ function registerResourcesRoutes(ctx) {
|
|
|
19779
20093
|
}
|
|
19780
20094
|
|
|
19781
20095
|
// server/routes/courseware.ts
|
|
19782
|
-
var
|
|
19783
|
-
var
|
|
20096
|
+
var import_path10 = __toESM(require("path"), 1);
|
|
20097
|
+
var import_fs10 = __toESM(require("fs"), 1);
|
|
19784
20098
|
var import_crypto9 = __toESM(require("crypto"), 1);
|
|
19785
20099
|
function registerCoursewareRoutes(ctx) {
|
|
19786
|
-
const {
|
|
19787
|
-
|
|
19788
|
-
io,
|
|
19789
|
-
loginLimiter,
|
|
19790
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
19791
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
19792
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
19793
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
19794
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
19795
|
-
buildOpenAITools: buildOpenAITools2,
|
|
19796
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
19797
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
19798
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
19799
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
19800
|
-
} = ctx;
|
|
19801
|
-
app.post("/api/courseware/upload", async (req, res) => {
|
|
20100
|
+
const { app, io } = ctx;
|
|
20101
|
+
app.post("/api/courseware/upload", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
19802
20102
|
try {
|
|
19803
20103
|
const { name, filename, base64Data } = req.body;
|
|
19804
|
-
const
|
|
20104
|
+
const actorId = getActorId(req) || "teacher";
|
|
20105
|
+
const cmd = kernelContainer.commandBus.createCommand("courseware.upload", { name, filename, base64Data }, actorId);
|
|
19805
20106
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
19806
20107
|
res.json(result);
|
|
19807
20108
|
} catch (e) {
|
|
19808
|
-
res
|
|
20109
|
+
sendSafeError(res, e);
|
|
19809
20110
|
}
|
|
19810
20111
|
});
|
|
19811
|
-
app.post("/api/courseware/confirm", async (req, res) => {
|
|
20112
|
+
app.post("/api/courseware/confirm", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
19812
20113
|
try {
|
|
19813
20114
|
const { uuid, name, entry } = req.body;
|
|
19814
|
-
const
|
|
20115
|
+
const actorId = getActorId(req) || "teacher";
|
|
20116
|
+
const cmd = kernelContainer.commandBus.createCommand("courseware.confirm", { uuid, name, entry }, actorId);
|
|
19815
20117
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
19816
20118
|
res.json(result);
|
|
19817
20119
|
} catch (e) {
|
|
19818
|
-
res
|
|
20120
|
+
sendSafeError(res, e);
|
|
19819
20121
|
}
|
|
19820
20122
|
});
|
|
19821
|
-
app.get("/api/courseware", async (req, res) => {
|
|
20123
|
+
app.get("/api/courseware", requireAuth(), async (req, res) => {
|
|
19822
20124
|
try {
|
|
19823
|
-
const
|
|
20125
|
+
const actorId = getActorId(req) || "system";
|
|
20126
|
+
const cmd = kernelContainer.commandBus.createCommand("courseware.list", {}, actorId);
|
|
19824
20127
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
19825
20128
|
res.json(result);
|
|
19826
20129
|
} catch (e) {
|
|
19827
|
-
res
|
|
20130
|
+
sendSafeError(res, e);
|
|
19828
20131
|
}
|
|
19829
20132
|
});
|
|
19830
|
-
app.delete("/api/courseware/:id", async (req, res) => {
|
|
20133
|
+
app.delete("/api/courseware/:id", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
19831
20134
|
try {
|
|
19832
|
-
const
|
|
20135
|
+
const actorId = getActorId(req) || "teacher";
|
|
20136
|
+
const cmd = kernelContainer.commandBus.createCommand("courseware.delete", { id: req.params.id }, actorId);
|
|
19833
20137
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
19834
20138
|
res.json(result);
|
|
19835
20139
|
} catch (e) {
|
|
19836
|
-
res
|
|
20140
|
+
sendSafeError(res, e);
|
|
19837
20141
|
}
|
|
19838
20142
|
});
|
|
19839
20143
|
function extractScoreCommentCompletion(payload) {
|
|
@@ -19926,6 +20230,19 @@ function registerCoursewareRoutes(ctx) {
|
|
|
19926
20230
|
try {
|
|
19927
20231
|
const { attemptId } = req.params;
|
|
19928
20232
|
const { eventType, payload } = req.body;
|
|
20233
|
+
const token = getCookieToken(req);
|
|
20234
|
+
const session = req.session || (token ? getValidSession(token) : null);
|
|
20235
|
+
if (!session) {
|
|
20236
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
20237
|
+
}
|
|
20238
|
+
if (session.role !== "teacher" && session.role !== "administrator") {
|
|
20239
|
+
const attemptRow = kernelContainer.db.prepare(
|
|
20240
|
+
"SELECT student_id FROM courseware_attempt WHERE id = ?"
|
|
20241
|
+
).get(attemptId);
|
|
20242
|
+
if (attemptRow && attemptRow.student_id !== session.userId) {
|
|
20243
|
+
return res.status(403).json({ error: "Forbidden: Cannot modify logs for another student" });
|
|
20244
|
+
}
|
|
20245
|
+
}
|
|
19929
20246
|
const rawId = "raw_" + import_crypto9.default.randomBytes(8).toString("hex");
|
|
19930
20247
|
kernelContainer.db.prepare(
|
|
19931
20248
|
"INSERT INTO submission_raw (id, attempt_id, event_type, payload_json, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
@@ -19979,15 +20296,36 @@ function registerCoursewareRoutes(ctx) {
|
|
|
19979
20296
|
}
|
|
19980
20297
|
}
|
|
19981
20298
|
io.emit("courseware-attempt-updated", { attemptId, type: "log" });
|
|
20299
|
+
void kernelContainer.eventBus.publish({
|
|
20300
|
+
id: "evt_" + import_crypto9.default.randomBytes(8).toString("hex"),
|
|
20301
|
+
type: "courseware.event_logged",
|
|
20302
|
+
source: "builtin.courseware",
|
|
20303
|
+
payload: { attemptId, eventType, payload },
|
|
20304
|
+
timestamp: Date.now(),
|
|
20305
|
+
correlationId: attemptId
|
|
20306
|
+
});
|
|
19982
20307
|
res.json({ success: true });
|
|
19983
20308
|
} catch (e) {
|
|
19984
|
-
res
|
|
20309
|
+
sendSafeError(res, e);
|
|
19985
20310
|
}
|
|
19986
20311
|
});
|
|
19987
20312
|
app.post("/api/courseware/attempts/:attemptId/submit", async (req, res) => {
|
|
19988
20313
|
try {
|
|
19989
20314
|
const { attemptId } = req.params;
|
|
19990
20315
|
let { score, comment, completion, status, extra = {} } = req.body;
|
|
20316
|
+
const token = getCookieToken(req);
|
|
20317
|
+
const session = req.session || (token ? getValidSession(token) : null);
|
|
20318
|
+
if (!session) {
|
|
20319
|
+
return res.status(401).json({ error: "Authentication required to submit attempt scores" });
|
|
20320
|
+
}
|
|
20321
|
+
if (session.role !== "teacher" && session.role !== "administrator") {
|
|
20322
|
+
const attemptRow = kernelContainer.db.prepare(
|
|
20323
|
+
"SELECT student_id FROM courseware_attempt WHERE id = ?"
|
|
20324
|
+
).get(attemptId);
|
|
20325
|
+
if (attemptRow && attemptRow.student_id !== session.userId) {
|
|
20326
|
+
return res.status(403).json({ error: "Forbidden: Cannot submit scores for another student" });
|
|
20327
|
+
}
|
|
20328
|
+
}
|
|
19991
20329
|
const extracted = extractScoreCommentCompletion({ ...req.body, ...extra });
|
|
19992
20330
|
if (score === void 0 || score === null) score = extracted.score;
|
|
19993
20331
|
if (comment === void 0 || comment === null) comment = extracted.comment;
|
|
@@ -20006,6 +20344,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20006
20344
|
parsedCompletion = num;
|
|
20007
20345
|
}
|
|
20008
20346
|
}
|
|
20347
|
+
const actorId = session.userId || getActorId(req) || "student";
|
|
20009
20348
|
const cmd = kernelContainer.commandBus.createCommand("courseware.submit_attempt", {
|
|
20010
20349
|
attemptId,
|
|
20011
20350
|
score: parsedScore,
|
|
@@ -20013,12 +20352,12 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20013
20352
|
completion: parsedCompletion,
|
|
20014
20353
|
status,
|
|
20015
20354
|
extra
|
|
20016
|
-
},
|
|
20355
|
+
}, actorId);
|
|
20017
20356
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20018
20357
|
io.emit("courseware-attempt-updated", { attemptId, type: "submit" });
|
|
20019
20358
|
res.json(result);
|
|
20020
20359
|
} catch (e) {
|
|
20021
|
-
res
|
|
20360
|
+
sendSafeError(res, e);
|
|
20022
20361
|
}
|
|
20023
20362
|
});
|
|
20024
20363
|
app.get("/api/courseware/attempts", (req, res) => {
|
|
@@ -20043,7 +20382,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20043
20382
|
`).all();
|
|
20044
20383
|
res.json(rows);
|
|
20045
20384
|
} catch (e) {
|
|
20046
|
-
res
|
|
20385
|
+
sendSafeError(res, e);
|
|
20047
20386
|
}
|
|
20048
20387
|
});
|
|
20049
20388
|
app.post("/api/courseware/debug", (req, res) => {
|
|
@@ -20051,12 +20390,12 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20051
20390
|
const { msg, url, student, courseware } = req.body;
|
|
20052
20391
|
const logMsg = `[CLIENT DEBUG] ${msg} | URL: ${url} | Student: ${JSON.stringify(student)} | Courseware: ${JSON.stringify(courseware)}`;
|
|
20053
20392
|
console.log(`\x1B[35m[CLIENT DEBUG]\x1B[0m ${msg}`);
|
|
20054
|
-
const logFile2 =
|
|
20055
|
-
|
|
20393
|
+
const logFile2 = import_path10.default.join(process.cwd(), "client_debug.log");
|
|
20394
|
+
import_fs10.default.appendFileSync(logFile2, `${(/* @__PURE__ */ new Date()).toISOString()} - ${logMsg}
|
|
20056
20395
|
`);
|
|
20057
20396
|
res.json({ success: true });
|
|
20058
20397
|
} catch (e) {
|
|
20059
|
-
res
|
|
20398
|
+
sendSafeError(res, e);
|
|
20060
20399
|
}
|
|
20061
20400
|
});
|
|
20062
20401
|
app.get("/api/courseware/attempts/:attemptId/raw", async (req, res) => {
|
|
@@ -20066,7 +20405,28 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20066
20405
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20067
20406
|
res.json(result);
|
|
20068
20407
|
} catch (e) {
|
|
20069
|
-
res
|
|
20408
|
+
sendSafeError(res, e);
|
|
20409
|
+
}
|
|
20410
|
+
});
|
|
20411
|
+
app.get("/api/courseware/attempts/:attemptId/progress", (req, res) => {
|
|
20412
|
+
try {
|
|
20413
|
+
const { attemptId } = req.params;
|
|
20414
|
+
const result = kernelContainer.db.prepare(
|
|
20415
|
+
"SELECT score, comment, completion, extra_json FROM submission_result WHERE attempt_id = ?"
|
|
20416
|
+
).get(attemptId);
|
|
20417
|
+
if (!result) {
|
|
20418
|
+
return res.json({ progress: null });
|
|
20419
|
+
}
|
|
20420
|
+
let extra = {};
|
|
20421
|
+
try {
|
|
20422
|
+
extra = JSON.parse(result.extra_json || "{}");
|
|
20423
|
+
} catch {
|
|
20424
|
+
}
|
|
20425
|
+
res.json({
|
|
20426
|
+
progress: { score: result.score, comment: result.comment, completion: result.completion, extra }
|
|
20427
|
+
});
|
|
20428
|
+
} catch (e) {
|
|
20429
|
+
sendSafeError(res, e);
|
|
20070
20430
|
}
|
|
20071
20431
|
});
|
|
20072
20432
|
app.post("/api/courseware/attempts/:attemptId/promote", async (req, res) => {
|
|
@@ -20157,7 +20517,7 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20157
20517
|
});
|
|
20158
20518
|
res.json({ success: true, assignmentId, score: finalScore });
|
|
20159
20519
|
} catch (e) {
|
|
20160
|
-
res
|
|
20520
|
+
sendSafeError(res, e);
|
|
20161
20521
|
}
|
|
20162
20522
|
});
|
|
20163
20523
|
app.get("/api/courseware/:id", (req, res) => {
|
|
@@ -20174,30 +20534,16 @@ function registerCoursewareRoutes(ctx) {
|
|
|
20174
20534
|
const html = injectLmsSdk(node.content || "", req, { id: node.id, name: node.name, uuid: node.id });
|
|
20175
20535
|
res.send(html);
|
|
20176
20536
|
} catch (e) {
|
|
20177
|
-
res
|
|
20537
|
+
sendSafeError(res, e);
|
|
20178
20538
|
}
|
|
20179
20539
|
});
|
|
20180
20540
|
}
|
|
20181
20541
|
|
|
20182
20542
|
// server/routes/bridge.ts
|
|
20183
|
-
var
|
|
20184
|
-
var
|
|
20543
|
+
var import_path11 = __toESM(require("path"), 1);
|
|
20544
|
+
var import_fs11 = __toESM(require("fs"), 1);
|
|
20185
20545
|
function registerBridgeRoutes(ctx) {
|
|
20186
|
-
const {
|
|
20187
|
-
app,
|
|
20188
|
-
io,
|
|
20189
|
-
loginLimiter,
|
|
20190
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
20191
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
20192
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
20193
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
20194
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
20195
|
-
buildOpenAITools: buildOpenAITools2,
|
|
20196
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
20197
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
20198
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
20199
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
20200
|
-
} = ctx;
|
|
20546
|
+
const { app } = ctx;
|
|
20201
20547
|
app.get("/bridge.js", (_req, res) => {
|
|
20202
20548
|
res.setHeader("Content-Type", "application/javascript; charset=utf-8");
|
|
20203
20549
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -20218,23 +20564,23 @@ function registerBridgeRoutes(ctx) {
|
|
|
20218
20564
|
if (!subpath || subpath === "") {
|
|
20219
20565
|
subpath = courseware.entry;
|
|
20220
20566
|
}
|
|
20221
|
-
const storageDir =
|
|
20222
|
-
const filePath =
|
|
20567
|
+
const storageDir = import_path11.default.resolve(process.cwd(), "storage", "courseware", uuid);
|
|
20568
|
+
const filePath = import_path11.default.resolve(storageDir, subpath);
|
|
20223
20569
|
if (!filePath.startsWith(storageDir)) {
|
|
20224
20570
|
return res.status(403).send("Access denied");
|
|
20225
20571
|
}
|
|
20226
|
-
if (!
|
|
20572
|
+
if (!import_fs11.default.existsSync(filePath)) {
|
|
20227
20573
|
return res.status(404).send(`File not found: ${subpath}`);
|
|
20228
20574
|
}
|
|
20229
|
-
const stat =
|
|
20575
|
+
const stat = import_fs11.default.statSync(filePath);
|
|
20230
20576
|
if (stat.isDirectory()) {
|
|
20231
|
-
const indexHtml =
|
|
20232
|
-
if (
|
|
20577
|
+
const indexHtml = import_path11.default.join(filePath, "index.html");
|
|
20578
|
+
if (import_fs11.default.existsSync(indexHtml)) {
|
|
20233
20579
|
return res.redirect(`/runtime/${uuid}/${subpath.endsWith("/") ? subpath : subpath + "/"}index.html`);
|
|
20234
20580
|
}
|
|
20235
20581
|
return res.status(404).send("Directory index not found");
|
|
20236
20582
|
}
|
|
20237
|
-
const ext =
|
|
20583
|
+
const ext = import_path11.default.extname(filePath).toLowerCase();
|
|
20238
20584
|
let contentType = "text/plain; charset=utf-8";
|
|
20239
20585
|
if (ext === ".html" || ext === ".htm") contentType = "text/html; charset=utf-8";
|
|
20240
20586
|
else if (ext === ".css") contentType = "text/css; charset=utf-8";
|
|
@@ -20250,14 +20596,14 @@ function registerBridgeRoutes(ctx) {
|
|
|
20250
20596
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
20251
20597
|
const isHtml = ext === ".html" || ext === ".htm";
|
|
20252
20598
|
if (isHtml) {
|
|
20253
|
-
let html =
|
|
20599
|
+
let html = import_fs11.default.readFileSync(filePath, "utf8");
|
|
20254
20600
|
html = injectLmsSdk(html, req, { id: courseware.id, name: courseware.name, uuid: courseware.uuid });
|
|
20255
20601
|
return res.send(html);
|
|
20256
20602
|
} else {
|
|
20257
20603
|
return res.sendFile(filePath);
|
|
20258
20604
|
}
|
|
20259
20605
|
} catch (e) {
|
|
20260
|
-
res
|
|
20606
|
+
sendSafeError(res, e, 500, "Failed to load courseware runtime file");
|
|
20261
20607
|
}
|
|
20262
20608
|
});
|
|
20263
20609
|
}
|
|
@@ -20265,27 +20611,89 @@ function registerBridgeRoutes(ctx) {
|
|
|
20265
20611
|
// server/routes/lessons.ts
|
|
20266
20612
|
var import_genai3 = require("@google/genai");
|
|
20267
20613
|
var import_uuid16 = require("uuid");
|
|
20614
|
+
function checkLessonOwnership(req, lessonId) {
|
|
20615
|
+
const token = getCookieToken(req);
|
|
20616
|
+
const session = req.session || (token ? getValidSession(token) : null);
|
|
20617
|
+
if (!session) {
|
|
20618
|
+
return { allowed: false, status: 401, error: "Authentication required" };
|
|
20619
|
+
}
|
|
20620
|
+
const isAdmin = session.username === "admin" || session.userId === "usr_admin" || session.role === "admin" || session.role === "administrator";
|
|
20621
|
+
const isTeacherOrAdmin = isAdmin || session.role === "teacher";
|
|
20622
|
+
if (!isTeacherOrAdmin) {
|
|
20623
|
+
return { allowed: false, status: 403, error: "Forbidden: Only teachers or administrators can modify lessons" };
|
|
20624
|
+
}
|
|
20625
|
+
const lesson = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(lessonId);
|
|
20626
|
+
if (!lesson) {
|
|
20627
|
+
return { allowed: false, status: 404, error: "Lesson not found" };
|
|
20628
|
+
}
|
|
20629
|
+
if (isAdmin) {
|
|
20630
|
+
return { allowed: true, status: 200, lesson };
|
|
20631
|
+
}
|
|
20632
|
+
if (!lesson.creator_id || lesson.creator_id === session.userId || lesson.creator_id === session.username) {
|
|
20633
|
+
return { allowed: true, status: 200, lesson };
|
|
20634
|
+
}
|
|
20635
|
+
return {
|
|
20636
|
+
allowed: false,
|
|
20637
|
+
status: 403,
|
|
20638
|
+
error: "Forbidden: You do not have permission to modify this lesson because it was created by another teacher"
|
|
20639
|
+
};
|
|
20640
|
+
}
|
|
20641
|
+
function requireWhiteboardWriteAccess() {
|
|
20642
|
+
return (req, res, next) => {
|
|
20643
|
+
const { id } = req.params;
|
|
20644
|
+
const token = getCookieToken(req);
|
|
20645
|
+
const session = req.session || (token ? getValidSession(token) : null);
|
|
20646
|
+
if (!session) {
|
|
20647
|
+
return res.status(401).json({ success: false, error: "Authentication required" });
|
|
20648
|
+
}
|
|
20649
|
+
req.session = session;
|
|
20650
|
+
const isAdmin = session.username === "admin" || session.userId === "usr_admin" || session.role === "admin" || session.role === "administrator";
|
|
20651
|
+
if (id.startsWith("assignment-")) {
|
|
20652
|
+
if (isAdmin || session.role === "teacher") {
|
|
20653
|
+
return next();
|
|
20654
|
+
}
|
|
20655
|
+
if (session.role === "student") {
|
|
20656
|
+
const studentMatch = id.match(/-student-([^-]+)$/);
|
|
20657
|
+
const targetStudentId = studentMatch ? studentMatch[1] : null;
|
|
20658
|
+
const currentStudentId = session.studentId || session.userId;
|
|
20659
|
+
if (targetStudentId && currentStudentId && targetStudentId === currentStudentId) {
|
|
20660
|
+
return next();
|
|
20661
|
+
}
|
|
20662
|
+
return res.status(403).json({
|
|
20663
|
+
success: false,
|
|
20664
|
+
error: "Forbidden: You do not have permission to modify another student's assignment whiteboard"
|
|
20665
|
+
});
|
|
20666
|
+
}
|
|
20667
|
+
return res.status(403).json({ success: false, error: "Forbidden: Invalid role for whiteboard write" });
|
|
20668
|
+
}
|
|
20669
|
+
const isTeacherOrAdmin = isAdmin || session.role === "teacher";
|
|
20670
|
+
if (!isTeacherOrAdmin) {
|
|
20671
|
+
return res.status(403).json({
|
|
20672
|
+
success: false,
|
|
20673
|
+
error: "Forbidden: Students cannot modify classroom whiteboards"
|
|
20674
|
+
});
|
|
20675
|
+
}
|
|
20676
|
+
const lesson = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(id);
|
|
20677
|
+
if (!lesson) {
|
|
20678
|
+
return res.status(404).json({ success: false, error: "Lesson not found" });
|
|
20679
|
+
}
|
|
20680
|
+
if (isAdmin || !lesson.creator_id || lesson.creator_id === session.userId || lesson.creator_id === session.username) {
|
|
20681
|
+
return next();
|
|
20682
|
+
}
|
|
20683
|
+
return res.status(403).json({
|
|
20684
|
+
success: false,
|
|
20685
|
+
error: "Forbidden: You do not have permission to modify this lesson whiteboard because it was created by another teacher"
|
|
20686
|
+
});
|
|
20687
|
+
};
|
|
20688
|
+
}
|
|
20268
20689
|
function registerLessonsRoutes(ctx) {
|
|
20269
|
-
const {
|
|
20270
|
-
app,
|
|
20271
|
-
io,
|
|
20272
|
-
loginLimiter,
|
|
20273
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
20274
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
20275
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
20276
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
20277
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
20278
|
-
buildOpenAITools: buildOpenAITools2,
|
|
20279
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
20280
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
20281
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
20282
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
20283
|
-
} = ctx;
|
|
20690
|
+
const { app, io } = ctx;
|
|
20284
20691
|
app.get("/api/lessons", (req, res) => {
|
|
20285
20692
|
const lessons = kernelContainer.db.prepare(`
|
|
20286
|
-
SELECT l.*,
|
|
20693
|
+
SELECT l.*, u.name as creator_name,
|
|
20287
20694
|
(SELECT COUNT(*) FROM student_lesson_progress WHERE lesson_id = l.id) as enrollment_count
|
|
20288
20695
|
FROM lessons l
|
|
20696
|
+
LEFT JOIN users u ON l.creator_id = u.id
|
|
20289
20697
|
ORDER BY l.created_at DESC
|
|
20290
20698
|
`).all();
|
|
20291
20699
|
res.json(lessons);
|
|
@@ -20301,7 +20709,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
20301
20709
|
`).all(lessonId);
|
|
20302
20710
|
res.json(rows);
|
|
20303
20711
|
} catch (err) {
|
|
20304
|
-
res
|
|
20712
|
+
sendSafeError(res, err);
|
|
20305
20713
|
}
|
|
20306
20714
|
});
|
|
20307
20715
|
app.get("/api/lessons/:lessonId/eval-grades", (req, res) => {
|
|
@@ -20345,7 +20753,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
20345
20753
|
}
|
|
20346
20754
|
res.json(result);
|
|
20347
20755
|
} catch (err) {
|
|
20348
|
-
res
|
|
20756
|
+
sendSafeError(res, err);
|
|
20349
20757
|
}
|
|
20350
20758
|
});
|
|
20351
20759
|
app.get("/api/eval-submissions/:submissionId/reviews", (req, res) => {
|
|
@@ -20359,7 +20767,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
20359
20767
|
`).all(submissionId);
|
|
20360
20768
|
res.json(rows);
|
|
20361
20769
|
} catch (err) {
|
|
20362
|
-
res
|
|
20770
|
+
sendSafeError(res, err);
|
|
20363
20771
|
}
|
|
20364
20772
|
});
|
|
20365
20773
|
app.get("/api/lessons/:lessonId/students/:studentId/eval-status", (req, res) => {
|
|
@@ -20396,43 +20804,55 @@ function registerLessonsRoutes(ctx) {
|
|
|
20396
20804
|
grade
|
|
20397
20805
|
});
|
|
20398
20806
|
} catch (err) {
|
|
20399
|
-
res
|
|
20807
|
+
sendSafeError(res, err);
|
|
20400
20808
|
}
|
|
20401
20809
|
});
|
|
20402
|
-
app.post("/api/lessons", async (req, res) => {
|
|
20810
|
+
app.post("/api/lessons", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
20403
20811
|
try {
|
|
20404
20812
|
const { title, content } = req.body;
|
|
20813
|
+
const session = req.session;
|
|
20814
|
+
const creatorId = session?.userId || session?.username || "usr_teacher";
|
|
20815
|
+
const actorId = getActorId(req) || "teacher";
|
|
20405
20816
|
const cmd = kernelContainer.commandBus.createCommand(
|
|
20406
20817
|
"lesson.create",
|
|
20407
|
-
{ title, content },
|
|
20408
|
-
|
|
20818
|
+
{ title, content, creatorId },
|
|
20819
|
+
actorId,
|
|
20409
20820
|
{ approved: true }
|
|
20410
20821
|
);
|
|
20411
20822
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20412
20823
|
res.json({ success: true, result });
|
|
20413
20824
|
} catch (e) {
|
|
20414
|
-
res
|
|
20825
|
+
sendSafeError(res, e);
|
|
20415
20826
|
}
|
|
20416
20827
|
});
|
|
20417
|
-
app.put("/api/lessons/:id/timeline", async (req, res) => {
|
|
20828
|
+
app.put("/api/lessons/:id/timeline", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
20418
20829
|
try {
|
|
20419
20830
|
const { id } = req.params;
|
|
20831
|
+
const ownership = checkLessonOwnership(req, id);
|
|
20832
|
+
if (!ownership.allowed) {
|
|
20833
|
+
return res.status(ownership.status).json({ success: false, error: ownership.error });
|
|
20834
|
+
}
|
|
20420
20835
|
const { timeline } = req.body;
|
|
20836
|
+
const actorId = getActorId(req) || "teacher";
|
|
20421
20837
|
const cmd = kernelContainer.commandBus.createCommand(
|
|
20422
20838
|
"lesson.update_timeline",
|
|
20423
20839
|
{ lessonId: id, timeline },
|
|
20424
|
-
|
|
20840
|
+
actorId,
|
|
20425
20841
|
{ approved: true }
|
|
20426
20842
|
);
|
|
20427
20843
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20428
20844
|
res.json({ success: true, result });
|
|
20429
20845
|
} catch (e) {
|
|
20430
|
-
res
|
|
20846
|
+
sendSafeError(res, e);
|
|
20431
20847
|
}
|
|
20432
20848
|
});
|
|
20433
|
-
app.put("/api/lessons/:id/progress-mode", async (req, res) => {
|
|
20849
|
+
app.put("/api/lessons/:id/progress-mode", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
20434
20850
|
try {
|
|
20435
20851
|
const { id } = req.params;
|
|
20852
|
+
const ownership = checkLessonOwnership(req, id);
|
|
20853
|
+
if (!ownership.allowed) {
|
|
20854
|
+
return res.status(ownership.status).json({ success: false, error: ownership.error });
|
|
20855
|
+
}
|
|
20436
20856
|
const { progressMode, progressConditions } = req.body;
|
|
20437
20857
|
const conditionsStr = typeof progressConditions === "string" ? progressConditions : JSON.stringify(progressConditions || null);
|
|
20438
20858
|
kernelContainer.db.prepare("UPDATE lessons SET progress_mode = ?, progress_conditions = ?, updated_at = ? WHERE id = ?").run(progressMode || "manual", conditionsStr, Date.now(), id);
|
|
@@ -20443,7 +20863,7 @@ function registerLessonsRoutes(ctx) {
|
|
|
20443
20863
|
});
|
|
20444
20864
|
res.json({ success: true });
|
|
20445
20865
|
} catch (e) {
|
|
20446
|
-
res
|
|
20866
|
+
sendSafeError(res, e);
|
|
20447
20867
|
}
|
|
20448
20868
|
});
|
|
20449
20869
|
app.get("/api/lessons/:id/whiteboard", (req, res) => {
|
|
@@ -20469,14 +20889,13 @@ function registerLessonsRoutes(ctx) {
|
|
|
20469
20889
|
}
|
|
20470
20890
|
res.json(elements);
|
|
20471
20891
|
});
|
|
20472
|
-
app.post("/api/lessons/:id/whiteboard/reset", async (req, res) => {
|
|
20892
|
+
app.post("/api/lessons/:id/whiteboard/reset", requireWhiteboardWriteAccess(), async (req, res) => {
|
|
20473
20893
|
try {
|
|
20474
20894
|
const id = req.params.id;
|
|
20475
20895
|
if (id.startsWith("assignment-")) {
|
|
20476
20896
|
const deleteStmt = kernelContainer.db.prepare("DELETE FROM whiteboard_elements WHERE lesson_id = ?");
|
|
20477
20897
|
deleteStmt.run(id);
|
|
20478
|
-
res.json({ success: true, message: "Assignment whiteboard reset to empty" });
|
|
20479
|
-
return;
|
|
20898
|
+
return res.json({ success: true, message: "Assignment whiteboard reset to empty" });
|
|
20480
20899
|
}
|
|
20481
20900
|
const snapshotId = `snapshot-${id}`;
|
|
20482
20901
|
const hasSnapshot = kernelContainer.db.prepare("SELECT count(*) as count FROM whiteboard_elements WHERE lesson_id = ?").get(snapshotId);
|
|
@@ -20499,94 +20918,77 @@ function registerLessonsRoutes(ctx) {
|
|
|
20499
20918
|
res.json({ success: true, message: "Lesson whiteboard cleared (no snapshot)" });
|
|
20500
20919
|
}
|
|
20501
20920
|
} catch (e) {
|
|
20502
|
-
res
|
|
20921
|
+
sendSafeError(res, e);
|
|
20503
20922
|
}
|
|
20504
20923
|
});
|
|
20505
|
-
app.post("/api/lessons/:id/whiteboard", async (req, res) => {
|
|
20924
|
+
app.post("/api/lessons/:id/whiteboard", requireWhiteboardWriteAccess(), async (req, res) => {
|
|
20506
20925
|
try {
|
|
20507
20926
|
const { id } = req.params;
|
|
20508
|
-
if (!id.startsWith("assignment-") && !checkIsTeacherOrAdmin(req)) {
|
|
20509
|
-
return res.status(403).json({ error: "Forbidden: Only teachers can draw on classroom whiteboards" });
|
|
20510
|
-
}
|
|
20511
20927
|
const { type, data } = req.body;
|
|
20928
|
+
const actorId = getActorId(req) || "user-frontend";
|
|
20512
20929
|
const cmd = kernelContainer.commandBus.createCommand("whiteboard.draw", {
|
|
20513
20930
|
lessonId: id,
|
|
20514
20931
|
type,
|
|
20515
20932
|
data: JSON.stringify(data)
|
|
20516
|
-
},
|
|
20933
|
+
}, actorId, { approved: true });
|
|
20517
20934
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20518
20935
|
res.json(result);
|
|
20519
20936
|
} catch (e) {
|
|
20520
|
-
res
|
|
20937
|
+
sendSafeError(res, e);
|
|
20521
20938
|
}
|
|
20522
20939
|
});
|
|
20523
|
-
app.put("/api/lessons/:id/whiteboard/:elementId", async (req, res) => {
|
|
20940
|
+
app.put("/api/lessons/:id/whiteboard/:elementId", requireWhiteboardWriteAccess(), async (req, res) => {
|
|
20524
20941
|
try {
|
|
20525
|
-
const { id } = req.params;
|
|
20526
|
-
if (!id.startsWith("assignment-") && !checkIsTeacherOrAdmin(req)) {
|
|
20527
|
-
return res.status(403).json({ error: "Forbidden: Only teachers can update elements on classroom whiteboards" });
|
|
20528
|
-
}
|
|
20942
|
+
const { id, elementId } = req.params;
|
|
20529
20943
|
const { data } = req.body;
|
|
20944
|
+
const actorId = getActorId(req) || "user-frontend";
|
|
20530
20945
|
const cmd = kernelContainer.commandBus.createCommand("whiteboard.update", {
|
|
20531
20946
|
lessonId: id,
|
|
20532
|
-
elementId
|
|
20947
|
+
elementId,
|
|
20533
20948
|
data: JSON.stringify(data)
|
|
20534
|
-
},
|
|
20949
|
+
}, actorId, { approved: true });
|
|
20535
20950
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20536
20951
|
res.json(result);
|
|
20537
20952
|
} catch (e) {
|
|
20538
|
-
res
|
|
20953
|
+
sendSafeError(res, e);
|
|
20539
20954
|
}
|
|
20540
20955
|
});
|
|
20541
|
-
app.delete("/api/lessons/:id/whiteboard", async (req, res) => {
|
|
20956
|
+
app.delete("/api/lessons/:id/whiteboard", requireWhiteboardWriteAccess(), async (req, res) => {
|
|
20542
20957
|
try {
|
|
20543
20958
|
const { id } = req.params;
|
|
20544
|
-
|
|
20545
|
-
return res.status(403).json({ error: "Forbidden: Only teachers can clear the classroom whiteboard" });
|
|
20546
|
-
}
|
|
20959
|
+
const actorId = getActorId(req) || "user-frontend";
|
|
20547
20960
|
const cmd = kernelContainer.commandBus.createCommand("whiteboard.clear", {
|
|
20548
20961
|
lessonId: id
|
|
20549
|
-
},
|
|
20962
|
+
}, actorId, { approved: true });
|
|
20550
20963
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20551
20964
|
res.json(result);
|
|
20552
20965
|
} catch (e) {
|
|
20553
|
-
res
|
|
20966
|
+
sendSafeError(res, e);
|
|
20554
20967
|
}
|
|
20555
20968
|
});
|
|
20556
|
-
app.delete("/api/lessons/:id/whiteboard/:elementId", async (req, res) => {
|
|
20969
|
+
app.delete("/api/lessons/:id/whiteboard/:elementId", requireWhiteboardWriteAccess(), async (req, res) => {
|
|
20557
20970
|
try {
|
|
20558
|
-
const { id } = req.params;
|
|
20559
|
-
|
|
20560
|
-
return res.status(403).json({ error: "Forbidden: Only teachers can delete elements from classroom whiteboards" });
|
|
20561
|
-
}
|
|
20971
|
+
const { id, elementId } = req.params;
|
|
20972
|
+
const actorId = getActorId(req) || "user-frontend";
|
|
20562
20973
|
const cmd = kernelContainer.commandBus.createCommand("whiteboard.delete", {
|
|
20563
20974
|
lessonId: id,
|
|
20564
|
-
elementId
|
|
20565
|
-
},
|
|
20975
|
+
elementId
|
|
20976
|
+
}, actorId, { approved: true });
|
|
20566
20977
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20567
20978
|
res.json(result);
|
|
20568
20979
|
} catch (e) {
|
|
20569
|
-
res
|
|
20980
|
+
sendSafeError(res, e);
|
|
20570
20981
|
}
|
|
20571
20982
|
});
|
|
20572
|
-
app.post("/api/lessons/:id/quiz-submit", async (req, res) => {
|
|
20983
|
+
app.post("/api/lessons/:id/quiz-submit", requireAuth("student", "teacher", "administrator"), async (req, res) => {
|
|
20573
20984
|
try {
|
|
20574
20985
|
const { id: lessonId } = req.params;
|
|
20575
20986
|
const { elementId, answer } = req.body;
|
|
20576
20987
|
if (!elementId || answer === void 0) {
|
|
20577
20988
|
return res.status(400).json({ error: "Missing elementId or answer" });
|
|
20578
20989
|
}
|
|
20579
|
-
const
|
|
20580
|
-
|
|
20581
|
-
if (token) {
|
|
20582
|
-
const sessionRow = kernelContainer.db.prepare("SELECT * FROM client_sessions WHERE id = ?").get(token);
|
|
20583
|
-
if (sessionRow) {
|
|
20584
|
-
const session = JSON.parse(sessionRow.session_data);
|
|
20585
|
-
if (session.role === "student") {
|
|
20586
|
-
studentId = session.studentId || session.userId || "guest";
|
|
20587
|
-
}
|
|
20588
|
-
}
|
|
20589
|
-
}
|
|
20990
|
+
const session = req.session;
|
|
20991
|
+
const studentId = session.studentId || session.userId || "guest";
|
|
20590
20992
|
const row = kernelContainer.db.prepare(
|
|
20591
20993
|
"SELECT data FROM whiteboard_elements WHERE id = ? AND lesson_id = ?"
|
|
20592
20994
|
).get(elementId, lessonId);
|
|
@@ -20610,15 +21012,12 @@ function registerLessonsRoutes(ctx) {
|
|
|
20610
21012
|
io.to(`lesson-${lessonId}`).emit("whiteboard-sync", { type: "element-updated", elementId });
|
|
20611
21013
|
res.json({ success: true, isCorrect, score, studentId });
|
|
20612
21014
|
} catch (e) {
|
|
20613
|
-
res
|
|
21015
|
+
sendSafeError(res, e);
|
|
20614
21016
|
}
|
|
20615
21017
|
});
|
|
20616
|
-
app.get("/api/lessons/:id/quiz-submissions", (req, res) => {
|
|
21018
|
+
app.get("/api/lessons/:id/quiz-submissions", requireAuth("teacher", "administrator"), (req, res) => {
|
|
20617
21019
|
try {
|
|
20618
21020
|
const { id: lessonId } = req.params;
|
|
20619
|
-
if (!checkIsTeacherOrAdmin(req)) {
|
|
20620
|
-
return res.status(403).json({ error: "Forbidden" });
|
|
20621
|
-
}
|
|
20622
21021
|
const elements = kernelContainer.db.prepare(
|
|
20623
21022
|
"SELECT id, type, data FROM whiteboard_elements WHERE lesson_id = ? AND type = ?"
|
|
20624
21023
|
).all(lessonId, "quiz");
|
|
@@ -20639,10 +21038,10 @@ function registerLessonsRoutes(ctx) {
|
|
|
20639
21038
|
});
|
|
20640
21039
|
res.json({ success: true, quizzes });
|
|
20641
21040
|
} catch (e) {
|
|
20642
|
-
res
|
|
21041
|
+
sendSafeError(res, e);
|
|
20643
21042
|
}
|
|
20644
21043
|
});
|
|
20645
|
-
app.post("/api/lessons/:id/ai-tutor", async (req, res) => {
|
|
21044
|
+
app.post("/api/lessons/:id/ai-tutor", requireAuth("student", "teacher", "administrator"), async (req, res) => {
|
|
20646
21045
|
try {
|
|
20647
21046
|
const { elements } = req.body;
|
|
20648
21047
|
const ai = new import_genai3.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
@@ -20671,12 +21070,16 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
20671
21070
|
res.json({ success: true, hint });
|
|
20672
21071
|
} catch (e) {
|
|
20673
21072
|
console.error("AI Tutor error:", e);
|
|
20674
|
-
res
|
|
21073
|
+
sendSafeError(res, e);
|
|
20675
21074
|
}
|
|
20676
21075
|
});
|
|
20677
|
-
app.delete("/api/lessons/:id", async (req, res) => {
|
|
21076
|
+
app.delete("/api/lessons/:id", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
20678
21077
|
try {
|
|
20679
21078
|
const { id } = req.params;
|
|
21079
|
+
const ownership = checkLessonOwnership(req, id);
|
|
21080
|
+
if (!ownership.allowed) {
|
|
21081
|
+
return res.status(ownership.status).json({ success: false, error: ownership.error });
|
|
21082
|
+
}
|
|
20680
21083
|
kernelContainer.db.prepare("DELETE FROM whiteboard_elements WHERE lesson_id = ?").run(id);
|
|
20681
21084
|
kernelContainer.db.prepare("DELETE FROM student_lesson_progress WHERE lesson_id = ?").run(id);
|
|
20682
21085
|
kernelContainer.db.prepare("DELETE FROM schedules WHERE lesson_id = ?").run(id);
|
|
@@ -20687,7 +21090,7 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
20687
21090
|
}
|
|
20688
21091
|
res.json({ success: true });
|
|
20689
21092
|
} catch (e) {
|
|
20690
|
-
res
|
|
21093
|
+
sendSafeError(res, e);
|
|
20691
21094
|
}
|
|
20692
21095
|
});
|
|
20693
21096
|
app.get("/api/lessons/:id/stats", (req, res) => {
|
|
@@ -20707,22 +21110,24 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
20707
21110
|
).get(id).count;
|
|
20708
21111
|
res.json({ whiteboardCount, scheduleCount, enrollmentCount, assignmentCount });
|
|
20709
21112
|
} catch (e) {
|
|
20710
|
-
res
|
|
21113
|
+
sendSafeError(res, e);
|
|
20711
21114
|
}
|
|
20712
21115
|
});
|
|
20713
|
-
app.post("/api/lessons/:id/clone", async (req, res) => {
|
|
21116
|
+
app.post("/api/lessons/:id/clone", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
20714
21117
|
try {
|
|
20715
21118
|
const { id } = req.params;
|
|
20716
21119
|
const original = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(id);
|
|
20717
21120
|
if (!original) {
|
|
20718
21121
|
return res.status(404).json({ error: "Lesson not found" });
|
|
20719
21122
|
}
|
|
21123
|
+
const session = req.session;
|
|
21124
|
+
const creatorId = session?.userId || session?.username || "usr_teacher";
|
|
20720
21125
|
const newId = (0, import_uuid16.v7)();
|
|
20721
21126
|
const now = Date.now();
|
|
20722
21127
|
const newTitle = `\u526F\u672C-${original.title}`;
|
|
20723
21128
|
kernelContainer.db.prepare(
|
|
20724
|
-
"INSERT INTO lessons (id, title, content, timeline, progress_mode, progress_conditions, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
|
20725
|
-
).run(newId, newTitle, original.content, original.timeline, original.progress_mode, original.progress_conditions, now, now);
|
|
21129
|
+
"INSERT INTO lessons (id, title, content, timeline, progress_mode, progress_conditions, creator_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
21130
|
+
).run(newId, newTitle, original.content, original.timeline, original.progress_mode, original.progress_conditions, creatorId, now, now);
|
|
20726
21131
|
const whiteboardElements = kernelContainer.db.prepare(
|
|
20727
21132
|
"SELECT * FROM whiteboard_elements WHERE lesson_id = ?"
|
|
20728
21133
|
).all(id);
|
|
@@ -20732,40 +21137,29 @@ Provide a short, friendly, and helpful hint (1-2 sentences) directly related to
|
|
|
20732
21137
|
for (const el of whiteboardElements) {
|
|
20733
21138
|
insertElement.run((0, import_uuid16.v7)(), newId, el.type, el.data, now);
|
|
20734
21139
|
}
|
|
20735
|
-
const cloned = kernelContainer.db.prepare(
|
|
20736
|
-
|
|
20737
|
-
|
|
21140
|
+
const cloned = kernelContainer.db.prepare(`
|
|
21141
|
+
SELECT l.*, u.name as creator_name, 0 as enrollment_count
|
|
21142
|
+
FROM lessons l
|
|
21143
|
+
LEFT JOIN users u ON l.creator_id = u.id
|
|
21144
|
+
WHERE l.id = ?
|
|
21145
|
+
`).get(newId);
|
|
20738
21146
|
res.json({ success: true, lesson: cloned });
|
|
20739
21147
|
} catch (e) {
|
|
20740
|
-
res
|
|
21148
|
+
sendSafeError(res, e);
|
|
20741
21149
|
}
|
|
20742
21150
|
});
|
|
20743
21151
|
}
|
|
20744
21152
|
|
|
20745
21153
|
// server/routes/workspace.ts
|
|
20746
|
-
var
|
|
21154
|
+
var import_path12 = __toESM(require("path"), 1);
|
|
20747
21155
|
function registerWorkspaceRoutes(ctx) {
|
|
20748
|
-
const {
|
|
20749
|
-
|
|
20750
|
-
io,
|
|
20751
|
-
loginLimiter,
|
|
20752
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
20753
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
20754
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
20755
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
20756
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
20757
|
-
buildOpenAITools: buildOpenAITools2,
|
|
20758
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
20759
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
20760
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
20761
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
20762
|
-
} = ctx;
|
|
20763
|
-
app.get("/api/events", (req, res) => {
|
|
21156
|
+
const { app, MF_REMOTE_CACHE: MF_REMOTE_CACHE2 } = ctx;
|
|
21157
|
+
app.get("/api/events", requireAuth("administrator"), (req, res) => {
|
|
20764
21158
|
try {
|
|
20765
21159
|
const events = kernelContainer.db.prepare("SELECT * FROM events ORDER BY timestamp DESC LIMIT 50").all();
|
|
20766
21160
|
res.json(events);
|
|
20767
21161
|
} catch (e) {
|
|
20768
|
-
res
|
|
21162
|
+
sendSafeError(res, e);
|
|
20769
21163
|
}
|
|
20770
21164
|
});
|
|
20771
21165
|
app.get("/api/mfe/remotes", (req, res) => {
|
|
@@ -20797,12 +21191,15 @@ function registerWorkspaceRoutes(ctx) {
|
|
|
20797
21191
|
MF_REMOTE_CACHE2.set(name, result);
|
|
20798
21192
|
res.json({ success: true, result });
|
|
20799
21193
|
} catch (e) {
|
|
20800
|
-
res
|
|
21194
|
+
sendSafeError(res, e);
|
|
20801
21195
|
}
|
|
20802
21196
|
});
|
|
20803
|
-
app.get("/api/vfs", (req, res) => {
|
|
21197
|
+
app.get("/api/vfs", requireAuth(), (req, res) => {
|
|
20804
21198
|
try {
|
|
20805
21199
|
const parentId = req.query.parentId === "null" ? null : req.query.parentId || null;
|
|
21200
|
+
const isStaff = checkIsTeacherOrAdmin(req);
|
|
21201
|
+
const token = getCookieToken(req);
|
|
21202
|
+
const session = req.session || (token ? getValidSession(token) : null);
|
|
20806
21203
|
let nodes = [];
|
|
20807
21204
|
if (parentId === "virtual-lessons") {
|
|
20808
21205
|
const lessons = kernelContainer.db.prepare("SELECT id, title, content FROM lessons").all();
|
|
@@ -20811,12 +21208,23 @@ function registerWorkspaceRoutes(ctx) {
|
|
|
20811
21208
|
const assignments = kernelContainer.db.prepare("SELECT a.id, a.title, c.name as cname, a.content FROM assignments a JOIN classes c ON a.class_id = c.id").all();
|
|
20812
21209
|
nodes = assignments.map((a) => ({ id: `assgn-${a.id}`, parent_id: "virtual-assignments", type: "file", name: `[${a.cname}] ${a.title}.md`, content: a.content }));
|
|
20813
21210
|
} else if (parentId === "virtual-submissions") {
|
|
20814
|
-
|
|
20815
|
-
|
|
20816
|
-
|
|
20817
|
-
|
|
20818
|
-
|
|
20819
|
-
|
|
21211
|
+
let submissions = [];
|
|
21212
|
+
if (isStaff) {
|
|
21213
|
+
submissions = kernelContainer.db.prepare(`
|
|
21214
|
+
SELECT sub.id, sub.content, a.title, s.name as sname, sub.score
|
|
21215
|
+
FROM assignment_submissions sub
|
|
21216
|
+
JOIN assignments a ON sub.assignment_id = a.id
|
|
21217
|
+
JOIN students s ON sub.student_id = s.id
|
|
21218
|
+
`).all();
|
|
21219
|
+
} else if (session?.userId) {
|
|
21220
|
+
submissions = kernelContainer.db.prepare(`
|
|
21221
|
+
SELECT sub.id, sub.content, a.title, s.name as sname, sub.score
|
|
21222
|
+
FROM assignment_submissions sub
|
|
21223
|
+
JOIN assignments a ON sub.assignment_id = a.id
|
|
21224
|
+
JOIN students s ON sub.student_id = s.id
|
|
21225
|
+
WHERE sub.student_id = ?
|
|
21226
|
+
`).all(session.userId);
|
|
21227
|
+
}
|
|
20820
21228
|
nodes = submissions.map((sub) => ({
|
|
20821
21229
|
id: `sub-${sub.id}`,
|
|
20822
21230
|
parent_id: "virtual-submissions",
|
|
@@ -20843,7 +21251,7 @@ ${sub.content}`
|
|
|
20843
21251
|
}
|
|
20844
21252
|
res.json(nodes);
|
|
20845
21253
|
} catch (e) {
|
|
20846
|
-
res
|
|
21254
|
+
sendSafeError(res, e);
|
|
20847
21255
|
}
|
|
20848
21256
|
});
|
|
20849
21257
|
app.get("/files/*", (req, res) => {
|
|
@@ -20877,7 +21285,7 @@ ${sub.content}`
|
|
|
20877
21285
|
}
|
|
20878
21286
|
const filename = parts[parts.length - 1];
|
|
20879
21287
|
res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"`);
|
|
20880
|
-
const ext =
|
|
21288
|
+
const ext = import_path12.default.extname(filename).toLowerCase();
|
|
20881
21289
|
const binaryExtensions = [".pdf", ".xlsx", ".xls", ".zip", ".png", ".jpg", ".jpeg", ".gif", ".mp4", ".mp3"];
|
|
20882
21290
|
const content = foundNode.content || "";
|
|
20883
21291
|
if (binaryExtensions.includes(ext)) {
|
|
@@ -20889,29 +21297,15 @@ ${sub.content}`
|
|
|
20889
21297
|
}
|
|
20890
21298
|
res.send(content);
|
|
20891
21299
|
} catch (e) {
|
|
20892
|
-
res
|
|
21300
|
+
sendSafeError(res, e);
|
|
20893
21301
|
}
|
|
20894
21302
|
});
|
|
20895
21303
|
}
|
|
20896
21304
|
|
|
20897
21305
|
// server/routes/processes.ts
|
|
20898
21306
|
function registerProcessesRoutes(ctx) {
|
|
20899
|
-
const {
|
|
20900
|
-
|
|
20901
|
-
io,
|
|
20902
|
-
loginLimiter,
|
|
20903
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
20904
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
20905
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
20906
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
20907
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
20908
|
-
buildOpenAITools: buildOpenAITools2,
|
|
20909
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
20910
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
20911
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
20912
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
20913
|
-
} = ctx;
|
|
20914
|
-
app.get("/api/approvals", (req, res) => {
|
|
21307
|
+
const { app } = ctx;
|
|
21308
|
+
app.get("/api/approvals", requireAuth("administrator"), (req, res) => {
|
|
20915
21309
|
try {
|
|
20916
21310
|
const list = kernelContainer.db.prepare("SELECT * FROM pending_commands ORDER BY created_at DESC").all();
|
|
20917
21311
|
res.json(list);
|
|
@@ -20919,71 +21313,54 @@ function registerProcessesRoutes(ctx) {
|
|
|
20919
21313
|
res.status(500).json({ error: e.message });
|
|
20920
21314
|
}
|
|
20921
21315
|
});
|
|
20922
|
-
app.post("/api/approvals/:id/approve", async (req, res) => {
|
|
21316
|
+
app.post("/api/approvals/:id/approve", requireAuth("administrator"), async (req, res) => {
|
|
20923
21317
|
try {
|
|
20924
21318
|
const pending = kernelContainer.db.prepare("SELECT * FROM pending_commands WHERE id = ?").get(req.params.id);
|
|
20925
21319
|
if (!pending) return res.status(404).json({ error: "Not found" });
|
|
20926
|
-
|
|
20927
|
-
|
|
20928
|
-
payload = { ...payload, ...req.body.payloadOverride };
|
|
20929
|
-
}
|
|
21320
|
+
const payload = JSON.parse(pending.payload);
|
|
21321
|
+
const approverId = getActorId(req) || "admin";
|
|
20930
21322
|
const cmd = kernelContainer.commandBus.createCommand(
|
|
20931
21323
|
pending.command_type,
|
|
20932
21324
|
payload,
|
|
20933
|
-
|
|
21325
|
+
approverId,
|
|
20934
21326
|
{ approved: true }
|
|
20935
|
-
// Bypass high risk check now
|
|
20936
21327
|
);
|
|
20937
21328
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
20938
21329
|
kernelContainer.db.prepare("DELETE FROM pending_commands WHERE id = ?").run(pending.id);
|
|
20939
21330
|
res.json({ success: true, result });
|
|
20940
21331
|
} catch (e) {
|
|
20941
|
-
res
|
|
21332
|
+
sendSafeError(res, e);
|
|
20942
21333
|
}
|
|
20943
21334
|
});
|
|
20944
|
-
app.post("/api/approvals/:id/reject", async (req, res) => {
|
|
21335
|
+
app.post("/api/approvals/:id/reject", requireAuth("administrator"), async (req, res) => {
|
|
20945
21336
|
try {
|
|
20946
21337
|
kernelContainer.db.prepare("DELETE FROM pending_commands WHERE id = ?").run(req.params.id);
|
|
20947
21338
|
res.json({ success: true });
|
|
20948
21339
|
} catch (e) {
|
|
20949
|
-
res
|
|
21340
|
+
sendSafeError(res, e);
|
|
20950
21341
|
}
|
|
20951
21342
|
});
|
|
20952
|
-
app.get("/api/processes", (req, res) => {
|
|
21343
|
+
app.get("/api/processes", requireAuth("administrator"), (req, res) => {
|
|
20953
21344
|
try {
|
|
20954
21345
|
const list = kernelContainer.db.prepare("SELECT id, name, status, created_at, updated_at FROM processes WHERE status = 'running' ORDER BY created_at DESC").all();
|
|
20955
21346
|
res.json(list);
|
|
20956
21347
|
} catch (e) {
|
|
20957
|
-
res
|
|
21348
|
+
sendSafeError(res, e);
|
|
20958
21349
|
}
|
|
20959
21350
|
});
|
|
20960
|
-
app.get("/api/processes/:id/logs", (req, res) => {
|
|
21351
|
+
app.get("/api/processes/:id/logs", requireAuth("administrator"), (req, res) => {
|
|
20961
21352
|
try {
|
|
20962
21353
|
const dbRow = kernelContainer.db.prepare("SELECT logs FROM processes WHERE id = ?").get(req.params.id);
|
|
20963
21354
|
res.json(dbRow || { logs: "" });
|
|
20964
21355
|
} catch (e) {
|
|
20965
|
-
res
|
|
21356
|
+
sendSafeError(res, e);
|
|
20966
21357
|
}
|
|
20967
21358
|
});
|
|
20968
21359
|
}
|
|
20969
21360
|
|
|
20970
21361
|
// server/routes/admin.ts
|
|
20971
21362
|
function registerAdminRoutes(ctx) {
|
|
20972
|
-
const {
|
|
20973
|
-
app,
|
|
20974
|
-
io,
|
|
20975
|
-
loginLimiter,
|
|
20976
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
20977
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
20978
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
20979
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
20980
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
20981
|
-
buildOpenAITools: buildOpenAITools2,
|
|
20982
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
20983
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
20984
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
20985
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
20986
|
-
} = ctx;
|
|
21363
|
+
const { app } = ctx;
|
|
20987
21364
|
app.post("/api/admin/seed-demo", requireAuth("administrator"), (req, res) => {
|
|
20988
21365
|
try {
|
|
20989
21366
|
const db2 = kernelContainer.db;
|
|
@@ -21047,7 +21424,7 @@ function registerAdminRoutes(ctx) {
|
|
|
21047
21424
|
}
|
|
21048
21425
|
res.json({ success: true, classId: DEMO_CLASS_ID, scheduleId: DEMO_SCHEDULE_ID, lessonId });
|
|
21049
21426
|
} catch (e) {
|
|
21050
|
-
res
|
|
21427
|
+
sendSafeError(res, e);
|
|
21051
21428
|
}
|
|
21052
21429
|
});
|
|
21053
21430
|
app.post("/api/classes/import", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -21097,7 +21474,7 @@ function registerAdminRoutes(ctx) {
|
|
|
21097
21474
|
}
|
|
21098
21475
|
res.json({ success: true, imported });
|
|
21099
21476
|
} catch (e) {
|
|
21100
|
-
res
|
|
21477
|
+
sendSafeError(res, e);
|
|
21101
21478
|
}
|
|
21102
21479
|
});
|
|
21103
21480
|
app.post("/api/students/import", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -21133,32 +21510,18 @@ function registerAdminRoutes(ctx) {
|
|
|
21133
21510
|
}
|
|
21134
21511
|
res.json({ success: true, imported });
|
|
21135
21512
|
} catch (e) {
|
|
21136
|
-
res
|
|
21513
|
+
sendSafeError(res, e);
|
|
21137
21514
|
}
|
|
21138
21515
|
});
|
|
21139
21516
|
}
|
|
21140
21517
|
|
|
21141
21518
|
// server/routes/roster.ts
|
|
21142
|
-
var
|
|
21143
|
-
var
|
|
21519
|
+
var import_path13 = __toESM(require("path"), 1);
|
|
21520
|
+
var import_fs12 = __toESM(require("fs"), 1);
|
|
21144
21521
|
var import_crypto10 = __toESM(require("crypto"), 1);
|
|
21145
21522
|
var import_bcryptjs2 = __toESM(require("bcryptjs"), 1);
|
|
21146
21523
|
function registerRosterRoutes(ctx) {
|
|
21147
|
-
const {
|
|
21148
|
-
app,
|
|
21149
|
-
io,
|
|
21150
|
-
loginLimiter,
|
|
21151
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
21152
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
21153
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
21154
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
21155
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
21156
|
-
buildOpenAITools: buildOpenAITools2,
|
|
21157
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
21158
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
21159
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
21160
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
21161
|
-
} = ctx;
|
|
21524
|
+
const { app, io, loginLimiter } = ctx;
|
|
21162
21525
|
app.get("/api/classes", (req, res) => {
|
|
21163
21526
|
try {
|
|
21164
21527
|
const classes = kernelContainer.db.prepare(`
|
|
@@ -21192,10 +21555,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21192
21555
|
`).all(req.params.id);
|
|
21193
21556
|
res.json(students);
|
|
21194
21557
|
} catch (e) {
|
|
21195
|
-
res
|
|
21558
|
+
sendSafeError(res, e);
|
|
21196
21559
|
}
|
|
21197
21560
|
});
|
|
21198
|
-
app.post("/api/classes", (req, res) => {
|
|
21561
|
+
app.post("/api/classes", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21199
21562
|
try {
|
|
21200
21563
|
const { name, description } = req.body;
|
|
21201
21564
|
const classId = Math.random().toString(36).slice(2);
|
|
@@ -21207,10 +21570,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21207
21570
|
);
|
|
21208
21571
|
res.json({ success: true, id: classId });
|
|
21209
21572
|
} catch (e) {
|
|
21210
|
-
res
|
|
21573
|
+
sendSafeError(res, e);
|
|
21211
21574
|
}
|
|
21212
21575
|
});
|
|
21213
|
-
app.put("/api/classes/:id", (req, res) => {
|
|
21576
|
+
app.put("/api/classes/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21214
21577
|
try {
|
|
21215
21578
|
const { name, description, class_passcode } = req.body;
|
|
21216
21579
|
if (name) kernelContainer.db.prepare("UPDATE classes SET name = ? WHERE id = ?").run(name, req.params.id);
|
|
@@ -21218,10 +21581,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21218
21581
|
if (class_passcode !== void 0) kernelContainer.db.prepare("UPDATE classes SET class_passcode = ? WHERE id = ?").run(class_passcode, req.params.id);
|
|
21219
21582
|
res.json({ success: true });
|
|
21220
21583
|
} catch (e) {
|
|
21221
|
-
res
|
|
21584
|
+
sendSafeError(res, e);
|
|
21222
21585
|
}
|
|
21223
21586
|
});
|
|
21224
|
-
app.delete("/api/classes/:id", (req, res) => {
|
|
21587
|
+
app.delete("/api/classes/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21225
21588
|
try {
|
|
21226
21589
|
const classId = req.params.id;
|
|
21227
21590
|
const db2 = kernelContainer.db;
|
|
@@ -21263,7 +21626,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21263
21626
|
deleteTransaction();
|
|
21264
21627
|
res.json({ success: true });
|
|
21265
21628
|
} catch (e) {
|
|
21266
|
-
res
|
|
21629
|
+
sendSafeError(res, e);
|
|
21267
21630
|
}
|
|
21268
21631
|
});
|
|
21269
21632
|
app.get("/api/db-status", (req, res) => {
|
|
@@ -21328,7 +21691,8 @@ function registerRosterRoutes(ctx) {
|
|
|
21328
21691
|
}
|
|
21329
21692
|
return res.status(500).json({ status: "disconnected", error: "Unexpected response from SQLite" });
|
|
21330
21693
|
} catch (e) {
|
|
21331
|
-
|
|
21694
|
+
const exposed = process.env.NODE_ENV === "production" ? "Database connection error" : e.message;
|
|
21695
|
+
return res.status(500).json({ status: "disconnected", error: exposed });
|
|
21332
21696
|
}
|
|
21333
21697
|
});
|
|
21334
21698
|
app.get("/api/auth/session", (req, res) => {
|
|
@@ -21343,7 +21707,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21343
21707
|
}
|
|
21344
21708
|
res.json({ session });
|
|
21345
21709
|
} catch (e) {
|
|
21346
|
-
res
|
|
21710
|
+
sendSafeError(res, e);
|
|
21347
21711
|
}
|
|
21348
21712
|
});
|
|
21349
21713
|
app.post("/api/auth/logout", (req, res) => {
|
|
@@ -21352,10 +21716,12 @@ function registerRosterRoutes(ctx) {
|
|
|
21352
21716
|
if (token) {
|
|
21353
21717
|
kernelContainer.db.prepare("DELETE FROM client_sessions WHERE id = ?").run(token);
|
|
21354
21718
|
}
|
|
21355
|
-
|
|
21719
|
+
const isSecure = req.secure || req.headers["x-forwarded-proto"] === "https" || process.env.NODE_ENV === "production" && process.env.ENABLE_HTTPS === "true";
|
|
21720
|
+
const secureFlag = isSecure ? "; Secure" : "";
|
|
21721
|
+
res.setHeader("Set-Cookie", `edu_os_token=; Path=/; HttpOnly; Max-Age=0; SameSite=Lax${secureFlag}`);
|
|
21356
21722
|
res.json({ success: true });
|
|
21357
21723
|
} catch (e) {
|
|
21358
|
-
res
|
|
21724
|
+
sendSafeError(res, e);
|
|
21359
21725
|
}
|
|
21360
21726
|
});
|
|
21361
21727
|
app.post("/api/auth/change-password", (req, res) => {
|
|
@@ -21414,7 +21780,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21414
21780
|
}
|
|
21415
21781
|
res.status(400).json({ error: "Unsupported role" });
|
|
21416
21782
|
} catch (e) {
|
|
21417
|
-
res
|
|
21783
|
+
sendSafeError(res, e);
|
|
21418
21784
|
}
|
|
21419
21785
|
});
|
|
21420
21786
|
app.post("/api/auth/profile", (req, res) => {
|
|
@@ -21445,7 +21811,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21445
21811
|
}
|
|
21446
21812
|
res.json({ success: true, name });
|
|
21447
21813
|
} catch (e) {
|
|
21448
|
-
res
|
|
21814
|
+
sendSafeError(res, e);
|
|
21449
21815
|
}
|
|
21450
21816
|
});
|
|
21451
21817
|
app.post("/api/auth/avatar", (req, res) => {
|
|
@@ -21458,7 +21824,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21458
21824
|
if (!filename || !base64Data) {
|
|
21459
21825
|
return res.status(400).json({ error: "Filename and base64Data are required" });
|
|
21460
21826
|
}
|
|
21461
|
-
const ext =
|
|
21827
|
+
const ext = import_path13.default.extname(filename).toLowerCase();
|
|
21462
21828
|
const ALLOWED_IMG = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
|
|
21463
21829
|
if (BLOCKED_EXTENSIONS.includes(ext) || !ALLOWED_IMG.includes(ext)) {
|
|
21464
21830
|
return res.status(400).json({ error: "Only image files (jpg / png / gif / webp) are allowed" });
|
|
@@ -21471,11 +21837,11 @@ function registerRosterRoutes(ctx) {
|
|
|
21471
21837
|
if (!validateMagicBytes(fileBuffer, filename)) {
|
|
21472
21838
|
return res.status(400).json({ error: "File content does not match the declared image type" });
|
|
21473
21839
|
}
|
|
21474
|
-
const avatarDir =
|
|
21475
|
-
|
|
21840
|
+
const avatarDir = import_path13.default.join(process.cwd(), "uploads", "avatars");
|
|
21841
|
+
import_fs12.default.mkdirSync(avatarDir, { recursive: true });
|
|
21476
21842
|
const uniqueName = `${Date.now()}-${import_crypto10.default.randomBytes(4).toString("hex")}${ext}`;
|
|
21477
|
-
const filePath =
|
|
21478
|
-
|
|
21843
|
+
const filePath = import_path13.default.join(avatarDir, uniqueName);
|
|
21844
|
+
import_fs12.default.writeFileSync(filePath, fileBuffer);
|
|
21479
21845
|
const avatarUrl = `/uploads/avatars/${uniqueName}`;
|
|
21480
21846
|
let oldAvatar = null;
|
|
21481
21847
|
if (session.role === "student") {
|
|
@@ -21489,7 +21855,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21489
21855
|
}
|
|
21490
21856
|
if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
|
|
21491
21857
|
try {
|
|
21492
|
-
|
|
21858
|
+
import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
|
|
21493
21859
|
} catch {
|
|
21494
21860
|
}
|
|
21495
21861
|
}
|
|
@@ -21504,7 +21870,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21504
21870
|
}
|
|
21505
21871
|
res.json({ success: true, avatar: avatarUrl });
|
|
21506
21872
|
} catch (e) {
|
|
21507
|
-
res
|
|
21873
|
+
sendSafeError(res, e);
|
|
21508
21874
|
}
|
|
21509
21875
|
});
|
|
21510
21876
|
app.delete("/api/auth/avatar", (req, res) => {
|
|
@@ -21525,7 +21891,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21525
21891
|
}
|
|
21526
21892
|
if (oldAvatar && oldAvatar.startsWith("/uploads/avatars/")) {
|
|
21527
21893
|
try {
|
|
21528
|
-
|
|
21894
|
+
import_fs12.default.unlinkSync(import_path13.default.join(process.cwd(), oldAvatar));
|
|
21529
21895
|
} catch {
|
|
21530
21896
|
}
|
|
21531
21897
|
}
|
|
@@ -21540,7 +21906,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21540
21906
|
}
|
|
21541
21907
|
res.json({ success: true });
|
|
21542
21908
|
} catch (e) {
|
|
21543
|
-
res
|
|
21909
|
+
sendSafeError(res, e);
|
|
21544
21910
|
}
|
|
21545
21911
|
});
|
|
21546
21912
|
app.post("/api/auth/login", loginLimiter, (req, res) => {
|
|
@@ -21635,7 +22001,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21635
22001
|
const now = Date.now();
|
|
21636
22002
|
const expiresAt = now + 7 * 24 * 60 * 60 * 1e3;
|
|
21637
22003
|
kernelContainer.db.prepare("INSERT INTO client_sessions (id, session_data, updated_at, expires_at) VALUES (?, ?, ?, ?)").run(sessionToken, JSON.stringify(sessionData), now, expiresAt);
|
|
21638
|
-
|
|
22004
|
+
const isSecure = req.secure || req.headers["x-forwarded-proto"] === "https" || process.env.NODE_ENV === "production" && process.env.ENABLE_HTTPS === "true";
|
|
22005
|
+
const secureFlag = isSecure ? "; Secure" : "";
|
|
22006
|
+
const maxAgeSeconds = 7 * 24 * 60 * 60;
|
|
22007
|
+
res.setHeader("Set-Cookie", `edu_os_token=${sessionToken}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAgeSeconds}${secureFlag}`);
|
|
21639
22008
|
return res.json({
|
|
21640
22009
|
success: true,
|
|
21641
22010
|
session: sessionData
|
|
@@ -21643,7 +22012,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21643
22012
|
}
|
|
21644
22013
|
res.status(400).json({ error: "Unsupported entry type" });
|
|
21645
22014
|
} catch (e) {
|
|
21646
|
-
res
|
|
22015
|
+
sendSafeError(res, e);
|
|
21647
22016
|
}
|
|
21648
22017
|
});
|
|
21649
22018
|
app.get("/api/students/:id/read_notifications", (req, res) => {
|
|
@@ -21651,10 +22020,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21651
22020
|
const rows = kernelContainer.db.prepare("SELECT notification_id FROM student_read_notifications WHERE student_id = ?").all(req.params.id);
|
|
21652
22021
|
res.json(rows.map((r) => r.notification_id));
|
|
21653
22022
|
} catch (e) {
|
|
21654
|
-
res
|
|
22023
|
+
sendSafeError(res, e);
|
|
21655
22024
|
}
|
|
21656
22025
|
});
|
|
21657
|
-
app.post("/api/students/:id/read_notifications", (req, res) => {
|
|
22026
|
+
app.post("/api/students/:id/read_notifications", requireAuth(), (req, res) => {
|
|
21658
22027
|
try {
|
|
21659
22028
|
const { notificationId } = req.body;
|
|
21660
22029
|
if (!notificationId) {
|
|
@@ -21667,10 +22036,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21667
22036
|
});
|
|
21668
22037
|
res.json({ success: true });
|
|
21669
22038
|
} catch (e) {
|
|
21670
|
-
res
|
|
22039
|
+
sendSafeError(res, e);
|
|
21671
22040
|
}
|
|
21672
22041
|
});
|
|
21673
|
-
app.post("/api/classes/:classId/lock_lesson", (req, res) => {
|
|
22042
|
+
app.post("/api/classes/:classId/lock_lesson", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21674
22043
|
try {
|
|
21675
22044
|
const { lessonId } = req.body;
|
|
21676
22045
|
if (!lessonId) {
|
|
@@ -21687,7 +22056,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21687
22056
|
res.status(550).json({ error: e.message });
|
|
21688
22057
|
}
|
|
21689
22058
|
});
|
|
21690
|
-
app.post("/api/classes/:classId/unlock_lesson", (req, res) => {
|
|
22059
|
+
app.post("/api/classes/:classId/unlock_lesson", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21691
22060
|
try {
|
|
21692
22061
|
kernelContainer.db.prepare("UPDATE students SET locked_lesson_id = NULL WHERE id IN (SELECT student_id FROM class_students WHERE class_id = ?)").run(req.params.classId);
|
|
21693
22062
|
io.emit("class-lock-status-changed", {
|
|
@@ -21757,7 +22126,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21757
22126
|
const labs = kernelContainer.db.prepare("SELECT * FROM computer_labs ORDER BY created_at DESC").all();
|
|
21758
22127
|
res.json(labs);
|
|
21759
22128
|
} catch (e) {
|
|
21760
|
-
res
|
|
22129
|
+
sendSafeError(res, e);
|
|
21761
22130
|
}
|
|
21762
22131
|
});
|
|
21763
22132
|
app.post("/api/labs", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -21769,7 +22138,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21769
22138
|
).run(id, room_number, parseInt(rows), parseInt(cols), Date.now());
|
|
21770
22139
|
res.json({ success: true, id, room_number, rows, cols });
|
|
21771
22140
|
} catch (e) {
|
|
21772
|
-
res
|
|
22141
|
+
sendSafeError(res, e);
|
|
21773
22142
|
}
|
|
21774
22143
|
});
|
|
21775
22144
|
app.put("/api/labs/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -21780,7 +22149,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21780
22149
|
).run(room_number, parseInt(rows), parseInt(cols), req.params.id);
|
|
21781
22150
|
res.json({ success: true });
|
|
21782
22151
|
} catch (e) {
|
|
21783
|
-
res
|
|
22152
|
+
sendSafeError(res, e);
|
|
21784
22153
|
}
|
|
21785
22154
|
});
|
|
21786
22155
|
app.delete("/api/labs/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -21789,7 +22158,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21789
22158
|
kernelContainer.db.prepare("DELETE FROM student_seats WHERE lab_id = ?").run(req.params.id);
|
|
21790
22159
|
res.json({ success: true });
|
|
21791
22160
|
} catch (e) {
|
|
21792
|
-
res
|
|
22161
|
+
sendSafeError(res, e);
|
|
21793
22162
|
}
|
|
21794
22163
|
});
|
|
21795
22164
|
app.get("/api/classes/:classId/seats", (req, res) => {
|
|
@@ -21799,7 +22168,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21799
22168
|
const seats = kernelContainer.db.prepare("SELECT * FROM student_seats WHERE class_id = ?").all(req.params.classId);
|
|
21800
22169
|
res.json({ lab_id: labId, seats });
|
|
21801
22170
|
} catch (e) {
|
|
21802
|
-
res
|
|
22171
|
+
sendSafeError(res, e);
|
|
21803
22172
|
}
|
|
21804
22173
|
});
|
|
21805
22174
|
app.post("/api/classes/:classId/seats", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -21817,10 +22186,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21817
22186
|
}
|
|
21818
22187
|
res.json({ success: true });
|
|
21819
22188
|
} catch (e) {
|
|
21820
|
-
res
|
|
22189
|
+
sendSafeError(res, e);
|
|
21821
22190
|
}
|
|
21822
22191
|
});
|
|
21823
|
-
app.post("/api/students", (req, res) => {
|
|
22192
|
+
app.post("/api/students", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21824
22193
|
try {
|
|
21825
22194
|
const { name, email, password, student_number } = req.body;
|
|
21826
22195
|
const studentId = Math.random().toString(36).slice(2);
|
|
@@ -21839,10 +22208,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21839
22208
|
);
|
|
21840
22209
|
res.json({ success: true, id: studentId, student_number: finalNum, tempPassword: password || "123456" });
|
|
21841
22210
|
} catch (e) {
|
|
21842
|
-
res
|
|
22211
|
+
sendSafeError(res, e);
|
|
21843
22212
|
}
|
|
21844
22213
|
});
|
|
21845
|
-
app.put("/api/students/:id", (req, res) => {
|
|
22214
|
+
app.put("/api/students/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21846
22215
|
try {
|
|
21847
22216
|
const { name, email, password, locked_lesson_id, private_notes, student_number } = req.body;
|
|
21848
22217
|
if (name) kernelContainer.db.prepare("UPDATE students SET name = ? WHERE id = ?").run(name, req.params.id);
|
|
@@ -21856,10 +22225,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21856
22225
|
if (student_number !== void 0) kernelContainer.db.prepare("UPDATE students SET student_number = ? WHERE id = ?").run(student_number, req.params.id);
|
|
21857
22226
|
res.json({ success: true });
|
|
21858
22227
|
} catch (e) {
|
|
21859
|
-
res
|
|
22228
|
+
sendSafeError(res, e);
|
|
21860
22229
|
}
|
|
21861
22230
|
});
|
|
21862
|
-
app.delete("/api/students/:id", (req, res) => {
|
|
22231
|
+
app.delete("/api/students/:id", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21863
22232
|
try {
|
|
21864
22233
|
kernelContainer.db.prepare("DELETE FROM class_students WHERE student_id = ?").run(req.params.id);
|
|
21865
22234
|
kernelContainer.db.prepare("DELETE FROM student_lesson_progress WHERE student_id = ?").run(req.params.id);
|
|
@@ -21875,7 +22244,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21875
22244
|
kernelContainer.db.prepare("DELETE FROM students WHERE id = ?").run(req.params.id);
|
|
21876
22245
|
res.json({ success: true });
|
|
21877
22246
|
} catch (e) {
|
|
21878
|
-
res
|
|
22247
|
+
sendSafeError(res, e);
|
|
21879
22248
|
}
|
|
21880
22249
|
});
|
|
21881
22250
|
app.get("/api/students/:id/export", (req, res) => {
|
|
@@ -21912,14 +22281,11 @@ function registerRosterRoutes(ctx) {
|
|
|
21912
22281
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21913
22282
|
});
|
|
21914
22283
|
} catch (e) {
|
|
21915
|
-
res
|
|
22284
|
+
sendSafeError(res, e);
|
|
21916
22285
|
}
|
|
21917
22286
|
});
|
|
21918
|
-
app.delete("/api/students/:id/gdpr-delete", (req, res) => {
|
|
22287
|
+
app.delete("/api/students/:id/gdpr-delete", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21919
22288
|
try {
|
|
21920
|
-
if (!checkIsTeacherOrAdmin(req)) {
|
|
21921
|
-
return res.status(403).json({ error: "Only teachers and administrators can perform GDPR deletion" });
|
|
21922
|
-
}
|
|
21923
22289
|
const studentId = req.params.id;
|
|
21924
22290
|
const { confirm } = req.body;
|
|
21925
22291
|
if (confirm !== true) {
|
|
@@ -21940,7 +22306,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21940
22306
|
console.log(`[GDPR] Complete data deletion for student ${studentId}`);
|
|
21941
22307
|
res.json({ success: true, message: "All student data has been permanently deleted." });
|
|
21942
22308
|
} catch (e) {
|
|
21943
|
-
res
|
|
22309
|
+
sendSafeError(res, e);
|
|
21944
22310
|
}
|
|
21945
22311
|
});
|
|
21946
22312
|
app.get("/api/students/:id/progress", (req, res) => {
|
|
@@ -21953,11 +22319,16 @@ function registerRosterRoutes(ctx) {
|
|
|
21953
22319
|
`).all(req.params.id);
|
|
21954
22320
|
res.json(progress);
|
|
21955
22321
|
} catch (e) {
|
|
21956
|
-
res
|
|
22322
|
+
sendSafeError(res, e);
|
|
21957
22323
|
}
|
|
21958
22324
|
});
|
|
21959
|
-
app.post("/api/students/:id/progress", (req, res) => {
|
|
22325
|
+
app.post("/api/students/:id/progress", requireAuth(), (req, res) => {
|
|
21960
22326
|
try {
|
|
22327
|
+
const session = req.session;
|
|
22328
|
+
const isPrivileged = session && (session.role === "teacher" || session.role === "administrator");
|
|
22329
|
+
if (!isPrivileged && session?.userId !== req.params.id) {
|
|
22330
|
+
return res.status(403).json({ error: "Cannot update progress for another student" });
|
|
22331
|
+
}
|
|
21961
22332
|
const { lessonId, completed, progressPercent, completedSegments } = req.body;
|
|
21962
22333
|
const completedSegmentsStr = typeof completedSegments === "string" ? completedSegments : JSON.stringify(completedSegments || []);
|
|
21963
22334
|
kernelContainer.db.prepare(`
|
|
@@ -21984,10 +22355,10 @@ function registerRosterRoutes(ctx) {
|
|
|
21984
22355
|
});
|
|
21985
22356
|
res.json({ success: true });
|
|
21986
22357
|
} catch (e) {
|
|
21987
|
-
res
|
|
22358
|
+
sendSafeError(res, e);
|
|
21988
22359
|
}
|
|
21989
22360
|
});
|
|
21990
|
-
app.post("/api/classes/:id/students", (req, res) => {
|
|
22361
|
+
app.post("/api/classes/:id/students", requireAuth("teacher", "administrator"), (req, res) => {
|
|
21991
22362
|
try {
|
|
21992
22363
|
const { studentId } = req.body;
|
|
21993
22364
|
kernelContainer.db.prepare("INSERT OR IGNORE INTO class_students (class_id, student_id, joined_at) VALUES (?, ?, ?)").run(
|
|
@@ -21997,7 +22368,7 @@ function registerRosterRoutes(ctx) {
|
|
|
21997
22368
|
);
|
|
21998
22369
|
res.json({ success: true });
|
|
21999
22370
|
} catch (e) {
|
|
22000
|
-
res
|
|
22371
|
+
sendSafeError(res, e);
|
|
22001
22372
|
}
|
|
22002
22373
|
});
|
|
22003
22374
|
app.post("/api/classes/:id/students/bulk-enroll", requireAuth("teacher", "administrator"), (req, res) => {
|
|
@@ -22039,7 +22410,7 @@ function registerRosterRoutes(ctx) {
|
|
|
22039
22410
|
}
|
|
22040
22411
|
res.json({ success: true, count: results.length, results });
|
|
22041
22412
|
} catch (e) {
|
|
22042
|
-
res
|
|
22413
|
+
sendSafeError(res, e);
|
|
22043
22414
|
}
|
|
22044
22415
|
});
|
|
22045
22416
|
app.get("/api/classes/:id/progress", (req, res) => {
|
|
@@ -22054,7 +22425,7 @@ function registerRosterRoutes(ctx) {
|
|
|
22054
22425
|
`).all(req.params.id);
|
|
22055
22426
|
res.json(progress);
|
|
22056
22427
|
} catch (e) {
|
|
22057
|
-
res
|
|
22428
|
+
sendSafeError(res, e);
|
|
22058
22429
|
}
|
|
22059
22430
|
});
|
|
22060
22431
|
app.get("/api/classes/:classId/lessons/:lessonId/progress", (req, res) => {
|
|
@@ -22074,15 +22445,15 @@ function registerRosterRoutes(ctx) {
|
|
|
22074
22445
|
`).all(req.params.lessonId, req.params.lessonId, req.params.classId);
|
|
22075
22446
|
res.json(progress);
|
|
22076
22447
|
} catch (e) {
|
|
22077
|
-
res
|
|
22448
|
+
sendSafeError(res, e);
|
|
22078
22449
|
}
|
|
22079
22450
|
});
|
|
22080
|
-
app.delete("/api/classes/:classId/students/:studentId", (req, res) => {
|
|
22451
|
+
app.delete("/api/classes/:classId/students/:studentId", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22081
22452
|
try {
|
|
22082
22453
|
kernelContainer.db.prepare("DELETE FROM class_students WHERE class_id = ? AND student_id = ?").run(req.params.classId, req.params.studentId);
|
|
22083
22454
|
res.json({ success: true });
|
|
22084
22455
|
} catch (e) {
|
|
22085
|
-
res
|
|
22456
|
+
sendSafeError(res, e);
|
|
22086
22457
|
}
|
|
22087
22458
|
});
|
|
22088
22459
|
app.get("/api/classes/:classId/dashboard", (req, res) => {
|
|
@@ -22125,11 +22496,16 @@ function registerRosterRoutes(ctx) {
|
|
|
22125
22496
|
`).all(req.params.classId, req.params.classId);
|
|
22126
22497
|
res.json({ assignments, recentSubmissions, performance: performance2, rollcallStats });
|
|
22127
22498
|
} catch (e) {
|
|
22128
|
-
res
|
|
22499
|
+
sendSafeError(res, e);
|
|
22129
22500
|
}
|
|
22130
22501
|
});
|
|
22131
|
-
app.get("/api/students/:id/dashboard", (req, res) => {
|
|
22502
|
+
app.get("/api/students/:id/dashboard", requireAuth(), (req, res) => {
|
|
22132
22503
|
try {
|
|
22504
|
+
const session = req.session;
|
|
22505
|
+
const isPrivileged = session && (session.role === "teacher" || session.role === "administrator");
|
|
22506
|
+
if (!isPrivileged && session?.userId !== req.params.id) {
|
|
22507
|
+
return res.status(403).json({ error: "Cannot access another student dashboard" });
|
|
22508
|
+
}
|
|
22133
22509
|
const studentId = req.params.id;
|
|
22134
22510
|
const studentClasses = kernelContainer.db.prepare(`
|
|
22135
22511
|
SELECT c.*
|
|
@@ -22204,7 +22580,7 @@ function registerRosterRoutes(ctx) {
|
|
|
22204
22580
|
`).get(studentId);
|
|
22205
22581
|
res.json({ classes: studentClasses, schedules, assignments, progress, rollcalls, profile });
|
|
22206
22582
|
} catch (e) {
|
|
22207
|
-
res
|
|
22583
|
+
sendSafeError(res, e);
|
|
22208
22584
|
}
|
|
22209
22585
|
});
|
|
22210
22586
|
}
|
|
@@ -22212,30 +22588,16 @@ function registerRosterRoutes(ctx) {
|
|
|
22212
22588
|
// server/routes/assignments.ts
|
|
22213
22589
|
var import_genai4 = require("@google/genai");
|
|
22214
22590
|
function registerAssignmentsRoutes(ctx) {
|
|
22215
|
-
const {
|
|
22216
|
-
app,
|
|
22217
|
-
io,
|
|
22218
|
-
loginLimiter,
|
|
22219
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
22220
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
22221
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
22222
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
22223
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
22224
|
-
buildOpenAITools: buildOpenAITools2,
|
|
22225
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
22226
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
22227
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
22228
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
22229
|
-
} = ctx;
|
|
22591
|
+
const { app } = ctx;
|
|
22230
22592
|
app.get("/api/classes/:classId/assignments", (req, res) => {
|
|
22231
22593
|
try {
|
|
22232
22594
|
const assignments = kernelContainer.db.prepare("SELECT * FROM assignments WHERE class_id = ? ORDER BY created_at DESC").all(req.params.classId);
|
|
22233
22595
|
res.json(assignments);
|
|
22234
22596
|
} catch (e) {
|
|
22235
|
-
res
|
|
22597
|
+
sendSafeError(res, e);
|
|
22236
22598
|
}
|
|
22237
22599
|
});
|
|
22238
|
-
app.post("/api/classes/:classId/assignments/generate", async (req, res) => {
|
|
22600
|
+
app.post("/api/classes/:classId/assignments/generate", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
22239
22601
|
try {
|
|
22240
22602
|
const { topic, lessonId } = req.body;
|
|
22241
22603
|
const ai = new import_genai4.GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
@@ -22260,10 +22622,10 @@ function registerAssignmentsRoutes(ctx) {
|
|
|
22260
22622
|
);
|
|
22261
22623
|
res.json({ success: true, assignment: { id, class_id: req.params.classId, lesson_id: lessonId || null, title: gen.title, description: gen.description, content: gen.content } });
|
|
22262
22624
|
} catch (e) {
|
|
22263
|
-
res
|
|
22625
|
+
sendSafeError(res, e);
|
|
22264
22626
|
}
|
|
22265
22627
|
});
|
|
22266
|
-
app.post("/api/classes/:classId/assignments/suggest", async (req, res) => {
|
|
22628
|
+
app.post("/api/classes/:classId/assignments/suggest", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
22267
22629
|
try {
|
|
22268
22630
|
const { lessonId } = req.body;
|
|
22269
22631
|
const lesson = kernelContainer.db.prepare("SELECT * FROM lessons WHERE id = ?").get(lessonId);
|
|
@@ -22327,10 +22689,10 @@ Generate the response in the specified JSON schema.`;
|
|
|
22327
22689
|
const text = response.text || "{}";
|
|
22328
22690
|
res.json(JSON.parse(text));
|
|
22329
22691
|
} catch (e) {
|
|
22330
|
-
res
|
|
22692
|
+
sendSafeError(res, e);
|
|
22331
22693
|
}
|
|
22332
22694
|
});
|
|
22333
|
-
app.post("/api/classes/:classId/assignments/create-suggested-quiz", async (req, res) => {
|
|
22695
|
+
app.post("/api/classes/:classId/assignments/create-suggested-quiz", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
22334
22696
|
try {
|
|
22335
22697
|
const { title, description, questions, learningObjectives, timeLimit, lessonId } = req.body;
|
|
22336
22698
|
const id = "ast-" + Date.now().toString(36);
|
|
@@ -22351,12 +22713,17 @@ Generate the response in the specified JSON schema.`;
|
|
|
22351
22713
|
);
|
|
22352
22714
|
res.json({ success: true, assignmentId: id });
|
|
22353
22715
|
} catch (e) {
|
|
22354
|
-
res
|
|
22716
|
+
sendSafeError(res, e);
|
|
22355
22717
|
}
|
|
22356
22718
|
});
|
|
22357
|
-
app.post("/api/assignments/:id/submissions", (req, res) => {
|
|
22719
|
+
app.post("/api/assignments/:id/submissions", requireAuth(), (req, res) => {
|
|
22358
22720
|
try {
|
|
22359
22721
|
const { studentId, content } = req.body;
|
|
22722
|
+
const session = req.session;
|
|
22723
|
+
const isPrivileged = session && (session.role === "teacher" || session.role === "administrator");
|
|
22724
|
+
if (!isPrivileged && session?.userId !== studentId) {
|
|
22725
|
+
return res.status(403).json({ error: "Cannot submit assignment on behalf of another student" });
|
|
22726
|
+
}
|
|
22360
22727
|
kernelContainer.db.prepare(`
|
|
22361
22728
|
INSERT INTO assignment_submissions (assignment_id, student_id, content, submitted_at, status)
|
|
22362
22729
|
VALUES (?, ?, ?, ?, 'submitted')
|
|
@@ -22364,7 +22731,7 @@ Generate the response in the specified JSON schema.`;
|
|
|
22364
22731
|
`).run(req.params.id, studentId, content, Date.now());
|
|
22365
22732
|
res.json({ success: true });
|
|
22366
22733
|
} catch (e) {
|
|
22367
|
-
res
|
|
22734
|
+
sendSafeError(res, e);
|
|
22368
22735
|
}
|
|
22369
22736
|
});
|
|
22370
22737
|
app.get("/api/assignments/:id/submissions", (req, res) => {
|
|
@@ -22378,10 +22745,10 @@ Generate the response in the specified JSON schema.`;
|
|
|
22378
22745
|
`).all(req.params.id);
|
|
22379
22746
|
res.json(submissions);
|
|
22380
22747
|
} catch (e) {
|
|
22381
|
-
res
|
|
22748
|
+
sendSafeError(res, e);
|
|
22382
22749
|
}
|
|
22383
22750
|
});
|
|
22384
|
-
app.post("/api/assignments/:id/submissions/:studentId/grade", async (req, res) => {
|
|
22751
|
+
app.post("/api/assignments/:id/submissions/:studentId/grade", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
22385
22752
|
try {
|
|
22386
22753
|
const asb = kernelContainer.db.prepare("SELECT * FROM assignment_submissions WHERE assignment_id = ? AND student_id = ?").get(req.params.id, req.params.studentId);
|
|
22387
22754
|
const ast = kernelContainer.db.prepare("SELECT * FROM assignments WHERE id = ?").get(req.params.id);
|
|
@@ -22463,7 +22830,7 @@ Provide a grade score (0-100) and brief feedback. Ensure you output in this exac
|
|
|
22463
22830
|
});
|
|
22464
22831
|
res.json({ success: true, pendingApproval: true, message: "Grade generated and sent for approval.", score: grade.score, feedback: grade.feedback });
|
|
22465
22832
|
} catch (e) {
|
|
22466
|
-
res
|
|
22833
|
+
sendSafeError(res, e);
|
|
22467
22834
|
}
|
|
22468
22835
|
});
|
|
22469
22836
|
}
|
|
@@ -22471,21 +22838,7 @@ Provide a grade score (0-100) and brief feedback. Ensure you output in this exac
|
|
|
22471
22838
|
// server/routes/schedules.ts
|
|
22472
22839
|
var import_genai5 = require("@google/genai");
|
|
22473
22840
|
function registerSchedulesRoutes(ctx) {
|
|
22474
|
-
const {
|
|
22475
|
-
app,
|
|
22476
|
-
io,
|
|
22477
|
-
loginLimiter,
|
|
22478
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
22479
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
22480
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
22481
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
22482
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
22483
|
-
buildOpenAITools: buildOpenAITools2,
|
|
22484
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
22485
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
22486
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
22487
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
22488
|
-
} = ctx;
|
|
22841
|
+
const { app } = ctx;
|
|
22489
22842
|
app.get("/api/schedules/today", (req, res) => {
|
|
22490
22843
|
try {
|
|
22491
22844
|
const clientDate = req.query.date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -22540,7 +22893,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
22540
22893
|
res.status(500).json({ error: e.message });
|
|
22541
22894
|
}
|
|
22542
22895
|
});
|
|
22543
|
-
app.post("/api/classes/:classId/schedules", (req, res) => {
|
|
22896
|
+
app.post("/api/classes/:classId/schedules", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22544
22897
|
try {
|
|
22545
22898
|
const { lessonId, scheduledDate, timeSlot, status, notes } = req.body;
|
|
22546
22899
|
const id = "sch-" + Date.now().toString(36);
|
|
@@ -22573,7 +22926,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
22573
22926
|
res.status(500).json({ error: e.message });
|
|
22574
22927
|
}
|
|
22575
22928
|
});
|
|
22576
|
-
app.put("/api/classes/:classId/schedules/:scheduleId", (req, res) => {
|
|
22929
|
+
app.put("/api/classes/:classId/schedules/:scheduleId", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22577
22930
|
try {
|
|
22578
22931
|
const { lessonId, scheduledDate, timeSlot, status, notes } = req.body;
|
|
22579
22932
|
kernelContainer.db.prepare(`
|
|
@@ -22594,7 +22947,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
22594
22947
|
res.status(500).json({ error: e.message });
|
|
22595
22948
|
}
|
|
22596
22949
|
});
|
|
22597
|
-
app.delete("/api/classes/:classId/schedules/:scheduleId", (req, res) => {
|
|
22950
|
+
app.delete("/api/classes/:classId/schedules/:scheduleId", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22598
22951
|
try {
|
|
22599
22952
|
kernelContainer.db.prepare("DELETE FROM schedules WHERE id = ? AND class_id = ?").run(req.params.scheduleId, req.params.classId);
|
|
22600
22953
|
kernelContainer.db.prepare("DELETE FROM attendance WHERE schedule_id = ?").run(req.params.scheduleId);
|
|
@@ -22603,7 +22956,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
22603
22956
|
res.status(500).json({ error: e.message });
|
|
22604
22957
|
}
|
|
22605
22958
|
});
|
|
22606
|
-
app.post("/api/classes/:classId/schedules/batch", (req, res) => {
|
|
22959
|
+
app.post("/api/classes/:classId/schedules/batch", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22607
22960
|
try {
|
|
22608
22961
|
const { schedules } = req.body;
|
|
22609
22962
|
const db2 = kernelContainer.db;
|
|
@@ -22632,7 +22985,7 @@ function registerSchedulesRoutes(ctx) {
|
|
|
22632
22985
|
res.status(500).json({ error: e.message });
|
|
22633
22986
|
}
|
|
22634
22987
|
});
|
|
22635
|
-
app.post("/api/timetable/ocr", async (req, res) => {
|
|
22988
|
+
app.post("/api/timetable/ocr", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
22636
22989
|
const startTime = Date.now();
|
|
22637
22990
|
console.log(`[OCR Start] Starting timetable OCR. Payload size: ${req.body.imageBase64?.length || 0} bytes. Lang: ${req.body.lang || "zh"}`);
|
|
22638
22991
|
try {
|
|
@@ -22800,24 +23153,10 @@ function registerSchedulesRoutes(ctx) {
|
|
|
22800
23153
|
}
|
|
22801
23154
|
|
|
22802
23155
|
// server/routes/grading.ts
|
|
22803
|
-
init_interfaces();
|
|
22804
23156
|
var import_genai6 = require("@google/genai");
|
|
23157
|
+
init_interfaces();
|
|
22805
23158
|
function registerGradingRoutes(ctx) {
|
|
22806
|
-
const {
|
|
22807
|
-
app,
|
|
22808
|
-
io,
|
|
22809
|
-
loginLimiter,
|
|
22810
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
22811
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
22812
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
22813
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
22814
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
22815
|
-
buildOpenAITools: buildOpenAITools2,
|
|
22816
|
-
executeAgentToolCall: executeAgentToolCall2,
|
|
22817
|
-
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
22818
|
-
runGeminiAgentChat: runGeminiAgentChat2,
|
|
22819
|
-
runOpenAIAgentChat: runOpenAIAgentChat2
|
|
22820
|
-
} = ctx;
|
|
23159
|
+
const { app } = ctx;
|
|
22821
23160
|
app.get("/api/classes/:classId/attendance-summary", (req, res) => {
|
|
22822
23161
|
try {
|
|
22823
23162
|
const classId = req.params.classId;
|
|
@@ -22931,7 +23270,7 @@ function registerGradingRoutes(ctx) {
|
|
|
22931
23270
|
res.status(500).json({ error: e.message });
|
|
22932
23271
|
}
|
|
22933
23272
|
});
|
|
22934
|
-
app.post("/api/schedules/:scheduleId/attendance", (req, res) => {
|
|
23273
|
+
app.post("/api/schedules/:scheduleId/attendance", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22935
23274
|
try {
|
|
22936
23275
|
const { studentId, status } = req.body;
|
|
22937
23276
|
kernelContainer.db.prepare(`
|
|
@@ -22961,7 +23300,7 @@ function registerGradingRoutes(ctx) {
|
|
|
22961
23300
|
res.status(500).json({ error: e.message });
|
|
22962
23301
|
}
|
|
22963
23302
|
});
|
|
22964
|
-
app.post("/api/classes/:classId/grade-weights", (req, res) => {
|
|
23303
|
+
app.post("/api/classes/:classId/grade-weights", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22965
23304
|
try {
|
|
22966
23305
|
const { attendance_weight, progress_weight, assignment_weight, exam_weight } = req.body;
|
|
22967
23306
|
const total = Number(attendance_weight) + Number(progress_weight) + Number(assignment_weight) + Number(exam_weight);
|
|
@@ -22995,7 +23334,7 @@ function registerGradingRoutes(ctx) {
|
|
|
22995
23334
|
res.status(500).json({ error: e.message });
|
|
22996
23335
|
}
|
|
22997
23336
|
});
|
|
22998
|
-
app.post("/api/classes/:classId/exams", (req, res) => {
|
|
23337
|
+
app.post("/api/classes/:classId/exams", requireAuth("teacher", "administrator"), (req, res) => {
|
|
22999
23338
|
try {
|
|
23000
23339
|
const { title, description, max_score } = req.body;
|
|
23001
23340
|
if (!title) return res.status(400).json({ error: "Title is required" });
|
|
@@ -23017,7 +23356,7 @@ function registerGradingRoutes(ctx) {
|
|
|
23017
23356
|
res.status(500).json({ error: e.message });
|
|
23018
23357
|
}
|
|
23019
23358
|
});
|
|
23020
|
-
app.post("/api/exams/:examId/scores", (req, res) => {
|
|
23359
|
+
app.post("/api/exams/:examId/scores", requireAuth("teacher", "administrator"), (req, res) => {
|
|
23021
23360
|
try {
|
|
23022
23361
|
const { scores } = req.body;
|
|
23023
23362
|
if (!Array.isArray(scores)) return res.status(400).json({ error: "Scores array is required" });
|
|
@@ -23173,11 +23512,8 @@ function registerGradingRoutes(ctx) {
|
|
|
23173
23512
|
res.status(500).json({ error: e.message });
|
|
23174
23513
|
}
|
|
23175
23514
|
});
|
|
23176
|
-
app.post("/api/grade-sync", async (req, res) => {
|
|
23515
|
+
app.post("/api/grade-sync", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
23177
23516
|
try {
|
|
23178
|
-
if (!checkIsTeacherOrAdmin(req)) {
|
|
23179
|
-
return res.status(403).json({ success: false, error: "Access Denied: Teachers or Administrators only" });
|
|
23180
|
-
}
|
|
23181
23517
|
const { lessonId, studentId, grade } = req.body;
|
|
23182
23518
|
if (!lessonId || !studentId || grade === void 0) {
|
|
23183
23519
|
return res.status(400).json({ success: false, error: "lessonId, studentId, and grade are required" });
|
|
@@ -23198,11 +23534,8 @@ function registerGradingRoutes(ctx) {
|
|
|
23198
23534
|
res.status(500).json({ success: false, error: e.message });
|
|
23199
23535
|
}
|
|
23200
23536
|
});
|
|
23201
|
-
app.post("/api/students/:studentId/points", async (req, res) => {
|
|
23537
|
+
app.post("/api/students/:studentId/points", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
23202
23538
|
try {
|
|
23203
|
-
if (!checkIsTeacherOrAdmin(req)) {
|
|
23204
|
-
return res.status(403).json({ success: false, error: "Access Denied: Teachers or Administrators only" });
|
|
23205
|
-
}
|
|
23206
23539
|
const { studentId } = req.params;
|
|
23207
23540
|
const { classId, dimensionId, deltaPoints, reason, pluginId } = req.body;
|
|
23208
23541
|
if (!classId || !dimensionId || deltaPoints === void 0 || !reason) {
|
|
@@ -23234,7 +23567,7 @@ function registerGradingRoutes(ctx) {
|
|
|
23234
23567
|
res.status(500).json({ success: false, error: e.message });
|
|
23235
23568
|
}
|
|
23236
23569
|
});
|
|
23237
|
-
app.post("/api/classes/:classId/semester-reports/archive", (req, res) => {
|
|
23570
|
+
app.post("/api/classes/:classId/semester-reports/archive", requireAuth("teacher", "administrator"), (req, res) => {
|
|
23238
23571
|
try {
|
|
23239
23572
|
const { semesterName, reports } = req.body;
|
|
23240
23573
|
if (!semesterName) return res.status(400).json({ error: "semesterName is required" });
|
|
@@ -23287,7 +23620,7 @@ function registerGradingRoutes(ctx) {
|
|
|
23287
23620
|
res.status(500).json({ error: e.message });
|
|
23288
23621
|
}
|
|
23289
23622
|
});
|
|
23290
|
-
app.post("/api/classes/:classId/students/:studentId/semester-ai-evaluation", async (req, res) => {
|
|
23623
|
+
app.post("/api/classes/:classId/students/:studentId/semester-ai-evaluation", requireAuth("teacher", "administrator"), async (req, res) => {
|
|
23291
23624
|
try {
|
|
23292
23625
|
const { classId, studentId } = req.params;
|
|
23293
23626
|
const { semesterName = "2026\u5E74\u6625\u5B63\u5B66\uFFFD?", providerId } = req.body;
|
|
@@ -23387,8 +23720,8 @@ ${examsText}
|
|
|
23387
23720
|
|
|
23388
23721
|
// server/routes/plugins.ts
|
|
23389
23722
|
var import_express2 = __toESM(require("express"), 1);
|
|
23390
|
-
var
|
|
23391
|
-
var
|
|
23723
|
+
var import_path14 = __toESM(require("path"), 1);
|
|
23724
|
+
var import_fs13 = __toESM(require("fs"), 1);
|
|
23392
23725
|
|
|
23393
23726
|
// server/services/version-fetcher.ts
|
|
23394
23727
|
var import_child_process2 = require("child_process");
|
|
@@ -23570,13 +23903,6 @@ function isSafeExternalUrl(urlStr) {
|
|
|
23570
23903
|
function registerPluginsRoutes(ctx) {
|
|
23571
23904
|
const {
|
|
23572
23905
|
app,
|
|
23573
|
-
io,
|
|
23574
|
-
loginLimiter,
|
|
23575
|
-
MF_REMOTE_CACHE: MF_REMOTE_CACHE2,
|
|
23576
|
-
lessonActiveSegments: lessonActiveSegments2,
|
|
23577
|
-
buildAgentSystemInstruction: buildAgentSystemInstruction2,
|
|
23578
|
-
buildAgentFinalMessage: buildAgentFinalMessage2,
|
|
23579
|
-
normalizeToolSchema: normalizeToolSchema2,
|
|
23580
23906
|
buildOpenAITools: buildOpenAITools2,
|
|
23581
23907
|
executeAgentToolCall: executeAgentToolCall2,
|
|
23582
23908
|
buildOpenAIChatUrl: buildOpenAIChatUrl2,
|
|
@@ -23585,29 +23911,26 @@ function registerPluginsRoutes(ctx) {
|
|
|
23585
23911
|
} = ctx;
|
|
23586
23912
|
app.get("/api/docs/plugin-guide", (req, res) => {
|
|
23587
23913
|
try {
|
|
23588
|
-
const docPath =
|
|
23589
|
-
if (!
|
|
23914
|
+
const docPath = import_path14.default.join(process.cwd(), "docs_plugin_guide.md");
|
|
23915
|
+
if (!import_fs13.default.existsSync(docPath)) {
|
|
23590
23916
|
return res.status(404).json({ success: false, error: "Document not found" });
|
|
23591
23917
|
}
|
|
23592
|
-
const content =
|
|
23918
|
+
const content = import_fs13.default.readFileSync(docPath, "utf-8");
|
|
23593
23919
|
res.json({ success: true, content });
|
|
23594
23920
|
} catch (e) {
|
|
23595
|
-
res
|
|
23921
|
+
sendSafeError(res, e);
|
|
23596
23922
|
}
|
|
23597
23923
|
});
|
|
23598
|
-
app.get("/api/admin/logs", (req, res) => {
|
|
23599
|
-
if (!checkIsTeacherOrAdmin(req)) {
|
|
23600
|
-
return res.status(403).json({ success: false, error: "Access denied: teachers or admins only" });
|
|
23601
|
-
}
|
|
23924
|
+
app.get("/api/admin/logs", requireAuth("administrator"), (req, res) => {
|
|
23602
23925
|
try {
|
|
23603
23926
|
const limit = parseInt(req.query.limit) || 200;
|
|
23604
23927
|
const component = req.query.component;
|
|
23605
23928
|
const levelFilter = req.query.level;
|
|
23606
|
-
const logFile2 =
|
|
23607
|
-
if (!
|
|
23929
|
+
const logFile2 = import_path14.default.resolve(process.cwd(), "logs", "openlearn.log");
|
|
23930
|
+
if (!import_fs13.default.existsSync(logFile2)) {
|
|
23608
23931
|
return res.json({ success: true, logs: [] });
|
|
23609
23932
|
}
|
|
23610
|
-
const content =
|
|
23933
|
+
const content = import_fs13.default.readFileSync(logFile2, "utf-8");
|
|
23611
23934
|
const lines = content.split("\n").filter(Boolean);
|
|
23612
23935
|
let parsedLogs = lines.map((line) => {
|
|
23613
23936
|
try {
|
|
@@ -23638,7 +23961,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23638
23961
|
const sliceStart = Math.max(0, parsedLogs.length - limit);
|
|
23639
23962
|
res.json({ success: true, logs: parsedLogs.slice(sliceStart) });
|
|
23640
23963
|
} catch (e) {
|
|
23641
|
-
res
|
|
23964
|
+
sendSafeError(res, e);
|
|
23642
23965
|
}
|
|
23643
23966
|
});
|
|
23644
23967
|
app.get("/api/plugins", (req, res) => {
|
|
@@ -23663,7 +23986,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23663
23986
|
isSystem: manifestId.startsWith("@openlearn/") || found.pluginId.startsWith("@openlearn/")
|
|
23664
23987
|
});
|
|
23665
23988
|
} catch (e) {
|
|
23666
|
-
res
|
|
23989
|
+
sendSafeError(res, e);
|
|
23667
23990
|
}
|
|
23668
23991
|
});
|
|
23669
23992
|
app.get("/api/plugins/market", async (req, res) => {
|
|
@@ -23689,7 +24012,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23689
24012
|
}
|
|
23690
24013
|
res.json({ success: true, market: results });
|
|
23691
24014
|
} catch (e) {
|
|
23692
|
-
res
|
|
24015
|
+
sendSafeError(res, e);
|
|
23693
24016
|
}
|
|
23694
24017
|
});
|
|
23695
24018
|
app.post("/api/plugins/:id(*)/check-update", async (req, res) => {
|
|
@@ -23711,7 +24034,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23711
24034
|
const result = await checkVersion(src, manifest.version || "0.0.0");
|
|
23712
24035
|
res.json({ success: true, ...result });
|
|
23713
24036
|
} catch (e) {
|
|
23714
|
-
res
|
|
24037
|
+
sendSafeError(res, e);
|
|
23715
24038
|
}
|
|
23716
24039
|
});
|
|
23717
24040
|
app.post("/api/plugins/:id(*)/one-click-update", requireAuth("administrator"), async (req, res) => {
|
|
@@ -23750,9 +24073,9 @@ function registerPluginsRoutes(ctx) {
|
|
|
23750
24073
|
});
|
|
23751
24074
|
}
|
|
23752
24075
|
} else {
|
|
23753
|
-
const zipPath =
|
|
23754
|
-
if (
|
|
23755
|
-
zipBuffer =
|
|
24076
|
+
const zipPath = import_path14.default.resolve(process.cwd(), "v2_plugins/research-workflow/aymwoo-plugin-research-workflow.zip");
|
|
24077
|
+
if (import_fs13.default.existsSync(zipPath)) {
|
|
24078
|
+
zipBuffer = import_fs13.default.readFileSync(zipPath);
|
|
23756
24079
|
} else {
|
|
23757
24080
|
return res.status(404).json({ success: false, error: "\u672A\u627E\u5230\u66F4\u65B0\u5B89\u88C5\u5305" });
|
|
23758
24081
|
}
|
|
@@ -23772,7 +24095,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23772
24095
|
});
|
|
23773
24096
|
} catch (err) {
|
|
23774
24097
|
console.error(err);
|
|
23775
|
-
res
|
|
24098
|
+
sendSafeError(res, err);
|
|
23776
24099
|
}
|
|
23777
24100
|
});
|
|
23778
24101
|
app.get("/api/plugins/:id(*)/contributions", (req, res) => {
|
|
@@ -23781,7 +24104,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23781
24104
|
const summary = kernelContainer.pluginHost.listContributions(rawId);
|
|
23782
24105
|
res.json({ success: true, result: summary });
|
|
23783
24106
|
} catch (e) {
|
|
23784
|
-
res
|
|
24107
|
+
sendSafeError(res, e);
|
|
23785
24108
|
}
|
|
23786
24109
|
});
|
|
23787
24110
|
app.get("/api/plugins/:id(*)/config", (req, res) => {
|
|
@@ -23801,7 +24124,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23801
24124
|
}
|
|
23802
24125
|
});
|
|
23803
24126
|
} catch (e) {
|
|
23804
|
-
res
|
|
24127
|
+
sendSafeError(res, e);
|
|
23805
24128
|
}
|
|
23806
24129
|
});
|
|
23807
24130
|
app.post("/api/plugins/:id(*)/config", requireAuth("administrator"), (req, res) => {
|
|
@@ -23820,7 +24143,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23820
24143
|
kernelContainer.pluginHost.setPluginConfig(pluginId, manifest, updates);
|
|
23821
24144
|
res.json({ success: true });
|
|
23822
24145
|
} catch (e) {
|
|
23823
|
-
res
|
|
24146
|
+
sendSafeError(res, e);
|
|
23824
24147
|
}
|
|
23825
24148
|
});
|
|
23826
24149
|
app.post("/api/plugins/:id(*)/toggle", requireAuth("administrator"), async (req, res) => {
|
|
@@ -23834,7 +24157,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23834
24157
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23835
24158
|
res.json(result);
|
|
23836
24159
|
} catch (err) {
|
|
23837
|
-
res
|
|
24160
|
+
sendSafeError(res, err);
|
|
23838
24161
|
}
|
|
23839
24162
|
});
|
|
23840
24163
|
app.delete("/api/plugins/:id(*)", requireAuth("administrator"), async (req, res) => {
|
|
@@ -23848,7 +24171,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23848
24171
|
const result = await kernelContainer.commandBus.execute(cmd);
|
|
23849
24172
|
res.json(result);
|
|
23850
24173
|
} catch (err) {
|
|
23851
|
-
res
|
|
24174
|
+
sendSafeError(res, err);
|
|
23852
24175
|
}
|
|
23853
24176
|
});
|
|
23854
24177
|
app.get("/api/plugins/:id(*)", async (req, res) => {
|
|
@@ -23877,7 +24200,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23877
24200
|
res.json(result);
|
|
23878
24201
|
} catch (err) {
|
|
23879
24202
|
console.error(err);
|
|
23880
|
-
res
|
|
24203
|
+
sendSafeError(res, err);
|
|
23881
24204
|
}
|
|
23882
24205
|
});
|
|
23883
24206
|
app.post("/api/plugins/upload-zip", requireAuth("administrator"), async (req, res) => {
|
|
@@ -23892,7 +24215,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23892
24215
|
res.json(result);
|
|
23893
24216
|
} catch (err) {
|
|
23894
24217
|
console.error(err);
|
|
23895
|
-
res
|
|
24218
|
+
sendSafeError(res, err);
|
|
23896
24219
|
}
|
|
23897
24220
|
});
|
|
23898
24221
|
app.post("/api/plugins/upload-zip-raw", requireAuth("administrator"), import_express2.default.raw({ type: "application/octet-stream", limit: "400mb" }), async (req, res) => {
|
|
@@ -23934,7 +24257,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23934
24257
|
});
|
|
23935
24258
|
} catch (err) {
|
|
23936
24259
|
console.error(err);
|
|
23937
|
-
res
|
|
24260
|
+
sendSafeError(res, err);
|
|
23938
24261
|
}
|
|
23939
24262
|
});
|
|
23940
24263
|
app.post(
|
|
@@ -23967,7 +24290,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
23967
24290
|
});
|
|
23968
24291
|
} catch (err) {
|
|
23969
24292
|
console.error(err);
|
|
23970
|
-
res
|
|
24293
|
+
sendSafeError(res, err);
|
|
23971
24294
|
}
|
|
23972
24295
|
}
|
|
23973
24296
|
);
|
|
@@ -24011,10 +24334,10 @@ function registerPluginsRoutes(ctx) {
|
|
|
24011
24334
|
res.json({ success: true, result });
|
|
24012
24335
|
} catch (err) {
|
|
24013
24336
|
console.error("[execute-command]", err.message);
|
|
24014
|
-
res
|
|
24337
|
+
sendSafeError(res, err);
|
|
24015
24338
|
}
|
|
24016
24339
|
});
|
|
24017
|
-
app.get("/api/ai-providers", (req, res) => {
|
|
24340
|
+
app.get("/api/ai-providers", requireAuth("teacher", "administrator"), (req, res) => {
|
|
24018
24341
|
try {
|
|
24019
24342
|
const providers = kernelContainer.db.prepare("SELECT * FROM ai_providers ORDER BY created_at DESC").all();
|
|
24020
24343
|
const masked = providers.map((p) => ({
|
|
@@ -24023,7 +24346,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24023
24346
|
}));
|
|
24024
24347
|
res.json(masked);
|
|
24025
24348
|
} catch (e) {
|
|
24026
|
-
res
|
|
24349
|
+
sendSafeError(res, e);
|
|
24027
24350
|
}
|
|
24028
24351
|
});
|
|
24029
24352
|
app.post("/api/ai-providers", requireAuth("administrator"), (req, res) => {
|
|
@@ -24038,7 +24361,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24038
24361
|
kernelContainer.db.prepare("INSERT INTO ai_providers (id, name, api_url, api_key, model_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, name, api_url, encryptedKey, model_name, now, now);
|
|
24039
24362
|
res.json({ success: true, id });
|
|
24040
24363
|
} catch (e) {
|
|
24041
|
-
res
|
|
24364
|
+
sendSafeError(res, e);
|
|
24042
24365
|
}
|
|
24043
24366
|
});
|
|
24044
24367
|
app.put("/api/ai-providers/:id", requireAuth("administrator"), (req, res) => {
|
|
@@ -24058,7 +24381,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24058
24381
|
kernelContainer.db.prepare("UPDATE ai_providers SET name = ?, api_url = ?, api_key = ?, model_name = ?, updated_at = ? WHERE id = ?").run(name, api_url, finalKey, model_name, now, req.params.id);
|
|
24059
24382
|
res.json({ success: true });
|
|
24060
24383
|
} catch (e) {
|
|
24061
|
-
res
|
|
24384
|
+
sendSafeError(res, e);
|
|
24062
24385
|
}
|
|
24063
24386
|
});
|
|
24064
24387
|
app.delete("/api/ai-providers/:id", requireAuth("administrator"), (req, res) => {
|
|
@@ -24066,7 +24389,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24066
24389
|
kernelContainer.db.prepare("DELETE FROM ai_providers WHERE id = ?").run(req.params.id);
|
|
24067
24390
|
res.json({ success: true });
|
|
24068
24391
|
} catch (e) {
|
|
24069
|
-
res
|
|
24392
|
+
sendSafeError(res, e);
|
|
24070
24393
|
}
|
|
24071
24394
|
});
|
|
24072
24395
|
app.get("/api/site-settings", (req, res) => {
|
|
@@ -24078,7 +24401,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24078
24401
|
logoUrl: row?.logo_url || null
|
|
24079
24402
|
});
|
|
24080
24403
|
} catch (e) {
|
|
24081
|
-
res
|
|
24404
|
+
sendSafeError(res, e);
|
|
24082
24405
|
}
|
|
24083
24406
|
});
|
|
24084
24407
|
app.put("/api/site-settings", requireAuth("administrator"), (req, res) => {
|
|
@@ -24093,7 +24416,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24093
24416
|
siteInfo: { siteName: siteName || "", slogan: slogan || "", logoUrl: logoUrl || null }
|
|
24094
24417
|
});
|
|
24095
24418
|
} catch (e) {
|
|
24096
|
-
res
|
|
24419
|
+
sendSafeError(res, e);
|
|
24097
24420
|
}
|
|
24098
24421
|
});
|
|
24099
24422
|
app.post("/api/ai-providers/test", requireAuth("administrator"), async (req, res) => {
|
|
@@ -24142,7 +24465,7 @@ function registerPluginsRoutes(ctx) {
|
|
|
24142
24465
|
res.status(response.status).json({ success: false, error: `API responded with status ${response.status}: ${responseText.slice(0, 200)}` });
|
|
24143
24466
|
}
|
|
24144
24467
|
} catch (e) {
|
|
24145
|
-
res
|
|
24468
|
+
sendSafeError(res, e, 500, "Connection failed");
|
|
24146
24469
|
}
|
|
24147
24470
|
});
|
|
24148
24471
|
}
|
|
@@ -24162,6 +24485,15 @@ async function startServer() {
|
|
|
24162
24485
|
environment: process.env.NODE_ENV || "development",
|
|
24163
24486
|
config: { port: Number(process.env.PORT) || 9e3 }
|
|
24164
24487
|
});
|
|
24488
|
+
try {
|
|
24489
|
+
const migrationsDir = import_path15.default.join(process.cwd(), "migrations");
|
|
24490
|
+
const migrations = loadMigrationsFromDirectory(migrationsDir);
|
|
24491
|
+
if (migrations.length > 0) {
|
|
24492
|
+
runMigrations(kernelContainer.db, migrations);
|
|
24493
|
+
}
|
|
24494
|
+
} catch (err) {
|
|
24495
|
+
console.error("[Migration] Failed to run database migrations:", err);
|
|
24496
|
+
}
|
|
24165
24497
|
await runStartupMigrations(kernelContainer.db);
|
|
24166
24498
|
await kernelContainer.ready;
|
|
24167
24499
|
const activityRegistry = new ActivityRegistry2();
|
|
@@ -24172,23 +24504,27 @@ async function startServer() {
|
|
|
24172
24504
|
kernelContainer.pluginHost.setExpressApp(app);
|
|
24173
24505
|
const PORT = parseInt(process.env.PORT || "9000", 10);
|
|
24174
24506
|
app.set("trust proxy", 1);
|
|
24507
|
+
const frameAllowedOrigins = process.env.ALLOWED_FRAME_ORIGINS ? process.env.ALLOWED_FRAME_ORIGINS.split(",").map((s) => s.trim()) : ["http://localhost:*", "http://127.0.0.1:*"];
|
|
24175
24508
|
app.use((0, import_helmet.default)({
|
|
24176
24509
|
contentSecurityPolicy: {
|
|
24177
24510
|
directives: {
|
|
24178
24511
|
defaultSrc: ["'self'"],
|
|
24179
|
-
|
|
24180
|
-
|
|
24512
|
+
// 移除通配 https: 与 data:,禁止加载全网任意第三方未授权脚本
|
|
24513
|
+
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "blob:"],
|
|
24514
|
+
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
|
24181
24515
|
imgSrc: ["'self'", "data:", "blob:", "https:"],
|
|
24182
24516
|
connectSrc: ["'self'", "ws:", "wss:", "https:"],
|
|
24183
|
-
fontSrc: ["'self'", "data:", "https
|
|
24184
|
-
|
|
24185
|
-
|
|
24517
|
+
fontSrc: ["'self'", "data:", "https://fonts.gstatic.com"],
|
|
24518
|
+
// 移除通配 http: 和 https:,限制 iframe 仅能加载本地、沙箱或受信任课件源
|
|
24519
|
+
frameSrc: ["'self'", "blob:", "data:", ...frameAllowedOrigins],
|
|
24520
|
+
objectSrc: ["'none'"],
|
|
24521
|
+
baseUri: ["'self'"]
|
|
24186
24522
|
}
|
|
24187
24523
|
},
|
|
24188
24524
|
crossOriginOpenerPolicy: false,
|
|
24189
24525
|
crossOriginEmbedderPolicy: false,
|
|
24190
24526
|
crossOriginResourcePolicy: { policy: "cross-origin" },
|
|
24191
|
-
// 允许沙箱 iframe(opaque origin
|
|
24527
|
+
// 允许沙箱 iframe(opaque origin)加载静态资?
|
|
24192
24528
|
originAgentCluster: false,
|
|
24193
24529
|
strictTransportSecurity: false
|
|
24194
24530
|
// �? HTTP 部署,禁�? HSTS(否则浏览器缓存后强�? HTTPS,导�? ERR_CONNECTION_REFUSED�?
|
|
@@ -24203,8 +24539,8 @@ async function startServer() {
|
|
|
24203
24539
|
});
|
|
24204
24540
|
app.use(import_express3.default.json({ limit: "10mb" }));
|
|
24205
24541
|
app.use(import_express3.default.urlencoded({ limit: "10mb", extended: true }));
|
|
24206
|
-
app.use("/uploads", import_express3.default.static(
|
|
24207
|
-
app.use("/plugins", import_express3.default.static(
|
|
24542
|
+
app.use("/uploads", import_express3.default.static(import_path15.default.join(process.cwd(), "uploads")));
|
|
24543
|
+
app.use("/plugins", import_express3.default.static(import_path15.default.join(process.cwd(), "plugins")));
|
|
24208
24544
|
app.use((req, res, next) => {
|
|
24209
24545
|
const origin = req.headers.origin;
|
|
24210
24546
|
if (origin === "null" || origin === void 0) {
|
|
@@ -24234,14 +24570,58 @@ async function startServer() {
|
|
|
24234
24570
|
activityRegistry
|
|
24235
24571
|
};
|
|
24236
24572
|
const httpServer = (0, import_http.createServer)(app);
|
|
24237
|
-
const allowedOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(",") : process.env.NODE_ENV === "production" ? [] : ["http://localhost:5173", "http://localhost:9000", "http://localhost:4173"];
|
|
24573
|
+
const allowedOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(",").map((s) => s.trim()) : process.env.NODE_ENV === "production" ? [] : ["http://localhost:5173", "http://localhost:9000", "http://localhost:4173"];
|
|
24238
24574
|
const io = new import_socket.Server(httpServer, {
|
|
24239
24575
|
cors: {
|
|
24240
|
-
origin:
|
|
24576
|
+
origin: (origin, callback) => {
|
|
24577
|
+
if (!origin) return callback(null, true);
|
|
24578
|
+
if (allowedOrigins.length === 0) {
|
|
24579
|
+
return callback(new Error("CORS not allowed"));
|
|
24580
|
+
}
|
|
24581
|
+
if (allowedOrigins.includes(origin) || allowedOrigins.includes("*")) {
|
|
24582
|
+
return callback(null, true);
|
|
24583
|
+
}
|
|
24584
|
+
return callback(new Error("Not allowed by CORS"));
|
|
24585
|
+
},
|
|
24241
24586
|
methods: ["GET", "POST"],
|
|
24242
24587
|
credentials: true
|
|
24243
24588
|
}
|
|
24244
24589
|
});
|
|
24590
|
+
io.use((socket, next) => {
|
|
24591
|
+
try {
|
|
24592
|
+
const cookieHeader = socket.handshake.headers.cookie;
|
|
24593
|
+
let token = null;
|
|
24594
|
+
if (cookieHeader) {
|
|
24595
|
+
const parts = cookieHeader.split(";");
|
|
24596
|
+
for (const part of parts) {
|
|
24597
|
+
const trimmed = part.trim();
|
|
24598
|
+
if (trimmed.startsWith("edu_os_token=")) {
|
|
24599
|
+
token = trimmed.substring("edu_os_token=".length);
|
|
24600
|
+
break;
|
|
24601
|
+
}
|
|
24602
|
+
}
|
|
24603
|
+
}
|
|
24604
|
+
if (!token && socket.handshake.auth?.token) {
|
|
24605
|
+
token = socket.handshake.auth.token;
|
|
24606
|
+
}
|
|
24607
|
+
if (process.env.NODE_ENV === "test" && !token) {
|
|
24608
|
+
return next();
|
|
24609
|
+
}
|
|
24610
|
+
if (!token) {
|
|
24611
|
+
return next(new Error("Authentication required: missing edu_os_token"));
|
|
24612
|
+
}
|
|
24613
|
+
const session = getValidSession(token);
|
|
24614
|
+
if (!session) {
|
|
24615
|
+
return next(new Error("Authentication required: session expired or invalid"));
|
|
24616
|
+
}
|
|
24617
|
+
socket.data.session = session;
|
|
24618
|
+
socket.data.userId = session.userId;
|
|
24619
|
+
socket.data.role = session.role;
|
|
24620
|
+
next();
|
|
24621
|
+
} catch (err) {
|
|
24622
|
+
next(new Error(`Authentication error: ${err.message}`));
|
|
24623
|
+
}
|
|
24624
|
+
});
|
|
24245
24625
|
kernelContainer.pluginHost.setSocketIO(io);
|
|
24246
24626
|
ctx.io = io;
|
|
24247
24627
|
registerOsRoutes(ctx);
|
|
@@ -24270,10 +24650,10 @@ async function startServer() {
|
|
|
24270
24650
|
});
|
|
24271
24651
|
app.use(vite.middlewares);
|
|
24272
24652
|
} else {
|
|
24273
|
-
const distPath = __dirname ||
|
|
24653
|
+
const distPath = __dirname || import_path15.default.join(process.cwd(), "dist");
|
|
24274
24654
|
app.use(import_express3.default.static(distPath));
|
|
24275
24655
|
app.get("*", (req, res) => {
|
|
24276
|
-
res.sendFile(
|
|
24656
|
+
res.sendFile(import_path15.default.join(distPath, "index.html"));
|
|
24277
24657
|
});
|
|
24278
24658
|
}
|
|
24279
24659
|
httpServer.on("error", (err) => {
|
|
@@ -24293,8 +24673,8 @@ async function startServer() {
|
|
|
24293
24673
|
const startTime = Date.now();
|
|
24294
24674
|
let platformVersion = "0.0.0";
|
|
24295
24675
|
try {
|
|
24296
|
-
const pkgPath =
|
|
24297
|
-
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"));
|
|
24298
24678
|
platformVersion = pkg.version ?? platformVersion;
|
|
24299
24679
|
} catch {
|
|
24300
24680
|
}
|
|
@@ -24310,7 +24690,7 @@ async function startServer() {
|
|
|
24310
24690
|
res.status(503).json({ status: "not_ready", error: e.message });
|
|
24311
24691
|
}
|
|
24312
24692
|
});
|
|
24313
|
-
app.get("/metrics", (_req, res) => {
|
|
24693
|
+
app.get("/metrics", requireAuth("administrator"), (_req, res) => {
|
|
24314
24694
|
const mem = process.memoryUsage();
|
|
24315
24695
|
res.json({
|
|
24316
24696
|
uptime: Math.floor((Date.now() - startTime) / 1e3),
|