@learnpack/learnpack 5.0.344 → 5.0.347

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/src/ui/app.tar.gz CHANGED
Binary file
package/src/utils/api.ts CHANGED
@@ -353,20 +353,15 @@ export interface TAcademy {
353
353
  timezone: string;
354
354
  }
355
355
 
356
- const neededPermissions = [
357
- "add_asset",
358
- "change_asset",
359
- "view_asset",
360
- "delete_asset",
361
- ]
356
+ const CRUD_ASSET_CAPABILITY_URL = `${HOST}/v1/auth/user/me/capability/crud_asset`
362
357
 
363
358
  export const listUserAcademies = async (
364
359
  breathecodeToken: string
365
360
  ): Promise<TAcademy[]> => {
366
- const url = "https://breathecode.herokuapp.com/v1/auth/user/me"
361
+ const meUrl = `${HOST}/v1/auth/user/me`
367
362
 
368
363
  try {
369
- const response = await axios.get(url, {
364
+ const response = await axios.get(meUrl, {
370
365
  headers: {
371
366
  Authorization: `Token ${breathecodeToken}`,
372
367
  },
@@ -376,27 +371,47 @@ export const listUserAcademies = async (
376
371
 
377
372
  const academiesMap = new Map<number, TAcademy>()
378
373
 
374
+ if (!Array.isArray(data.roles)) {
375
+ return []
376
+ }
377
+
379
378
  for (const role of data.roles) {
380
379
  const academy = role.academy
380
+ if (!academy) continue
381
381
  if (!academiesMap.has(academy.id)) {
382
382
  academiesMap.set(academy.id, academy)
383
383
  }
384
384
  }
385
385
 
386
- const permissions = new Set(data.permissions.map((p: any) => p.codename))
386
+ const academies = [...academiesMap.values()]
387
387
 
388
- // Validate if the user has ALL the needed permissions
389
- const hasAllPermissions = neededPermissions.every(permission =>
390
- permissions.has(permission)
391
- )
388
+ const allowed = await Promise.all(
389
+ academies.map(async academy => {
390
+ const capResponse = await axios.get(CRUD_ASSET_CAPABILITY_URL, {
391
+ headers: {
392
+ Authorization: `Token ${breathecodeToken}`,
393
+ Academy: academy.id,
394
+ },
395
+ validateStatus: () => true,
396
+ })
397
+
398
+ if (capResponse.status === 200) {
399
+ return academy
400
+ }
392
401
 
393
- if (!hasAllPermissions) {
394
- // The user does not have all the needed permissions
402
+ if (capResponse.status !== 403) {
403
+ console.warn(
404
+ `listUserAcademies: unexpected status ${capResponse.status} for academy ${academy.id}`
405
+ )
406
+ }
395
407
 
396
- return []
397
- }
408
+ return null
409
+ })
410
+ )
398
411
 
399
- return [...academiesMap.values()]
412
+ const filtered = allowed.filter((a): a is TAcademy => a !== null)
413
+ filtered.sort((a, b) => a.name.localeCompare(b.name))
414
+ return filtered
400
415
  } catch (error) {
401
416
  console.error("Failed to fetch user academies:", error)
402
417
  return []
@@ -417,6 +432,42 @@ export const validateToken = async (token: string) => {
417
432
  }
418
433
  }
419
434
 
435
+ /** keep in sync with ide/src/components/Creator/PublishButton.tsx AssetSyncError */
436
+ export type AssetSyncError =
437
+ | { kind: "lang_error"; lang: string; error: { detail: string } }
438
+ | { kind: "package_error"; error: { detail: string } };
439
+
440
+ function parseLearnpackPackageId(raw: unknown): number | null {
441
+ if (raw === undefined || raw === null) return null
442
+ const n = typeof raw === "number" ? raw : Number(raw)
443
+ if (!Number.isFinite(n) || !Number.isInteger(n)) return null
444
+ return n
445
+ }
446
+
447
+ /**
448
+ * GET Rigobot package by slug after a successful upload. Does not throw.
449
+ * @param rigoToken Rigobot API token (Bearer-style `Token` header value).
450
+ * @param courseSlug Learnpack package slug used in the Rigobot URL path.
451
+ * @returns Resolved numeric package id from Rigobot, or `null` if the request fails, the id is missing, or it is not a finite integer.
452
+ */
453
+ export async function resolveLearnpackPackageId(
454
+ rigoToken: string,
455
+ courseSlug: string
456
+ ): Promise<number | null> {
457
+ if (!rigoToken?.trim() || !courseSlug) return null
458
+ const url = `${RIGOBOT_HOST}/v1/learnpack/package/${encodeURIComponent(
459
+ courseSlug
460
+ )}/`
461
+ try {
462
+ const response = await axios.get<{ id?: unknown }>(url, {
463
+ headers: { Authorization: `Token ${rigoToken.trim()}` },
464
+ })
465
+ return parseLearnpackPackageId(response.data?.id)
466
+ } catch {
467
+ return null
468
+ }
469
+ }
470
+
420
471
  type TAssetMissing = {
421
472
  slug: string;
422
473
  title: string;
@@ -433,6 +484,7 @@ type TAssetMissing = {
433
484
  readme_raw: string;
434
485
  all_translations: string[];
435
486
  academy_id?: number;
487
+ learnpack_id: number;
436
488
  };
437
489
 
438
490
  export const createAsset = async (token: string, asset: TAssetMissing) => {
@@ -460,6 +512,7 @@ export const createAsset = async (token: string, asset: TAssetMissing) => {
460
512
  intro_video_url: null,
461
513
  translations: [asset.lang],
462
514
  learnpack_deploy_url: asset.learnpack_deploy_url,
515
+ learnpack_id: asset.learnpack_id,
463
516
  technologies: asset.technologies,
464
517
  readme_raw: asset.readme_raw,
465
518
  all_translations: asset.all_translations,
@@ -476,6 +529,15 @@ export const createAsset = async (token: string, asset: TAssetMissing) => {
476
529
  headers.Academy = String(asset.academy_id)
477
530
  }
478
531
 
532
+ console.log(
533
+ "[BC] POST",
534
+ url,
535
+ "| academy_id:",
536
+ asset.academy_id ?? "none",
537
+ "| slug:",
538
+ asset.slug
539
+ )
540
+
479
541
  try {
480
542
  const response = await axios.post(url, body, { headers })
481
543
  return response.data
@@ -512,14 +574,19 @@ export const doesAssetExists = async (
512
574
  const updateAsset = async (
513
575
  token: string,
514
576
  assetSlug: string,
515
- asset: Partial<TAssetMissing>
577
+ asset: Partial<TAssetMissing> & { learnpack_id: number }
516
578
  ) => {
517
579
  const url = `https://breathecode.herokuapp.com/v1/registry/asset/me/${assetSlug}`
518
580
  const headers = {
519
581
  Authorization: `Token ${token}`,
520
582
  }
583
+
584
+ const body = { ...asset, learnpack_id: asset.learnpack_id }
585
+
586
+ console.log("[BC] PUT", url, "| academy_id:", asset.academy_id ?? "none")
587
+
521
588
  try {
522
- const response = await axios.put(url, asset, { headers })
589
+ const response = await axios.put(url, body, { headers })
523
590
  return response.data
524
591
  } catch (error: any) {
525
592
  console.error("Failed to update asset:", error)
@@ -593,7 +660,11 @@ type TTechnology = {
593
660
 
594
661
  let technologiesCache: TTechnology[] = []
595
662
 
596
- /** Strip .env comments (e.g. "token # comment") so the token is sent without spaces. */
663
+ /**
664
+ * Strip .env comments (e.g. "token # comment") so the token is sent without spaces.
665
+ * @param token - Raw token value from env, possibly with trailing comment.
666
+ * @returns Trimmed token with inline `#` comments removed, or empty string if missing.
667
+ */
597
668
  function sanitizeToken(token: string | undefined): string {
598
669
  if (!token) return ""
599
670
  const trimmed = token.trim()
@@ -677,6 +748,7 @@ export default {
677
748
  createAsset,
678
749
  doesAssetExists,
679
750
  updateAsset,
751
+ resolveLearnpackPackageId,
680
752
  getCategories,
681
753
  updateRigoPackage,
682
754
  createRigoPackage,