@playcademy/vite-plugin 0.2.24-beta.3 → 0.2.24-beta.4

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 (2) hide show
  1. package/dist/index.js +214 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -25335,7 +25335,7 @@ var package_default;
25335
25335
  var init_package = __esm(() => {
25336
25336
  package_default = {
25337
25337
  name: "@playcademy/sandbox",
25338
- version: "0.3.17-beta.6",
25338
+ version: "0.3.17-beta.7",
25339
25339
  description: "Local development server for Playcademy game development",
25340
25340
  type: "module",
25341
25341
  exports: {
@@ -35611,7 +35611,7 @@ var init_table6 = __esm(() => {
35611
35611
  init_drizzle_orm();
35612
35612
  init_pg_core();
35613
35613
  init_table5();
35614
- userRoleEnum = pgEnum("user_role", ["admin", "player", "developer"]);
35614
+ userRoleEnum = pgEnum("user_role", ["admin", "player", "developer", "teacher"]);
35615
35615
  developerStatusEnum = pgEnum("developer_status", ["none", "pending", "approved"]);
35616
35616
  users = pgTable("user", {
35617
35617
  id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
@@ -50583,10 +50583,13 @@ var init_game_service = __esm(() => {
50583
50583
  });
50584
50584
  }
50585
50585
  async listManageable(user) {
50586
- this.validateDeveloperStatus(user);
50586
+ const seesAllGames = user.role === "admin" || user.role === "teacher";
50587
+ if (!seesAllGames) {
50588
+ this.validateDeveloperStatus(user);
50589
+ }
50587
50590
  const db2 = this.deps.db;
50588
50591
  return db2.query.games.findMany({
50589
- where: user.role === "admin" ? undefined : eq(games.developerId, user.id),
50592
+ where: seesAllGames ? undefined : eq(games.developerId, user.id),
50590
50593
  orderBy: [desc(games.createdAt)]
50591
50594
  });
50592
50595
  }
@@ -50990,6 +50993,19 @@ var init_game_service = __esm(() => {
50990
50993
  throw new NotFoundError("Game", gameId);
50991
50994
  }
50992
50995
  }
50996
+ async validateGameManagementAccess(user, gameId) {
50997
+ if (user.role === "admin" || user.role === "teacher") {
50998
+ const gameExists = await this.deps.db.query.games.findFirst({
50999
+ where: eq(games.id, gameId),
51000
+ columns: { id: true }
51001
+ });
51002
+ if (!gameExists) {
51003
+ throw new NotFoundError("Game", gameId);
51004
+ }
51005
+ return;
51006
+ }
51007
+ return this.validateDeveloperAccess(user, gameId);
51008
+ }
50993
51009
  async validateDeveloperAccessBySlug(user, slug) {
50994
51010
  this.validateDeveloperStatus(user);
50995
51011
  const db2 = this.deps.db;
@@ -51058,6 +51074,7 @@ function createGameServices(deps) {
51058
51074
  validators: {
51059
51075
  validateDeveloperAccessBySlug: (user, slug) => game.validateDeveloperAccessBySlug(user, slug),
51060
51076
  validateDeveloperAccess: (user, gameId) => game.validateDeveloperAccess(user, gameId),
51077
+ validateGameManagementAccess: (user, gameId) => game.validateGameManagementAccess(user, gameId),
51061
51078
  validateOwnership: (user, gameId) => game.validateOwnership(user, gameId)
51062
51079
  }
51063
51080
  };
@@ -54499,9 +54516,13 @@ class TimebackAdminService {
54499
54516
  });
54500
54517
  });
54501
54518
  }
54502
- async resolveAdminMutationContext(gameId, courseId, user, studentId) {
54519
+ async resolveAdminMutationContext(gameId, courseId, user, studentId, accessLevel = "developer") {
54503
54520
  const client = this.requireClient();
54504
- await this.deps.validateDeveloperAccess(user, gameId);
54521
+ if (accessLevel === "dashboard") {
54522
+ await this.deps.validateGameManagementAccess(user, gameId);
54523
+ } else {
54524
+ await this.deps.validateDeveloperAccess(user, gameId);
54525
+ }
54505
54526
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
54506
54527
  where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
54507
54528
  });
@@ -54761,7 +54782,7 @@ class TimebackAdminService {
54761
54782
  }
54762
54783
  async listStudentsForCourse(gameId, courseId, user) {
54763
54784
  const client = this.requireClient();
54764
- await this.deps.validateDeveloperAccess(user, gameId);
54785
+ await this.deps.validateGameManagementAccess(user, gameId);
54765
54786
  const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
54766
54787
  where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
54767
54788
  });
@@ -54799,7 +54820,7 @@ class TimebackAdminService {
54799
54820
  }
54800
54821
  async getStudentOverview(gameId, studentId, user, courseId) {
54801
54822
  const client = this.requireClient();
54802
- await this.deps.validateDeveloperAccess(user, gameId);
54823
+ await this.deps.validateGameManagementAccess(user, gameId);
54803
54824
  const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
54804
54825
  where: courseId ? and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId)) : eq(gameTimebackIntegrations.gameId, gameId)
54805
54826
  });
@@ -54853,7 +54874,7 @@ class TimebackAdminService {
54853
54874
  const client = this.requireClient();
54854
54875
  const safeLimit = Math.max(1, Math.min(limit, TimebackAdminService.MAX_STUDENT_ACTIVITY_LIMIT));
54855
54876
  const safeOffset = Math.max(0, Math.min(offset, TimebackAdminService.MAX_STUDENT_ACTIVITY_OFFSET));
54856
- await this.deps.validateDeveloperAccess(user, gameId);
54877
+ await this.deps.validateGameManagementAccess(user, gameId);
54857
54878
  const [integration, sensorUrl] = await Promise.all([
54858
54879
  this.deps.db.query.gameTimebackIntegrations.findFirst({
54859
54880
  where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
@@ -54917,7 +54938,7 @@ class TimebackAdminService {
54917
54938
  return { status: "ok" };
54918
54939
  }
54919
54940
  async toggleCourseCompletion(data, user) {
54920
- const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
54941
+ const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId, "dashboard");
54921
54942
  const historyClient = client;
54922
54943
  const ids = deriveSourcedIds(data.courseId);
54923
54944
  const lineItemId = `${ids.course}-mastery-completion-assessment`;
@@ -55010,6 +55031,77 @@ class TimebackAdminService {
55010
55031
  }
55011
55032
  return { status: "ok" };
55012
55033
  }
55034
+ async searchStudentsForEnrollment(gameId, courseId, query, user) {
55035
+ const client = this.requireClient();
55036
+ await this.deps.validateGameManagementAccess(user, gameId);
55037
+ const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
55038
+ where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
55039
+ });
55040
+ if (!integration) {
55041
+ throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
55042
+ }
55043
+ const trimmedQuery = query.trim();
55044
+ if (trimmedQuery.length < 2) {
55045
+ return { students: [] };
55046
+ }
55047
+ const filterParts = [
55048
+ `givenName~'${escapeFilterValue(trimmedQuery)}'`,
55049
+ `familyName~'${escapeFilterValue(trimmedQuery)}'`,
55050
+ `email~'${escapeFilterValue(trimmedQuery)}'`
55051
+ ];
55052
+ const filter = filterParts.join(" OR ");
55053
+ const params = new URLSearchParams({ filter, limit: "25" });
55054
+ const endpoint = `/ims/oneroster/rostering/v1p2/users?${params}`;
55055
+ let allUsers = [];
55056
+ try {
55057
+ const response = await client["request"](endpoint, "GET");
55058
+ allUsers = response.users || [];
55059
+ } catch (error) {
55060
+ logger16.warn("Failed to search OneRoster users", {
55061
+ query: trimmedQuery,
55062
+ error: error instanceof Error ? error.message : String(error)
55063
+ });
55064
+ return { students: [] };
55065
+ }
55066
+ const roster = await client.oneroster.enrollments.listByCourse(courseId, {
55067
+ role: "student",
55068
+ includeUsers: false
55069
+ });
55070
+ const enrolledStudentIds = new Set(roster.map((entry) => entry.enrollment.user.sourcedId));
55071
+ const students = allUsers.filter((entry) => Boolean(entry.sourcedId) && entry.roles?.some((role) => role.role === "student") === true).map((entry) => ({
55072
+ studentId: entry.sourcedId,
55073
+ name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || entry.sourcedId,
55074
+ email: entry.email || null,
55075
+ alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId)
55076
+ }));
55077
+ return { students };
55078
+ }
55079
+ async enrollStudent(data, user) {
55080
+ const client = this.requireClient();
55081
+ await this.deps.validateGameManagementAccess(user, data.gameId);
55082
+ const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
55083
+ where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
55084
+ });
55085
+ if (!integration) {
55086
+ throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
55087
+ }
55088
+ await client.edubridge.enrollments.enroll(data.studentId, data.courseId, {
55089
+ role: "student"
55090
+ });
55091
+ return { status: "ok" };
55092
+ }
55093
+ async unenrollStudent(data, user) {
55094
+ const client = this.requireClient();
55095
+ await this.deps.validateGameManagementAccess(user, data.gameId);
55096
+ const integration = await this.deps.db.query.gameTimebackIntegrations.findFirst({
55097
+ where: and(eq(gameTimebackIntegrations.gameId, data.gameId), eq(gameTimebackIntegrations.courseId, data.courseId))
55098
+ });
55099
+ if (!integration) {
55100
+ throw new NotFoundError("Timeback integration", `${data.gameId}:${data.courseId}`);
55101
+ }
55102
+ await client.edubridge.enrollments.unenroll(data.studentId, data.courseId);
55103
+ return { status: "ok" };
55104
+ }
55013
55105
  async getCompletionStatus(client, courseId, studentId) {
55014
55106
  const ids = deriveSourcedIds(courseId);
55015
55107
  const lineItemId = `${ids.course}-mastery-completion-assessment`;
@@ -55460,7 +55552,7 @@ class TimebackService {
55460
55552
  return { integrations, ...verbose && verboseData.length > 0 && { verbose: verboseData } };
55461
55553
  }
55462
55554
  async getIntegrations(gameId, user) {
55463
- await this.deps.validateDeveloperAccess(user, gameId);
55555
+ await this.deps.validateGameManagementAccess(user, gameId);
55464
55556
  const rows = await this.deps.db.query.gameTimebackIntegrations.findMany({
55465
55557
  where: eq(gameTimebackIntegrations.gameId, gameId)
55466
55558
  });
@@ -55712,6 +55804,7 @@ function createPlatformServices(deps) {
55712
55804
  alerts,
55713
55805
  validateDeveloperAccessBySlug,
55714
55806
  validateDeveloperAccess,
55807
+ validateGameManagementAccess,
55715
55808
  validateOwnership
55716
55809
  } = deps;
55717
55810
  const bucket = new BucketService({
@@ -55746,12 +55839,14 @@ function createPlatformServices(deps) {
55746
55839
  const timeback2 = new TimebackService({
55747
55840
  db: db2,
55748
55841
  timeback: timebackClient,
55749
- validateDeveloperAccess
55842
+ validateDeveloperAccess,
55843
+ validateGameManagementAccess
55750
55844
  });
55751
55845
  const timebackAdmin = new TimebackAdminService({
55752
55846
  db: db2,
55753
55847
  timeback: timebackClient,
55754
- validateDeveloperAccess
55848
+ validateDeveloperAccess,
55849
+ validateGameManagementAccess
55755
55850
  });
55756
55851
  return {
55757
55852
  bucket,
@@ -58845,6 +58940,34 @@ function createEduBridgeNamespace(client) {
58845
58940
  listByUser: async (userId) => {
58846
58941
  const response = await client["request"](`/edubridge/enrollments/user/${userId}`, "GET");
58847
58942
  return response.data;
58943
+ },
58944
+ enroll: async (userId, courseId, options) => {
58945
+ const segments = [userId, courseId];
58946
+ if (options?.schoolId) {
58947
+ segments.push(options.schoolId);
58948
+ }
58949
+ const body2 = {};
58950
+ if (options?.role) {
58951
+ body2.role = options.role;
58952
+ }
58953
+ if (options?.sourcedId) {
58954
+ body2.sourcedId = options.sourcedId;
58955
+ }
58956
+ if (options?.beginDate) {
58957
+ body2.beginDate = options.beginDate;
58958
+ }
58959
+ if (options?.metadata) {
58960
+ body2.metadata = options.metadata;
58961
+ }
58962
+ const response = await client["request"](`/edubridge/enrollments/enroll/${segments.join("/")}`, "POST", body2);
58963
+ return response.data;
58964
+ },
58965
+ unenroll: async (userId, courseId, options) => {
58966
+ const segments = [userId, courseId];
58967
+ if (options?.schoolId) {
58968
+ segments.push(options.schoolId);
58969
+ }
58970
+ await client["request"](`/edubridge/enrollments/unenroll/${segments.join("/")}`, "DELETE");
58848
58971
  }
58849
58972
  };
58850
58973
  const analytics = {
@@ -59020,6 +59143,10 @@ function createOneRosterNamespace(client) {
59020
59143
  logTimebackError("list course roster", error, { courseSourcedId });
59021
59144
  throw error;
59022
59145
  }
59146
+ },
59147
+ create: async (data) => client["request"](ONEROSTER_ENDPOINTS4.enrollments, "POST", { enrollment: data }),
59148
+ delete: async (sourcedId) => {
59149
+ await client["request"](`${ONEROSTER_ENDPOINTS4.enrollments}/${sourcedId}`, "DELETE");
59023
59150
  }
59024
59151
  },
59025
59152
  organizations: {
@@ -120050,6 +120177,8 @@ var GrantTimebackXpRequestSchema;
120050
120177
  var AdjustTimebackTimeRequestSchema;
120051
120178
  var AdjustTimebackMasteryRequestSchema;
120052
120179
  var ToggleCourseCompletionRequestSchema;
120180
+ var EnrollStudentRequestSchema;
120181
+ var UnenrollStudentRequestSchema;
120053
120182
  var init_schemas11 = __esm(() => {
120054
120183
  init_esm();
120055
120184
  TIMEBACK_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
@@ -120192,6 +120321,16 @@ var init_schemas11 = __esm(() => {
120192
120321
  studentId: exports_external.string().min(1),
120193
120322
  action: exports_external.enum(["complete", "resume"])
120194
120323
  });
120324
+ EnrollStudentRequestSchema = exports_external.object({
120325
+ gameId: exports_external.string().uuid(),
120326
+ courseId: exports_external.string().min(1),
120327
+ studentId: exports_external.string().min(1)
120328
+ });
120329
+ UnenrollStudentRequestSchema = exports_external.object({
120330
+ gameId: exports_external.string().uuid(),
120331
+ courseId: exports_external.string().min(1),
120332
+ studentId: exports_external.string().min(1)
120333
+ });
120195
120334
  });
120196
120335
  var init_schemas_index = __esm(() => {
120197
120336
  init_schemas();
@@ -120210,6 +120349,9 @@ function isAuthenticated(ctx) {
120210
120349
  return ctx.user != null;
120211
120350
  }
120212
120351
  var init_types9 = () => {};
120352
+ function hasGameManagementAccess(user) {
120353
+ return user.role === "admin" || user.role === "teacher" || user.role === "developer" && user.developerStatus === "approved";
120354
+ }
120213
120355
  function requireAuth(handler) {
120214
120356
  return async (ctx) => {
120215
120357
  if (!isAuthenticated(ctx)) {
@@ -120253,6 +120395,17 @@ function requireDeveloper(handler) {
120253
120395
  return handler(ctx);
120254
120396
  };
120255
120397
  }
120398
+ function requireGameManagementAccess(handler) {
120399
+ return async (ctx) => {
120400
+ if (!isAuthenticated(ctx)) {
120401
+ throw ApiError.unauthorized("Valid session or bearer token required");
120402
+ }
120403
+ if (!hasGameManagementAccess(ctx.user)) {
120404
+ throw ApiError.forbidden("Game management access required");
120405
+ }
120406
+ return handler(ctx);
120407
+ };
120408
+ }
120256
120409
  var init_auth_util = __esm(() => {
120257
120410
  init_errors();
120258
120411
  init_types9();
@@ -122386,6 +122539,9 @@ var grantXp;
122386
122539
  var adjustTime;
122387
122540
  var adjustMastery;
122388
122541
  var toggleCompletion;
122542
+ var searchStudents;
122543
+ var enrollStudent;
122544
+ var unenrollStudent;
122389
122545
  var timeback2;
122390
122546
  var init_timeback_controller = __esm(() => {
122391
122547
  init_esm();
@@ -122476,7 +122632,7 @@ var init_timeback_controller = __esm(() => {
122476
122632
  });
122477
122633
  return ctx.services.timeback.setupIntegration(body2.gameId, body2, ctx.user);
122478
122634
  });
122479
- getIntegrations = requireDeveloper(async (ctx) => {
122635
+ getIntegrations = requireGameManagementAccess(async (ctx) => {
122480
122636
  const gameId = ctx.params.gameId;
122481
122637
  if (!gameId) {
122482
122638
  throw ApiError.badRequest("Missing gameId");
@@ -122601,7 +122757,7 @@ var init_timeback_controller = __esm(() => {
122601
122757
  include
122602
122758
  });
122603
122759
  });
122604
- getRoster = requireDeveloper(async (ctx) => {
122760
+ getRoster = requireGameManagementAccess(async (ctx) => {
122605
122761
  const gameId = ctx.params.gameId;
122606
122762
  const courseId = ctx.params.courseId;
122607
122763
  if (!gameId || !courseId) {
@@ -122614,7 +122770,7 @@ var init_timeback_controller = __esm(() => {
122614
122770
  });
122615
122771
  return ctx.services.timebackAdmin.listStudentsForCourse(gameId, courseId, ctx.user);
122616
122772
  });
122617
- getStudentOverview = requireDeveloper(async (ctx) => {
122773
+ getStudentOverview = requireGameManagementAccess(async (ctx) => {
122618
122774
  const timebackId = ctx.params.timebackId;
122619
122775
  const gameId = ctx.url.searchParams.get("gameId") || undefined;
122620
122776
  const courseId = ctx.url.searchParams.get("courseId") || undefined;
@@ -122629,7 +122785,7 @@ var init_timeback_controller = __esm(() => {
122629
122785
  });
122630
122786
  return ctx.services.timebackAdmin.getStudentOverview(gameId, timebackId, ctx.user, courseId);
122631
122787
  });
122632
- getStudentActivity = requireDeveloper(async (ctx) => {
122788
+ getStudentActivity = requireGameManagementAccess(async (ctx) => {
122633
122789
  const timebackId = ctx.params.timebackId;
122634
122790
  const courseId = ctx.params.courseId;
122635
122791
  const gameId = ctx.url.searchParams.get("gameId") || undefined;
@@ -122692,7 +122848,7 @@ var init_timeback_controller = __esm(() => {
122692
122848
  });
122693
122849
  return ctx.services.timebackAdmin.adjustMasteredUnits(body2, ctx.user);
122694
122850
  });
122695
- toggleCompletion = requireDeveloper(async (ctx) => {
122851
+ toggleCompletion = requireGameManagementAccess(async (ctx) => {
122696
122852
  const body2 = await parseRequestBody(ctx.request, ToggleCourseCompletionRequestSchema);
122697
122853
  logger63.debug("Toggling course completion", {
122698
122854
  requesterId: ctx.user.id,
@@ -122703,6 +122859,41 @@ var init_timeback_controller = __esm(() => {
122703
122859
  });
122704
122860
  return ctx.services.timebackAdmin.toggleCourseCompletion(body2, ctx.user);
122705
122861
  });
122862
+ searchStudents = requireGameManagementAccess(async (ctx) => {
122863
+ const gameId = ctx.params.gameId;
122864
+ const courseId = ctx.params.courseId;
122865
+ const query = ctx.url.searchParams.get("q") || "";
122866
+ if (!gameId || !courseId) {
122867
+ throw ApiError.badRequest("Missing gameId or courseId parameter");
122868
+ }
122869
+ logger63.debug("Searching students for enrollment", {
122870
+ requesterId: ctx.user.id,
122871
+ gameId,
122872
+ courseId,
122873
+ query
122874
+ });
122875
+ return ctx.services.timebackAdmin.searchStudentsForEnrollment(gameId, courseId, query, ctx.user);
122876
+ });
122877
+ enrollStudent = requireGameManagementAccess(async (ctx) => {
122878
+ const body2 = await parseRequestBody(ctx.request, EnrollStudentRequestSchema);
122879
+ logger63.debug("Enrolling student", {
122880
+ requesterId: ctx.user.id,
122881
+ gameId: body2.gameId,
122882
+ courseId: body2.courseId,
122883
+ studentId: body2.studentId
122884
+ });
122885
+ return ctx.services.timebackAdmin.enrollStudent(body2, ctx.user);
122886
+ });
122887
+ unenrollStudent = requireGameManagementAccess(async (ctx) => {
122888
+ const body2 = await parseRequestBody(ctx.request, UnenrollStudentRequestSchema);
122889
+ logger63.debug("Unenrolling student", {
122890
+ requesterId: ctx.user.id,
122891
+ gameId: body2.gameId,
122892
+ courseId: body2.courseId,
122893
+ studentId: body2.studentId
122894
+ });
122895
+ return ctx.services.timebackAdmin.unenrollStudent(body2, ctx.user);
122896
+ });
122706
122897
  timeback2 = {
122707
122898
  getTodayXp,
122708
122899
  getTotalXp,
@@ -122724,7 +122915,10 @@ var init_timeback_controller = __esm(() => {
122724
122915
  grantXp,
122725
122916
  adjustTime,
122726
122917
  adjustMastery,
122727
- toggleCompletion
122918
+ toggleCompletion,
122919
+ searchStudents,
122920
+ enrollStudent,
122921
+ unenrollStudent
122728
122922
  };
122729
122923
  });
122730
122924
  var logger64;
@@ -125286,7 +125480,7 @@ var import_picocolors12 = __toESM(require_picocolors(), 1);
125286
125480
  // package.json
125287
125481
  var package_default2 = {
125288
125482
  name: "@playcademy/vite-plugin",
125289
- version: "0.2.24-beta.3",
125483
+ version: "0.2.24-beta.4",
125290
125484
  type: "module",
125291
125485
  exports: {
125292
125486
  ".": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/vite-plugin",
3
- "version": "0.2.24-beta.3",
3
+ "version": "0.2.24-beta.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {