@playcademy/sandbox 0.3.17-beta.5 → 0.3.17-beta.7

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.
Files changed (3) hide show
  1. package/dist/cli.js +210 -21
  2. package/dist/server.js +210 -21
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1310,7 +1310,7 @@ var package_default;
1310
1310
  var init_package = __esm(() => {
1311
1311
  package_default = {
1312
1312
  name: "@playcademy/sandbox",
1313
- version: "0.3.17-beta.5",
1313
+ version: "0.3.17-beta.7",
1314
1314
  description: "Local development server for Playcademy game development",
1315
1315
  type: "module",
1316
1316
  exports: {
@@ -11550,7 +11550,7 @@ var init_table6 = __esm(() => {
11550
11550
  init_drizzle_orm();
11551
11551
  init_pg_core();
11552
11552
  init_table5();
11553
- userRoleEnum = pgEnum("user_role", ["admin", "player", "developer"]);
11553
+ userRoleEnum = pgEnum("user_role", ["admin", "player", "developer", "teacher"]);
11554
11554
  developerStatusEnum = pgEnum("developer_status", ["none", "pending", "approved"]);
11555
11555
  users = pgTable("user", {
11556
11556
  id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
@@ -26733,10 +26733,13 @@ var init_game_service = __esm(() => {
26733
26733
  });
26734
26734
  }
26735
26735
  async listManageable(user) {
26736
- this.validateDeveloperStatus(user);
26736
+ const seesAllGames = user.role === "admin" || user.role === "teacher";
26737
+ if (!seesAllGames) {
26738
+ this.validateDeveloperStatus(user);
26739
+ }
26737
26740
  const db2 = this.deps.db;
26738
26741
  return db2.query.games.findMany({
26739
- where: user.role === "admin" ? undefined : eq(games.developerId, user.id),
26742
+ where: seesAllGames ? undefined : eq(games.developerId, user.id),
26740
26743
  orderBy: [desc(games.createdAt)]
26741
26744
  });
26742
26745
  }
@@ -27140,6 +27143,19 @@ var init_game_service = __esm(() => {
27140
27143
  throw new NotFoundError("Game", gameId);
27141
27144
  }
27142
27145
  }
27146
+ async validateGameManagementAccess(user, gameId) {
27147
+ if (user.role === "admin" || user.role === "teacher") {
27148
+ const gameExists = await this.deps.db.query.games.findFirst({
27149
+ where: eq(games.id, gameId),
27150
+ columns: { id: true }
27151
+ });
27152
+ if (!gameExists) {
27153
+ throw new NotFoundError("Game", gameId);
27154
+ }
27155
+ return;
27156
+ }
27157
+ return this.validateDeveloperAccess(user, gameId);
27158
+ }
27143
27159
  async validateDeveloperAccessBySlug(user, slug) {
27144
27160
  this.validateDeveloperStatus(user);
27145
27161
  const db2 = this.deps.db;
@@ -27210,6 +27226,7 @@ function createGameServices(deps) {
27210
27226
  validators: {
27211
27227
  validateDeveloperAccessBySlug: (user, slug) => game.validateDeveloperAccessBySlug(user, slug),
27212
27228
  validateDeveloperAccess: (user, gameId) => game.validateDeveloperAccess(user, gameId),
27229
+ validateGameManagementAccess: (user, gameId) => game.validateGameManagementAccess(user, gameId),
27213
27230
  validateOwnership: (user, gameId) => game.validateOwnership(user, gameId)
27214
27231
  }
27215
27232
  };
@@ -30612,9 +30629,13 @@ class TimebackAdminService {
30612
30629
  });
30613
30630
  });
30614
30631
  }
30615
- async resolveAdminMutationContext(gameId, courseId, user, studentId) {
30632
+ async resolveAdminMutationContext(gameId, courseId, user, studentId, accessLevel = "developer") {
30616
30633
  const client = this.requireClient();
30617
- await this.deps.validateDeveloperAccess(user, gameId);
30634
+ if (accessLevel === "dashboard") {
30635
+ await this.deps.validateGameManagementAccess(user, gameId);
30636
+ } else {
30637
+ await this.deps.validateDeveloperAccess(user, gameId);
30638
+ }
30618
30639
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
30619
30640
  where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
30620
30641
  });
@@ -30874,7 +30895,7 @@ class TimebackAdminService {
30874
30895
  }
30875
30896
  async listStudentsForCourse(gameId, courseId, user) {
30876
30897
  const client = this.requireClient();
30877
- await this.deps.validateDeveloperAccess(user, gameId);
30898
+ await this.deps.validateGameManagementAccess(user, gameId);
30878
30899
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
30879
30900
  where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
30880
30901
  });
@@ -30912,7 +30933,7 @@ class TimebackAdminService {
30912
30933
  }
30913
30934
  async getStudentOverview(gameId, studentId, user, courseId) {
30914
30935
  const client = this.requireClient();
30915
- await this.deps.validateDeveloperAccess(user, gameId);
30936
+ await this.deps.validateGameManagementAccess(user, gameId);
30916
30937
  const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
30917
30938
  where: courseId ? and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId)) : eq(gameTimebackIntegrations.gameId, gameId)
30918
30939
  });
@@ -30966,7 +30987,7 @@ class TimebackAdminService {
30966
30987
  const client = this.requireClient();
30967
30988
  const safeLimit = Math.max(1, Math.min(limit, TimebackAdminService.MAX_STUDENT_ACTIVITY_LIMIT));
30968
30989
  const safeOffset = Math.max(0, Math.min(offset, TimebackAdminService.MAX_STUDENT_ACTIVITY_OFFSET));
30969
- await this.deps.validateDeveloperAccess(user, gameId);
30990
+ await this.deps.validateGameManagementAccess(user, gameId);
30970
30991
  const [integration, sensorUrl] = await Promise.all([
30971
30992
  this.deps.db.query.gameTimebackIntegrations.findFirst({
30972
30993
  where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
@@ -31030,7 +31051,7 @@ class TimebackAdminService {
31030
31051
  return { status: "ok" };
31031
31052
  }
31032
31053
  async toggleCourseCompletion(data, user) {
31033
- const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
31054
+ const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId, "dashboard");
31034
31055
  const historyClient = client;
31035
31056
  const ids = deriveSourcedIds(data.courseId);
31036
31057
  const lineItemId = `${ids.course}-mastery-completion-assessment`;
@@ -31123,6 +31144,77 @@ class TimebackAdminService {
31123
31144
  }
31124
31145
  return { status: "ok" };
31125
31146
  }
31147
+ async searchStudentsForEnrollment(gameId, courseId, query, user) {
31148
+ const client = this.requireClient();
31149
+ await this.deps.validateGameManagementAccess(user, gameId);
31150
+ const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
31151
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
31152
+ });
31153
+ if (!integration) {
31154
+ throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
31155
+ }
31156
+ const trimmedQuery = query.trim();
31157
+ if (trimmedQuery.length < 2) {
31158
+ return { students: [] };
31159
+ }
31160
+ const filterParts = [
31161
+ `givenName~'${escapeFilterValue(trimmedQuery)}'`,
31162
+ `familyName~'${escapeFilterValue(trimmedQuery)}'`,
31163
+ `email~'${escapeFilterValue(trimmedQuery)}'`
31164
+ ];
31165
+ const filter = filterParts.join(" OR ");
31166
+ const params = new URLSearchParams({ filter, limit: "25" });
31167
+ const endpoint = `/ims/oneroster/rostering/v1p2/users?${params}`;
31168
+ let allUsers = [];
31169
+ try {
31170
+ const response = await client["request"](endpoint, "GET");
31171
+ allUsers = response.users || [];
31172
+ } catch (error) {
31173
+ logger16.warn("Failed to search OneRoster users", {
31174
+ query: trimmedQuery,
31175
+ error: error instanceof Error ? error.message : String(error)
31176
+ });
31177
+ return { students: [] };
31178
+ }
31179
+ const roster = await client.oneroster.enrollments.listByCourse(courseId, {
31180
+ role: "student",
31181
+ includeUsers: false
31182
+ });
31183
+ const enrolledStudentIds = new Set(roster.map((entry) => entry.enrollment.user.sourcedId));
31184
+ const students = allUsers.filter((entry) => Boolean(entry.sourcedId) && entry.roles?.some((role) => role.role === "student") === true).map((entry) => ({
31185
+ studentId: entry.sourcedId,
31186
+ name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || entry.sourcedId,
31187
+ email: entry.email || null,
31188
+ alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId)
31189
+ }));
31190
+ return { students };
31191
+ }
31192
+ async enrollStudent(data, user) {
31193
+ const client = this.requireClient();
31194
+ await this.deps.validateGameManagementAccess(user, data.gameId);
31195
+ const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
31196
+ where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
31197
+ });
31198
+ if (!integration) {
31199
+ throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
31200
+ }
31201
+ await client.edubridge.enrollments.enroll(data.studentId, data.courseId, {
31202
+ role: "student"
31203
+ });
31204
+ return { status: "ok" };
31205
+ }
31206
+ async unenrollStudent(data, user) {
31207
+ const client = this.requireClient();
31208
+ await this.deps.validateGameManagementAccess(user, data.gameId);
31209
+ const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
31210
+ where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
31211
+ });
31212
+ if (!integration) {
31213
+ throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
31214
+ }
31215
+ await client.edubridge.enrollments.unenroll(data.studentId, data.courseId);
31216
+ return { status: "ok" };
31217
+ }
31126
31218
  async getCompletionStatus(client, courseId, studentId) {
31127
31219
  const ids = deriveSourcedIds(courseId);
31128
31220
  const lineItemId = `${ids.course}-mastery-completion-assessment`;
@@ -31574,7 +31666,7 @@ class TimebackService {
31574
31666
  return { integrations, ...verbose && verboseData.length > 0 && { verbose: verboseData } };
31575
31667
  }
31576
31668
  async getIntegrations(gameId, user) {
31577
- await this.deps.validateDeveloperAccess(user, gameId);
31669
+ await this.deps.validateGameManagementAccess(user, gameId);
31578
31670
  const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
31579
31671
  where: eq(gameTimebackIntegrations.gameId, gameId)
31580
31672
  });
@@ -31829,6 +31921,7 @@ function createPlatformServices(deps) {
31829
31921
  alerts,
31830
31922
  validateDeveloperAccessBySlug,
31831
31923
  validateDeveloperAccess,
31924
+ validateGameManagementAccess,
31832
31925
  validateOwnership
31833
31926
  } = deps;
31834
31927
  const bucket = new BucketService({
@@ -31863,12 +31956,14 @@ function createPlatformServices(deps) {
31863
31956
  const timeback2 = new TimebackService({
31864
31957
  db: db2,
31865
31958
  timeback: timebackClient,
31866
- validateDeveloperAccess
31959
+ validateDeveloperAccess,
31960
+ validateGameManagementAccess
31867
31961
  });
31868
31962
  const timebackAdmin = new TimebackAdminService({
31869
31963
  db: db2,
31870
31964
  timeback: timebackClient,
31871
- validateDeveloperAccess
31965
+ validateDeveloperAccess,
31966
+ validateGameManagementAccess
31872
31967
  });
31873
31968
  return {
31874
31969
  bucket,
@@ -35000,6 +35095,34 @@ function createEduBridgeNamespace(client) {
35000
35095
  listByUser: async (userId) => {
35001
35096
  const response = await client["request"](`/edubridge/enrollments/user/${userId}`, "GET");
35002
35097
  return response.data;
35098
+ },
35099
+ enroll: async (userId, courseId, options) => {
35100
+ const segments = [userId, courseId];
35101
+ if (options?.schoolId) {
35102
+ segments.push(options.schoolId);
35103
+ }
35104
+ const body2 = {};
35105
+ if (options?.role) {
35106
+ body2.role = options.role;
35107
+ }
35108
+ if (options?.sourcedId) {
35109
+ body2.sourcedId = options.sourcedId;
35110
+ }
35111
+ if (options?.beginDate) {
35112
+ body2.beginDate = options.beginDate;
35113
+ }
35114
+ if (options?.metadata) {
35115
+ body2.metadata = options.metadata;
35116
+ }
35117
+ const response = await client["request"](`/edubridge/enrollments/enroll/${segments.join("/")}`, "POST", body2);
35118
+ return response.data;
35119
+ },
35120
+ unenroll: async (userId, courseId, options) => {
35121
+ const segments = [userId, courseId];
35122
+ if (options?.schoolId) {
35123
+ segments.push(options.schoolId);
35124
+ }
35125
+ await client["request"](`/edubridge/enrollments/unenroll/${segments.join("/")}`, "DELETE");
35003
35126
  }
35004
35127
  };
35005
35128
  const analytics = {
@@ -35175,6 +35298,10 @@ function createOneRosterNamespace(client) {
35175
35298
  logTimebackError("list course roster", error, { courseSourcedId });
35176
35299
  throw error;
35177
35300
  }
35301
+ },
35302
+ create: async (data) => client["request"](ONEROSTER_ENDPOINTS4.enrollments, "POST", { enrollment: data }),
35303
+ delete: async (sourcedId) => {
35304
+ await client["request"](`${ONEROSTER_ENDPOINTS4.enrollments}/${sourcedId}`, "DELETE");
35178
35305
  }
35179
35306
  },
35180
35307
  organizations: {
@@ -93507,7 +93634,7 @@ function isValidAdminAttributionDate(value) {
93507
93634
  const date4 = new Date(Date.UTC(year3, month - 1, day, 12, 0, 0));
93508
93635
  return date4.getUTCFullYear() === year3 && date4.getUTCMonth() + 1 === month && date4.getUTCDate() === day;
93509
93636
  }
93510
- var TIMEBACK_GRADES, TIMEBACK_SUBJECTS5, TimebackGradeSchema, TimebackSubjectSchema, UpdateTimebackXpRequestSchema, EndActivityRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ToggleCourseCompletionRequestSchema;
93637
+ var TIMEBACK_GRADES, TIMEBACK_SUBJECTS5, TimebackGradeSchema, TimebackSubjectSchema, UpdateTimebackXpRequestSchema, EndActivityRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ToggleCourseCompletionRequestSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema;
93511
93638
  var init_schemas11 = __esm(() => {
93512
93639
  init_esm();
93513
93640
  TIMEBACK_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
@@ -93650,6 +93777,16 @@ var init_schemas11 = __esm(() => {
93650
93777
  studentId: exports_external.string().min(1),
93651
93778
  action: exports_external.enum(["complete", "resume"])
93652
93779
  });
93780
+ EnrollStudentRequestSchema = exports_external.object({
93781
+ gameId: exports_external.string().uuid(),
93782
+ courseId: exports_external.string().min(1),
93783
+ studentId: exports_external.string().min(1)
93784
+ });
93785
+ UnenrollStudentRequestSchema = exports_external.object({
93786
+ gameId: exports_external.string().uuid(),
93787
+ courseId: exports_external.string().min(1),
93788
+ studentId: exports_external.string().min(1)
93789
+ });
93653
93790
  });
93654
93791
 
93655
93792
  // ../data/src/schemas.index.ts
@@ -93676,6 +93813,9 @@ function isAuthenticated(ctx) {
93676
93813
  var init_types9 = () => {};
93677
93814
 
93678
93815
  // ../api-core/src/utils/auth.util.ts
93816
+ function hasGameManagementAccess(user) {
93817
+ return user.role === "admin" || user.role === "teacher" || user.role === "developer" && user.developerStatus === "approved";
93818
+ }
93679
93819
  function requireAuth(handler) {
93680
93820
  return async (ctx) => {
93681
93821
  if (!isAuthenticated(ctx)) {
@@ -93719,6 +93859,17 @@ function requireDeveloper(handler) {
93719
93859
  return handler(ctx);
93720
93860
  };
93721
93861
  }
93862
+ function requireGameManagementAccess(handler) {
93863
+ return async (ctx) => {
93864
+ if (!isAuthenticated(ctx)) {
93865
+ throw ApiError.unauthorized("Valid session or bearer token required");
93866
+ }
93867
+ if (!hasGameManagementAccess(ctx.user)) {
93868
+ throw ApiError.forbidden("Game management access required");
93869
+ }
93870
+ return handler(ctx);
93871
+ };
93872
+ }
93722
93873
  var init_auth_util = __esm(() => {
93723
93874
  init_errors();
93724
93875
  init_types9();
@@ -95776,7 +95927,7 @@ var init_sprite_controller = __esm(() => {
95776
95927
  });
95777
95928
 
95778
95929
  // ../api-core/src/controllers/timeback.controller.ts
95779
- var logger63, getTodayXp, getTotalXp, updateTodayXp, getXpHistory, populateStudent, getUser, getUserById, setupIntegration, getIntegrations, verifyIntegration, getConfig2, deleteIntegrations, endActivity, getStudentXp, getRoster, getStudentOverview, getStudentActivity, grantXp, adjustTime, adjustMastery, toggleCompletion, timeback2;
95930
+ var logger63, getTodayXp, getTotalXp, updateTodayXp, getXpHistory, populateStudent, getUser, getUserById, setupIntegration, getIntegrations, verifyIntegration, getConfig2, deleteIntegrations, endActivity, getStudentXp, getRoster, getStudentOverview, getStudentActivity, grantXp, adjustTime, adjustMastery, toggleCompletion, searchStudents, enrollStudent, unenrollStudent, timeback2;
95780
95931
  var init_timeback_controller = __esm(() => {
95781
95932
  init_esm();
95782
95933
  init_schemas_index();
@@ -95866,7 +96017,7 @@ var init_timeback_controller = __esm(() => {
95866
96017
  });
95867
96018
  return ctx.services.timeback.setupIntegration(body2.gameId, body2, ctx.user);
95868
96019
  });
95869
- getIntegrations = requireDeveloper(async (ctx) => {
96020
+ getIntegrations = requireGameManagementAccess(async (ctx) => {
95870
96021
  const gameId = ctx.params.gameId;
95871
96022
  if (!gameId) {
95872
96023
  throw ApiError.badRequest("Missing gameId");
@@ -95991,7 +96142,7 @@ var init_timeback_controller = __esm(() => {
95991
96142
  include
95992
96143
  });
95993
96144
  });
95994
- getRoster = requireDeveloper(async (ctx) => {
96145
+ getRoster = requireGameManagementAccess(async (ctx) => {
95995
96146
  const gameId = ctx.params.gameId;
95996
96147
  const courseId = ctx.params.courseId;
95997
96148
  if (!gameId || !courseId) {
@@ -96004,7 +96155,7 @@ var init_timeback_controller = __esm(() => {
96004
96155
  });
96005
96156
  return ctx.services.timebackAdmin.listStudentsForCourse(gameId, courseId, ctx.user);
96006
96157
  });
96007
- getStudentOverview = requireDeveloper(async (ctx) => {
96158
+ getStudentOverview = requireGameManagementAccess(async (ctx) => {
96008
96159
  const timebackId = ctx.params.timebackId;
96009
96160
  const gameId = ctx.url.searchParams.get("gameId") || undefined;
96010
96161
  const courseId = ctx.url.searchParams.get("courseId") || undefined;
@@ -96019,7 +96170,7 @@ var init_timeback_controller = __esm(() => {
96019
96170
  });
96020
96171
  return ctx.services.timebackAdmin.getStudentOverview(gameId, timebackId, ctx.user, courseId);
96021
96172
  });
96022
- getStudentActivity = requireDeveloper(async (ctx) => {
96173
+ getStudentActivity = requireGameManagementAccess(async (ctx) => {
96023
96174
  const timebackId = ctx.params.timebackId;
96024
96175
  const courseId = ctx.params.courseId;
96025
96176
  const gameId = ctx.url.searchParams.get("gameId") || undefined;
@@ -96082,7 +96233,7 @@ var init_timeback_controller = __esm(() => {
96082
96233
  });
96083
96234
  return ctx.services.timebackAdmin.adjustMasteredUnits(body2, ctx.user);
96084
96235
  });
96085
- toggleCompletion = requireDeveloper(async (ctx) => {
96236
+ toggleCompletion = requireGameManagementAccess(async (ctx) => {
96086
96237
  const body2 = await parseRequestBody(ctx.request, ToggleCourseCompletionRequestSchema);
96087
96238
  logger63.debug("Toggling course completion", {
96088
96239
  requesterId: ctx.user.id,
@@ -96093,6 +96244,41 @@ var init_timeback_controller = __esm(() => {
96093
96244
  });
96094
96245
  return ctx.services.timebackAdmin.toggleCourseCompletion(body2, ctx.user);
96095
96246
  });
96247
+ searchStudents = requireGameManagementAccess(async (ctx) => {
96248
+ const gameId = ctx.params.gameId;
96249
+ const courseId = ctx.params.courseId;
96250
+ const query = ctx.url.searchParams.get("q") || "";
96251
+ if (!gameId || !courseId) {
96252
+ throw ApiError.badRequest("Missing gameId or courseId parameter");
96253
+ }
96254
+ logger63.debug("Searching students for enrollment", {
96255
+ requesterId: ctx.user.id,
96256
+ gameId,
96257
+ courseId,
96258
+ query
96259
+ });
96260
+ return ctx.services.timebackAdmin.searchStudentsForEnrollment(gameId, courseId, query, ctx.user);
96261
+ });
96262
+ enrollStudent = requireGameManagementAccess(async (ctx) => {
96263
+ const body2 = await parseRequestBody(ctx.request, EnrollStudentRequestSchema);
96264
+ logger63.debug("Enrolling student", {
96265
+ requesterId: ctx.user.id,
96266
+ gameId: body2.gameId,
96267
+ courseId: body2.courseId,
96268
+ studentId: body2.studentId
96269
+ });
96270
+ return ctx.services.timebackAdmin.enrollStudent(body2, ctx.user);
96271
+ });
96272
+ unenrollStudent = requireGameManagementAccess(async (ctx) => {
96273
+ const body2 = await parseRequestBody(ctx.request, UnenrollStudentRequestSchema);
96274
+ logger63.debug("Unenrolling student", {
96275
+ requesterId: ctx.user.id,
96276
+ gameId: body2.gameId,
96277
+ courseId: body2.courseId,
96278
+ studentId: body2.studentId
96279
+ });
96280
+ return ctx.services.timebackAdmin.unenrollStudent(body2, ctx.user);
96281
+ });
96096
96282
  timeback2 = {
96097
96283
  getTodayXp,
96098
96284
  getTotalXp,
@@ -96114,7 +96300,10 @@ var init_timeback_controller = __esm(() => {
96114
96300
  grantXp,
96115
96301
  adjustTime,
96116
96302
  adjustMastery,
96117
- toggleCompletion
96303
+ toggleCompletion,
96304
+ searchStudents,
96305
+ enrollStudent,
96306
+ unenrollStudent
96118
96307
  };
96119
96308
  });
96120
96309
 
package/dist/server.js CHANGED
@@ -1309,7 +1309,7 @@ var package_default;
1309
1309
  var init_package = __esm(() => {
1310
1310
  package_default = {
1311
1311
  name: "@playcademy/sandbox",
1312
- version: "0.3.17-beta.5",
1312
+ version: "0.3.17-beta.7",
1313
1313
  description: "Local development server for Playcademy game development",
1314
1314
  type: "module",
1315
1315
  exports: {
@@ -11549,7 +11549,7 @@ var init_table6 = __esm(() => {
11549
11549
  init_drizzle_orm();
11550
11550
  init_pg_core();
11551
11551
  init_table5();
11552
- userRoleEnum = pgEnum("user_role", ["admin", "player", "developer"]);
11552
+ userRoleEnum = pgEnum("user_role", ["admin", "player", "developer", "teacher"]);
11553
11553
  developerStatusEnum = pgEnum("developer_status", ["none", "pending", "approved"]);
11554
11554
  users = pgTable("user", {
11555
11555
  id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
@@ -26732,10 +26732,13 @@ var init_game_service = __esm(() => {
26732
26732
  });
26733
26733
  }
26734
26734
  async listManageable(user) {
26735
- this.validateDeveloperStatus(user);
26735
+ const seesAllGames = user.role === "admin" || user.role === "teacher";
26736
+ if (!seesAllGames) {
26737
+ this.validateDeveloperStatus(user);
26738
+ }
26736
26739
  const db2 = this.deps.db;
26737
26740
  return db2.query.games.findMany({
26738
- where: user.role === "admin" ? undefined : eq(games.developerId, user.id),
26741
+ where: seesAllGames ? undefined : eq(games.developerId, user.id),
26739
26742
  orderBy: [desc(games.createdAt)]
26740
26743
  });
26741
26744
  }
@@ -27139,6 +27142,19 @@ var init_game_service = __esm(() => {
27139
27142
  throw new NotFoundError("Game", gameId);
27140
27143
  }
27141
27144
  }
27145
+ async validateGameManagementAccess(user, gameId) {
27146
+ if (user.role === "admin" || user.role === "teacher") {
27147
+ const gameExists = await this.deps.db.query.games.findFirst({
27148
+ where: eq(games.id, gameId),
27149
+ columns: { id: true }
27150
+ });
27151
+ if (!gameExists) {
27152
+ throw new NotFoundError("Game", gameId);
27153
+ }
27154
+ return;
27155
+ }
27156
+ return this.validateDeveloperAccess(user, gameId);
27157
+ }
27142
27158
  async validateDeveloperAccessBySlug(user, slug) {
27143
27159
  this.validateDeveloperStatus(user);
27144
27160
  const db2 = this.deps.db;
@@ -27209,6 +27225,7 @@ function createGameServices(deps) {
27209
27225
  validators: {
27210
27226
  validateDeveloperAccessBySlug: (user, slug) => game.validateDeveloperAccessBySlug(user, slug),
27211
27227
  validateDeveloperAccess: (user, gameId) => game.validateDeveloperAccess(user, gameId),
27228
+ validateGameManagementAccess: (user, gameId) => game.validateGameManagementAccess(user, gameId),
27212
27229
  validateOwnership: (user, gameId) => game.validateOwnership(user, gameId)
27213
27230
  }
27214
27231
  };
@@ -30611,9 +30628,13 @@ class TimebackAdminService {
30611
30628
  });
30612
30629
  });
30613
30630
  }
30614
- async resolveAdminMutationContext(gameId, courseId, user, studentId) {
30631
+ async resolveAdminMutationContext(gameId, courseId, user, studentId, accessLevel = "developer") {
30615
30632
  const client = this.requireClient();
30616
- await this.deps.validateDeveloperAccess(user, gameId);
30633
+ if (accessLevel === "dashboard") {
30634
+ await this.deps.validateGameManagementAccess(user, gameId);
30635
+ } else {
30636
+ await this.deps.validateDeveloperAccess(user, gameId);
30637
+ }
30617
30638
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
30618
30639
  where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
30619
30640
  });
@@ -30873,7 +30894,7 @@ class TimebackAdminService {
30873
30894
  }
30874
30895
  async listStudentsForCourse(gameId, courseId, user) {
30875
30896
  const client = this.requireClient();
30876
- await this.deps.validateDeveloperAccess(user, gameId);
30897
+ await this.deps.validateGameManagementAccess(user, gameId);
30877
30898
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
30878
30899
  where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
30879
30900
  });
@@ -30911,7 +30932,7 @@ class TimebackAdminService {
30911
30932
  }
30912
30933
  async getStudentOverview(gameId, studentId, user, courseId) {
30913
30934
  const client = this.requireClient();
30914
- await this.deps.validateDeveloperAccess(user, gameId);
30935
+ await this.deps.validateGameManagementAccess(user, gameId);
30915
30936
  const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
30916
30937
  where: courseId ? and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId)) : eq(gameTimebackIntegrations.gameId, gameId)
30917
30938
  });
@@ -30965,7 +30986,7 @@ class TimebackAdminService {
30965
30986
  const client = this.requireClient();
30966
30987
  const safeLimit = Math.max(1, Math.min(limit, TimebackAdminService.MAX_STUDENT_ACTIVITY_LIMIT));
30967
30988
  const safeOffset = Math.max(0, Math.min(offset, TimebackAdminService.MAX_STUDENT_ACTIVITY_OFFSET));
30968
- await this.deps.validateDeveloperAccess(user, gameId);
30989
+ await this.deps.validateGameManagementAccess(user, gameId);
30969
30990
  const [integration, sensorUrl] = await Promise.all([
30970
30991
  this.deps.db.query.gameTimebackIntegrations.findFirst({
30971
30992
  where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
@@ -31029,7 +31050,7 @@ class TimebackAdminService {
31029
31050
  return { status: "ok" };
31030
31051
  }
31031
31052
  async toggleCourseCompletion(data, user) {
31032
- const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
31053
+ const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId, "dashboard");
31033
31054
  const historyClient = client;
31034
31055
  const ids = deriveSourcedIds(data.courseId);
31035
31056
  const lineItemId = `${ids.course}-mastery-completion-assessment`;
@@ -31122,6 +31143,77 @@ class TimebackAdminService {
31122
31143
  }
31123
31144
  return { status: "ok" };
31124
31145
  }
31146
+ async searchStudentsForEnrollment(gameId, courseId, query, user) {
31147
+ const client = this.requireClient();
31148
+ await this.deps.validateGameManagementAccess(user, gameId);
31149
+ const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
31150
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
31151
+ });
31152
+ if (!integration) {
31153
+ throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
31154
+ }
31155
+ const trimmedQuery = query.trim();
31156
+ if (trimmedQuery.length < 2) {
31157
+ return { students: [] };
31158
+ }
31159
+ const filterParts = [
31160
+ `givenName~'${escapeFilterValue(trimmedQuery)}'`,
31161
+ `familyName~'${escapeFilterValue(trimmedQuery)}'`,
31162
+ `email~'${escapeFilterValue(trimmedQuery)}'`
31163
+ ];
31164
+ const filter = filterParts.join(" OR ");
31165
+ const params = new URLSearchParams({ filter, limit: "25" });
31166
+ const endpoint = `/ims/oneroster/rostering/v1p2/users?${params}`;
31167
+ let allUsers = [];
31168
+ try {
31169
+ const response = await client["request"](endpoint, "GET");
31170
+ allUsers = response.users || [];
31171
+ } catch (error) {
31172
+ logger16.warn("Failed to search OneRoster users", {
31173
+ query: trimmedQuery,
31174
+ error: error instanceof Error ? error.message : String(error)
31175
+ });
31176
+ return { students: [] };
31177
+ }
31178
+ const roster = await client.oneroster.enrollments.listByCourse(courseId, {
31179
+ role: "student",
31180
+ includeUsers: false
31181
+ });
31182
+ const enrolledStudentIds = new Set(roster.map((entry) => entry.enrollment.user.sourcedId));
31183
+ const students = allUsers.filter((entry) => Boolean(entry.sourcedId) && entry.roles?.some((role) => role.role === "student") === true).map((entry) => ({
31184
+ studentId: entry.sourcedId,
31185
+ name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || entry.sourcedId,
31186
+ email: entry.email || null,
31187
+ alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId)
31188
+ }));
31189
+ return { students };
31190
+ }
31191
+ async enrollStudent(data, user) {
31192
+ const client = this.requireClient();
31193
+ await this.deps.validateGameManagementAccess(user, data.gameId);
31194
+ const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
31195
+ where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
31196
+ });
31197
+ if (!integration) {
31198
+ throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
31199
+ }
31200
+ await client.edubridge.enrollments.enroll(data.studentId, data.courseId, {
31201
+ role: "student"
31202
+ });
31203
+ return { status: "ok" };
31204
+ }
31205
+ async unenrollStudent(data, user) {
31206
+ const client = this.requireClient();
31207
+ await this.deps.validateGameManagementAccess(user, data.gameId);
31208
+ const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
31209
+ where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
31210
+ });
31211
+ if (!integration) {
31212
+ throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
31213
+ }
31214
+ await client.edubridge.enrollments.unenroll(data.studentId, data.courseId);
31215
+ return { status: "ok" };
31216
+ }
31125
31217
  async getCompletionStatus(client, courseId, studentId) {
31126
31218
  const ids = deriveSourcedIds(courseId);
31127
31219
  const lineItemId = `${ids.course}-mastery-completion-assessment`;
@@ -31573,7 +31665,7 @@ class TimebackService {
31573
31665
  return { integrations, ...verbose && verboseData.length > 0 && { verbose: verboseData } };
31574
31666
  }
31575
31667
  async getIntegrations(gameId, user) {
31576
- await this.deps.validateDeveloperAccess(user, gameId);
31668
+ await this.deps.validateGameManagementAccess(user, gameId);
31577
31669
  const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
31578
31670
  where: eq(gameTimebackIntegrations.gameId, gameId)
31579
31671
  });
@@ -31828,6 +31920,7 @@ function createPlatformServices(deps) {
31828
31920
  alerts,
31829
31921
  validateDeveloperAccessBySlug,
31830
31922
  validateDeveloperAccess,
31923
+ validateGameManagementAccess,
31831
31924
  validateOwnership
31832
31925
  } = deps;
31833
31926
  const bucket = new BucketService({
@@ -31862,12 +31955,14 @@ function createPlatformServices(deps) {
31862
31955
  const timeback2 = new TimebackService({
31863
31956
  db: db2,
31864
31957
  timeback: timebackClient,
31865
- validateDeveloperAccess
31958
+ validateDeveloperAccess,
31959
+ validateGameManagementAccess
31866
31960
  });
31867
31961
  const timebackAdmin = new TimebackAdminService({
31868
31962
  db: db2,
31869
31963
  timeback: timebackClient,
31870
- validateDeveloperAccess
31964
+ validateDeveloperAccess,
31965
+ validateGameManagementAccess
31871
31966
  });
31872
31967
  return {
31873
31968
  bucket,
@@ -34999,6 +35094,34 @@ function createEduBridgeNamespace(client) {
34999
35094
  listByUser: async (userId) => {
35000
35095
  const response = await client["request"](`/edubridge/enrollments/user/${userId}`, "GET");
35001
35096
  return response.data;
35097
+ },
35098
+ enroll: async (userId, courseId, options) => {
35099
+ const segments = [userId, courseId];
35100
+ if (options?.schoolId) {
35101
+ segments.push(options.schoolId);
35102
+ }
35103
+ const body2 = {};
35104
+ if (options?.role) {
35105
+ body2.role = options.role;
35106
+ }
35107
+ if (options?.sourcedId) {
35108
+ body2.sourcedId = options.sourcedId;
35109
+ }
35110
+ if (options?.beginDate) {
35111
+ body2.beginDate = options.beginDate;
35112
+ }
35113
+ if (options?.metadata) {
35114
+ body2.metadata = options.metadata;
35115
+ }
35116
+ const response = await client["request"](`/edubridge/enrollments/enroll/${segments.join("/")}`, "POST", body2);
35117
+ return response.data;
35118
+ },
35119
+ unenroll: async (userId, courseId, options) => {
35120
+ const segments = [userId, courseId];
35121
+ if (options?.schoolId) {
35122
+ segments.push(options.schoolId);
35123
+ }
35124
+ await client["request"](`/edubridge/enrollments/unenroll/${segments.join("/")}`, "DELETE");
35002
35125
  }
35003
35126
  };
35004
35127
  const analytics = {
@@ -35174,6 +35297,10 @@ function createOneRosterNamespace(client) {
35174
35297
  logTimebackError("list course roster", error, { courseSourcedId });
35175
35298
  throw error;
35176
35299
  }
35300
+ },
35301
+ create: async (data) => client["request"](ONEROSTER_ENDPOINTS4.enrollments, "POST", { enrollment: data }),
35302
+ delete: async (sourcedId) => {
35303
+ await client["request"](`${ONEROSTER_ENDPOINTS4.enrollments}/${sourcedId}`, "DELETE");
35177
35304
  }
35178
35305
  },
35179
35306
  organizations: {
@@ -93506,7 +93633,7 @@ function isValidAdminAttributionDate(value) {
93506
93633
  const date4 = new Date(Date.UTC(year3, month - 1, day, 12, 0, 0));
93507
93634
  return date4.getUTCFullYear() === year3 && date4.getUTCMonth() + 1 === month && date4.getUTCDate() === day;
93508
93635
  }
93509
- var TIMEBACK_GRADES, TIMEBACK_SUBJECTS5, TimebackGradeSchema, TimebackSubjectSchema, UpdateTimebackXpRequestSchema, EndActivityRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ToggleCourseCompletionRequestSchema;
93636
+ var TIMEBACK_GRADES, TIMEBACK_SUBJECTS5, TimebackGradeSchema, TimebackSubjectSchema, UpdateTimebackXpRequestSchema, EndActivityRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ToggleCourseCompletionRequestSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema;
93510
93637
  var init_schemas11 = __esm(() => {
93511
93638
  init_esm();
93512
93639
  TIMEBACK_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
@@ -93649,6 +93776,16 @@ var init_schemas11 = __esm(() => {
93649
93776
  studentId: exports_external.string().min(1),
93650
93777
  action: exports_external.enum(["complete", "resume"])
93651
93778
  });
93779
+ EnrollStudentRequestSchema = exports_external.object({
93780
+ gameId: exports_external.string().uuid(),
93781
+ courseId: exports_external.string().min(1),
93782
+ studentId: exports_external.string().min(1)
93783
+ });
93784
+ UnenrollStudentRequestSchema = exports_external.object({
93785
+ gameId: exports_external.string().uuid(),
93786
+ courseId: exports_external.string().min(1),
93787
+ studentId: exports_external.string().min(1)
93788
+ });
93652
93789
  });
93653
93790
 
93654
93791
  // ../data/src/schemas.index.ts
@@ -93675,6 +93812,9 @@ function isAuthenticated(ctx) {
93675
93812
  var init_types9 = () => {};
93676
93813
 
93677
93814
  // ../api-core/src/utils/auth.util.ts
93815
+ function hasGameManagementAccess(user) {
93816
+ return user.role === "admin" || user.role === "teacher" || user.role === "developer" && user.developerStatus === "approved";
93817
+ }
93678
93818
  function requireAuth(handler) {
93679
93819
  return async (ctx) => {
93680
93820
  if (!isAuthenticated(ctx)) {
@@ -93718,6 +93858,17 @@ function requireDeveloper(handler) {
93718
93858
  return handler(ctx);
93719
93859
  };
93720
93860
  }
93861
+ function requireGameManagementAccess(handler) {
93862
+ return async (ctx) => {
93863
+ if (!isAuthenticated(ctx)) {
93864
+ throw ApiError.unauthorized("Valid session or bearer token required");
93865
+ }
93866
+ if (!hasGameManagementAccess(ctx.user)) {
93867
+ throw ApiError.forbidden("Game management access required");
93868
+ }
93869
+ return handler(ctx);
93870
+ };
93871
+ }
93721
93872
  var init_auth_util = __esm(() => {
93722
93873
  init_errors();
93723
93874
  init_types9();
@@ -95775,7 +95926,7 @@ var init_sprite_controller = __esm(() => {
95775
95926
  });
95776
95927
 
95777
95928
  // ../api-core/src/controllers/timeback.controller.ts
95778
- var logger63, getTodayXp, getTotalXp, updateTodayXp, getXpHistory, populateStudent, getUser, getUserById, setupIntegration, getIntegrations, verifyIntegration, getConfig2, deleteIntegrations, endActivity, getStudentXp, getRoster, getStudentOverview, getStudentActivity, grantXp, adjustTime, adjustMastery, toggleCompletion, timeback2;
95929
+ var logger63, getTodayXp, getTotalXp, updateTodayXp, getXpHistory, populateStudent, getUser, getUserById, setupIntegration, getIntegrations, verifyIntegration, getConfig2, deleteIntegrations, endActivity, getStudentXp, getRoster, getStudentOverview, getStudentActivity, grantXp, adjustTime, adjustMastery, toggleCompletion, searchStudents, enrollStudent, unenrollStudent, timeback2;
95779
95930
  var init_timeback_controller = __esm(() => {
95780
95931
  init_esm();
95781
95932
  init_schemas_index();
@@ -95865,7 +96016,7 @@ var init_timeback_controller = __esm(() => {
95865
96016
  });
95866
96017
  return ctx.services.timeback.setupIntegration(body2.gameId, body2, ctx.user);
95867
96018
  });
95868
- getIntegrations = requireDeveloper(async (ctx) => {
96019
+ getIntegrations = requireGameManagementAccess(async (ctx) => {
95869
96020
  const gameId = ctx.params.gameId;
95870
96021
  if (!gameId) {
95871
96022
  throw ApiError.badRequest("Missing gameId");
@@ -95990,7 +96141,7 @@ var init_timeback_controller = __esm(() => {
95990
96141
  include
95991
96142
  });
95992
96143
  });
95993
- getRoster = requireDeveloper(async (ctx) => {
96144
+ getRoster = requireGameManagementAccess(async (ctx) => {
95994
96145
  const gameId = ctx.params.gameId;
95995
96146
  const courseId = ctx.params.courseId;
95996
96147
  if (!gameId || !courseId) {
@@ -96003,7 +96154,7 @@ var init_timeback_controller = __esm(() => {
96003
96154
  });
96004
96155
  return ctx.services.timebackAdmin.listStudentsForCourse(gameId, courseId, ctx.user);
96005
96156
  });
96006
- getStudentOverview = requireDeveloper(async (ctx) => {
96157
+ getStudentOverview = requireGameManagementAccess(async (ctx) => {
96007
96158
  const timebackId = ctx.params.timebackId;
96008
96159
  const gameId = ctx.url.searchParams.get("gameId") || undefined;
96009
96160
  const courseId = ctx.url.searchParams.get("courseId") || undefined;
@@ -96018,7 +96169,7 @@ var init_timeback_controller = __esm(() => {
96018
96169
  });
96019
96170
  return ctx.services.timebackAdmin.getStudentOverview(gameId, timebackId, ctx.user, courseId);
96020
96171
  });
96021
- getStudentActivity = requireDeveloper(async (ctx) => {
96172
+ getStudentActivity = requireGameManagementAccess(async (ctx) => {
96022
96173
  const timebackId = ctx.params.timebackId;
96023
96174
  const courseId = ctx.params.courseId;
96024
96175
  const gameId = ctx.url.searchParams.get("gameId") || undefined;
@@ -96081,7 +96232,7 @@ var init_timeback_controller = __esm(() => {
96081
96232
  });
96082
96233
  return ctx.services.timebackAdmin.adjustMasteredUnits(body2, ctx.user);
96083
96234
  });
96084
- toggleCompletion = requireDeveloper(async (ctx) => {
96235
+ toggleCompletion = requireGameManagementAccess(async (ctx) => {
96085
96236
  const body2 = await parseRequestBody(ctx.request, ToggleCourseCompletionRequestSchema);
96086
96237
  logger63.debug("Toggling course completion", {
96087
96238
  requesterId: ctx.user.id,
@@ -96092,6 +96243,41 @@ var init_timeback_controller = __esm(() => {
96092
96243
  });
96093
96244
  return ctx.services.timebackAdmin.toggleCourseCompletion(body2, ctx.user);
96094
96245
  });
96246
+ searchStudents = requireGameManagementAccess(async (ctx) => {
96247
+ const gameId = ctx.params.gameId;
96248
+ const courseId = ctx.params.courseId;
96249
+ const query = ctx.url.searchParams.get("q") || "";
96250
+ if (!gameId || !courseId) {
96251
+ throw ApiError.badRequest("Missing gameId or courseId parameter");
96252
+ }
96253
+ logger63.debug("Searching students for enrollment", {
96254
+ requesterId: ctx.user.id,
96255
+ gameId,
96256
+ courseId,
96257
+ query
96258
+ });
96259
+ return ctx.services.timebackAdmin.searchStudentsForEnrollment(gameId, courseId, query, ctx.user);
96260
+ });
96261
+ enrollStudent = requireGameManagementAccess(async (ctx) => {
96262
+ const body2 = await parseRequestBody(ctx.request, EnrollStudentRequestSchema);
96263
+ logger63.debug("Enrolling student", {
96264
+ requesterId: ctx.user.id,
96265
+ gameId: body2.gameId,
96266
+ courseId: body2.courseId,
96267
+ studentId: body2.studentId
96268
+ });
96269
+ return ctx.services.timebackAdmin.enrollStudent(body2, ctx.user);
96270
+ });
96271
+ unenrollStudent = requireGameManagementAccess(async (ctx) => {
96272
+ const body2 = await parseRequestBody(ctx.request, UnenrollStudentRequestSchema);
96273
+ logger63.debug("Unenrolling student", {
96274
+ requesterId: ctx.user.id,
96275
+ gameId: body2.gameId,
96276
+ courseId: body2.courseId,
96277
+ studentId: body2.studentId
96278
+ });
96279
+ return ctx.services.timebackAdmin.unenrollStudent(body2, ctx.user);
96280
+ });
96095
96281
  timeback2 = {
96096
96282
  getTodayXp,
96097
96283
  getTotalXp,
@@ -96113,7 +96299,10 @@ var init_timeback_controller = __esm(() => {
96113
96299
  grantXp,
96114
96300
  adjustTime,
96115
96301
  adjustMastery,
96116
- toggleCompletion
96302
+ toggleCompletion,
96303
+ searchStudents,
96304
+ enrollStudent,
96305
+ unenrollStudent
96117
96306
  };
96118
96307
  });
96119
96308
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.3.17-beta.5",
3
+ "version": "0.3.17-beta.7",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {