musora-content-services 2.169.0-0 → 2.169.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/CHANGELOG.md +4 -2
- package/package.json +2 -1
- package/src/filterBuilder.js +2 -2
- package/src/index.d.ts +30 -16
- package/src/index.js +30 -16
- package/src/lib/sanity/decorators/examples.ts +0 -3
- package/src/lib/sanity/decorators/need-lifetime-upgrade.ts +1 -1
- package/src/lib/sanity/filter.ts +4 -4
- package/src/services/endScreen/endScreen.ts +1 -2
- package/src/services/eventsAPI.js +1 -1
- package/src/services/forums/types.ts +0 -2
- package/src/services/{user → notifications}/notifications.js +3 -3
- package/src/services/notifications/public-announcements.ts +234 -0
- package/src/services/permissions/PermissionsAdapter.ts +0 -24
- package/src/services/permissions/PermissionsV1Adapter.ts +2 -2
- package/src/services/permissions/PermissionsV2Adapter.ts +2 -2
- package/src/services/permissions/permissions.ts +2 -2
- package/src/services/permissions/types.d.ts +0 -3
- package/src/services/sanity.js +5 -5
- package/src/services/user/account.ts +9 -3
- package/src/services/user/session.ts +40 -0
- package/src/services/user/types.d.ts +0 -1
- package/src/version-info.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### [2.169.1](https://github.com/railroadmedia/musora-content-services/compare/v2.169.0...v2.169.1) (2026-08-07)
|
|
6
|
+
|
|
7
|
+
## [2.169.0](https://github.com/railroadmedia/musora-content-services/compare/v2.168.0...v2.169.0) (2026-08-04)
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
### Features
|
|
9
11
|
|
|
10
|
-
* **
|
|
12
|
+
* **BEHSTP-415:** MCS public announcement fetch fn's ([#1010](https://github.com/railroadmedia/musora-content-services/issues/1010)) ([d504fb1](https://github.com/railroadmedia/musora-content-services/commit/d504fb105ba5303bfe7327ddef24fe468bdec374))
|
|
11
13
|
|
|
12
14
|
## [2.168.0](https://github.com/railroadmedia/musora-content-services/compare/v2.167.6...v2.168.0) (2026-07-30)
|
|
13
15
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musora-content-services",
|
|
3
|
-
"version": "2.169.
|
|
3
|
+
"version": "2.169.1",
|
|
4
4
|
"description": "A package for Musoras content services ",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"ts-jest": "^29.2.4"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@portabletext/to-html": "^2.0.14",
|
|
48
49
|
"@sanity/client": "^5.4.2",
|
|
49
50
|
"dayjs": "^1.11.13",
|
|
50
51
|
"docdash": "^2.0.2",
|
package/src/filterBuilder.js
CHANGED
|
@@ -95,7 +95,7 @@ export class FilterBuilder {
|
|
|
95
95
|
// This must be run before _applyPublishDateRestrictions()
|
|
96
96
|
if (this.bypassStatuses) return this
|
|
97
97
|
if (this.availableContentStatuses.length === 0) {
|
|
98
|
-
if (this.userData.
|
|
98
|
+
if (this.userData.isAdmin) {
|
|
99
99
|
this.availableContentStatuses = [
|
|
100
100
|
this.STATUS_DRAFT,
|
|
101
101
|
this.STATUS_SCHEDULED,
|
|
@@ -136,7 +136,7 @@ export class FilterBuilder {
|
|
|
136
136
|
if (this.bypassPermissions && !this.showOnlyOwnedContent) return this
|
|
137
137
|
const adapter = getPermissionsAdapter()
|
|
138
138
|
// Check if admin (admins bypass permissions)
|
|
139
|
-
if (adapter.
|
|
139
|
+
if (adapter.isAdmin(this.userData) && !this.showOnlyOwnedContent) return this
|
|
140
140
|
|
|
141
141
|
// Generate permissions filter using adapter
|
|
142
142
|
const permissionsFilter = adapter.generatePermissionsFilter(this.userData, {
|
package/src/index.d.ts
CHANGED
|
@@ -235,6 +235,28 @@ import {
|
|
|
235
235
|
updateMultiUserAccount
|
|
236
236
|
} from './services/multi-user-accounts/multi-user-accounts.ts';
|
|
237
237
|
|
|
238
|
+
import {
|
|
239
|
+
deleteAllNotifications,
|
|
240
|
+
deleteNotification,
|
|
241
|
+
fetchLiveEventPollingState,
|
|
242
|
+
fetchNotificationSettings,
|
|
243
|
+
fetchNotifications,
|
|
244
|
+
fetchUnreadCount,
|
|
245
|
+
markAllNotificationsAsRead,
|
|
246
|
+
markNotificationAsRead,
|
|
247
|
+
markNotificationAsUnread,
|
|
248
|
+
pauseLiveEventPolling,
|
|
249
|
+
restoreNotification,
|
|
250
|
+
startLiveEventPolling,
|
|
251
|
+
updateNotificationSetting
|
|
252
|
+
} from './services/notifications/notifications.js';
|
|
253
|
+
|
|
254
|
+
import {
|
|
255
|
+
blockContentToHtml,
|
|
256
|
+
fetchAllPublicAnnouncements,
|
|
257
|
+
fetchPublicAnnouncement
|
|
258
|
+
} from './services/notifications/public-announcements.ts';
|
|
259
|
+
|
|
238
260
|
import {
|
|
239
261
|
getRecentActivityOffline
|
|
240
262
|
} from './services/offline/activities.ts';
|
|
@@ -439,22 +461,6 @@ import {
|
|
|
439
461
|
upgradeSubscription
|
|
440
462
|
} from './services/user/memberships.ts';
|
|
441
463
|
|
|
442
|
-
import {
|
|
443
|
-
deleteAllNotifications,
|
|
444
|
-
deleteNotification,
|
|
445
|
-
fetchLiveEventPollingState,
|
|
446
|
-
fetchNotificationSettings,
|
|
447
|
-
fetchNotifications,
|
|
448
|
-
fetchUnreadCount,
|
|
449
|
-
markAllNotificationsAsRead,
|
|
450
|
-
markNotificationAsRead,
|
|
451
|
-
markNotificationAsUnread,
|
|
452
|
-
pauseLiveEventPolling,
|
|
453
|
-
restoreNotification,
|
|
454
|
-
startLiveEventPolling,
|
|
455
|
-
updateNotificationSetting
|
|
456
|
-
} from './services/user/notifications.js';
|
|
457
|
-
|
|
458
464
|
import {
|
|
459
465
|
getOnboardingRecommendedContent,
|
|
460
466
|
getOnboardingStatus,
|
|
@@ -484,6 +490,10 @@ import {
|
|
|
484
490
|
updateProfileVisibility
|
|
485
491
|
} from './services/user/profile.ts';
|
|
486
492
|
|
|
493
|
+
import {
|
|
494
|
+
verifyOAuthToken
|
|
495
|
+
} from './services/user/session.ts';
|
|
496
|
+
|
|
487
497
|
import {
|
|
488
498
|
generateAuthSessionUrl,
|
|
489
499
|
login,
|
|
@@ -534,6 +544,7 @@ declare module 'musora-content-services' {
|
|
|
534
544
|
applyCloudflareWrapper,
|
|
535
545
|
applySanityTransformations,
|
|
536
546
|
assignModeratorToComment,
|
|
547
|
+
blockContentToHtml,
|
|
537
548
|
blockUser,
|
|
538
549
|
blockedUsers,
|
|
539
550
|
buildEntityAndTotalQuery,
|
|
@@ -585,6 +596,7 @@ declare module 'musora-content-services' {
|
|
|
585
596
|
extractSanityUrl,
|
|
586
597
|
fetchAll,
|
|
587
598
|
fetchAllFilterOptions,
|
|
599
|
+
fetchAllPublicAnnouncements,
|
|
588
600
|
fetchArtistBySlug,
|
|
589
601
|
fetchArtistLessons,
|
|
590
602
|
fetchArtists,
|
|
@@ -649,6 +661,7 @@ declare module 'musora-content-services' {
|
|
|
649
661
|
fetchPost,
|
|
650
662
|
fetchPosts,
|
|
651
663
|
fetchPracticeGoals,
|
|
664
|
+
fetchPublicAnnouncement,
|
|
652
665
|
fetchRecent,
|
|
653
666
|
fetchRecentActivitiesActiveTabs,
|
|
654
667
|
fetchRecentUserActivities,
|
|
@@ -880,6 +893,7 @@ declare module 'musora-content-services' {
|
|
|
880
893
|
userOnboardingForBrand,
|
|
881
894
|
verifyImageSRC,
|
|
882
895
|
verifyLocalDataContext,
|
|
896
|
+
verifyOAuthToken,
|
|
883
897
|
whoLikedComment,
|
|
884
898
|
whoLikedContent,
|
|
885
899
|
whoLikedPost,
|
package/src/index.js
CHANGED
|
@@ -239,6 +239,28 @@ import {
|
|
|
239
239
|
updateMultiUserAccount
|
|
240
240
|
} from './services/multi-user-accounts/multi-user-accounts.ts';
|
|
241
241
|
|
|
242
|
+
import {
|
|
243
|
+
deleteAllNotifications,
|
|
244
|
+
deleteNotification,
|
|
245
|
+
fetchLiveEventPollingState,
|
|
246
|
+
fetchNotificationSettings,
|
|
247
|
+
fetchNotifications,
|
|
248
|
+
fetchUnreadCount,
|
|
249
|
+
markAllNotificationsAsRead,
|
|
250
|
+
markNotificationAsRead,
|
|
251
|
+
markNotificationAsUnread,
|
|
252
|
+
pauseLiveEventPolling,
|
|
253
|
+
restoreNotification,
|
|
254
|
+
startLiveEventPolling,
|
|
255
|
+
updateNotificationSetting
|
|
256
|
+
} from './services/notifications/notifications.js';
|
|
257
|
+
|
|
258
|
+
import {
|
|
259
|
+
blockContentToHtml,
|
|
260
|
+
fetchAllPublicAnnouncements,
|
|
261
|
+
fetchPublicAnnouncement
|
|
262
|
+
} from './services/notifications/public-announcements.ts';
|
|
263
|
+
|
|
242
264
|
import {
|
|
243
265
|
getRecentActivityOffline
|
|
244
266
|
} from './services/offline/activities.ts';
|
|
@@ -443,22 +465,6 @@ import {
|
|
|
443
465
|
upgradeSubscription
|
|
444
466
|
} from './services/user/memberships.ts';
|
|
445
467
|
|
|
446
|
-
import {
|
|
447
|
-
deleteAllNotifications,
|
|
448
|
-
deleteNotification,
|
|
449
|
-
fetchLiveEventPollingState,
|
|
450
|
-
fetchNotificationSettings,
|
|
451
|
-
fetchNotifications,
|
|
452
|
-
fetchUnreadCount,
|
|
453
|
-
markAllNotificationsAsRead,
|
|
454
|
-
markNotificationAsRead,
|
|
455
|
-
markNotificationAsUnread,
|
|
456
|
-
pauseLiveEventPolling,
|
|
457
|
-
restoreNotification,
|
|
458
|
-
startLiveEventPolling,
|
|
459
|
-
updateNotificationSetting
|
|
460
|
-
} from './services/user/notifications.js';
|
|
461
|
-
|
|
462
468
|
import {
|
|
463
469
|
getOnboardingRecommendedContent,
|
|
464
470
|
getOnboardingStatus,
|
|
@@ -488,6 +494,10 @@ import {
|
|
|
488
494
|
updateProfileVisibility
|
|
489
495
|
} from './services/user/profile.ts';
|
|
490
496
|
|
|
497
|
+
import {
|
|
498
|
+
verifyOAuthToken
|
|
499
|
+
} from './services/user/session.ts';
|
|
500
|
+
|
|
491
501
|
import {
|
|
492
502
|
generateAuthSessionUrl,
|
|
493
503
|
login,
|
|
@@ -533,6 +543,7 @@ export {
|
|
|
533
543
|
applyCloudflareWrapper,
|
|
534
544
|
applySanityTransformations,
|
|
535
545
|
assignModeratorToComment,
|
|
546
|
+
blockContentToHtml,
|
|
536
547
|
blockUser,
|
|
537
548
|
blockedUsers,
|
|
538
549
|
buildEntityAndTotalQuery,
|
|
@@ -584,6 +595,7 @@ export {
|
|
|
584
595
|
extractSanityUrl,
|
|
585
596
|
fetchAll,
|
|
586
597
|
fetchAllFilterOptions,
|
|
598
|
+
fetchAllPublicAnnouncements,
|
|
587
599
|
fetchArtistBySlug,
|
|
588
600
|
fetchArtistLessons,
|
|
589
601
|
fetchArtists,
|
|
@@ -648,6 +660,7 @@ export {
|
|
|
648
660
|
fetchPost,
|
|
649
661
|
fetchPosts,
|
|
650
662
|
fetchPracticeGoals,
|
|
663
|
+
fetchPublicAnnouncement,
|
|
651
664
|
fetchRecent,
|
|
652
665
|
fetchRecentActivitiesActiveTabs,
|
|
653
666
|
fetchRecentUserActivities,
|
|
@@ -879,6 +892,7 @@ export {
|
|
|
879
892
|
userOnboardingForBrand,
|
|
880
893
|
verifyImageSRC,
|
|
881
894
|
verifyLocalDataContext,
|
|
895
|
+
verifyOAuthToken,
|
|
882
896
|
whoLikedComment,
|
|
883
897
|
whoLikedContent,
|
|
884
898
|
whoLikedPost,
|
|
@@ -22,7 +22,7 @@ export function lifetimeUpgradeDecorator(
|
|
|
22
22
|
return {
|
|
23
23
|
field: NEED_LIFETIME_UPGRADE_FIELD,
|
|
24
24
|
compute: (item) => {
|
|
25
|
-
if (userPermissions?.
|
|
25
|
+
if (userPermissions?.isAdmin || !hasLifetime || hasPlus) return false
|
|
26
26
|
if (!item.need_access) return false
|
|
27
27
|
return item.membership_tier === 'plus'
|
|
28
28
|
},
|
package/src/lib/sanity/filter.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface StatusConfig {
|
|
|
23
23
|
statuses?: string[]
|
|
24
24
|
bypassStatuses?: boolean
|
|
25
25
|
isSingle?: boolean
|
|
26
|
-
|
|
26
|
+
isAdmin?: boolean
|
|
27
27
|
prefix?: Prefix
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -349,7 +349,7 @@ export class Filters {
|
|
|
349
349
|
const adapter = getPermissionsAdapter()
|
|
350
350
|
const userData = config.userData || (await adapter.fetchUserPermissions())
|
|
351
351
|
|
|
352
|
-
if (adapter.
|
|
352
|
+
if (adapter.isAdmin(userData)) return ''
|
|
353
353
|
|
|
354
354
|
const permissionsFilter = adapter.generatePermissionsFilter(userData, {
|
|
355
355
|
prefix: config.prefix || '',
|
|
@@ -373,9 +373,9 @@ export class Filters {
|
|
|
373
373
|
// Auto-determine statuses if not provided
|
|
374
374
|
if (statuses.length === 0) {
|
|
375
375
|
const userData = await getPermissionsAdapter().fetchUserPermissions()
|
|
376
|
-
const
|
|
376
|
+
const isAdmin = getPermissionsAdapter().isAdmin(userData)
|
|
377
377
|
|
|
378
|
-
if (config.
|
|
378
|
+
if (config.isAdmin || isAdmin) {
|
|
379
379
|
statuses = [
|
|
380
380
|
STATUS_DRAFT,
|
|
381
381
|
STATUS_SCHEDULED,
|
|
@@ -34,8 +34,7 @@ export async function getEndScreen({
|
|
|
34
34
|
brand
|
|
35
35
|
}: GetEndScreenParams): Promise<EndScreenResult> {
|
|
36
36
|
const userData = await getUserData()
|
|
37
|
-
|
|
38
|
-
const isAdmin = userData?.has_all_content_access ?? false
|
|
37
|
+
const isAdmin = userData?.is_admin ?? false
|
|
39
38
|
|
|
40
39
|
if (playlist) {
|
|
41
40
|
const nextItem = next_item ??
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {fetchLiveEventPollingState, fetchUnreadCount, pauseLiveEventPolling} from "./
|
|
1
|
+
import {fetchLiveEventPollingState, fetchUnreadCount, pauseLiveEventPolling} from "./notifications/notifications.js"
|
|
2
2
|
import {fetchLiveEvent} from "./sanity"
|
|
3
3
|
import { DataContext, PollingStateVersionKey } from './dataContext'
|
|
4
4
|
|
|
@@ -10,7 +10,6 @@ export interface Liker {
|
|
|
10
10
|
access_level: string
|
|
11
11
|
liked_at: string
|
|
12
12
|
liked_at_diff: string | null
|
|
13
|
-
badge: string | null
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
export interface WhoLikedResponse {
|
|
@@ -28,7 +27,6 @@ export interface ForumUser {
|
|
|
28
27
|
profile_picture_url: string | null
|
|
29
28
|
access_level: string
|
|
30
29
|
signature: string | null
|
|
31
|
-
badge: string | null
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
export interface ForumPost {
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { GET, PUT, DELETE } from '../../infrastructure/http/HttpClient.ts'
|
|
5
5
|
import eventsAPI from '../eventsAPI.js'
|
|
6
|
-
import '
|
|
7
|
-
import {globalConfig} from "../config";
|
|
8
|
-
import {fetchLiveEvent} from "../sanity";
|
|
6
|
+
import '../user/types.js'
|
|
7
|
+
import {globalConfig} from "../config.js";
|
|
8
|
+
import {fetchLiveEvent} from "../sanity.js";
|
|
9
9
|
|
|
10
10
|
const baseUrl = `/api/notifications`
|
|
11
11
|
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { fetchSanity, getSanityDate } from '../sanity.js'
|
|
2
|
+
import { globalConfig } from '../config.js'
|
|
3
|
+
|
|
4
|
+
interface PublicAnnouncement {
|
|
5
|
+
title: string
|
|
6
|
+
slug: string
|
|
7
|
+
published_on: string
|
|
8
|
+
description: string
|
|
9
|
+
message: string
|
|
10
|
+
forum_data?: Record<string, ForumData>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ForumData {
|
|
14
|
+
category_id: string
|
|
15
|
+
forum_id: string
|
|
16
|
+
url: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fetches a specific public announcement by slug, or by default the most recent published one.
|
|
21
|
+
* @param {string|null} slug
|
|
22
|
+
* @returns {Promise<PublicAnnouncement|null>} The public announcement object, or null if not found
|
|
23
|
+
*/
|
|
24
|
+
export async function fetchPublicAnnouncement(slug?: string): Promise<PublicAnnouncement | null> {
|
|
25
|
+
const now = getSanityDate(new Date())
|
|
26
|
+
|
|
27
|
+
const slugString = slug ? `&& slug.current == '${slug}'` : ''
|
|
28
|
+
|
|
29
|
+
// grab the specific one by slug, or the most recent one if no slug is provided
|
|
30
|
+
const query = `*[_type == 'public-announcement' && published_on <= '${now}' ${slugString}]
|
|
31
|
+
{
|
|
32
|
+
title,
|
|
33
|
+
'slug': slug.current,
|
|
34
|
+
published_on,
|
|
35
|
+
description,
|
|
36
|
+
message,
|
|
37
|
+
forum_data
|
|
38
|
+
}
|
|
39
|
+
| order(published_on desc)`
|
|
40
|
+
|
|
41
|
+
const result = await fetchSanity(query, false, {processNeedAccess: false, processPageType: false})
|
|
42
|
+
|
|
43
|
+
if (result?.message) {
|
|
44
|
+
result.message = blockContentToHtml(result.message)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Fetches all public announcements published within the last `spanDays` days (default 1 year).
|
|
52
|
+
* @param {number|null} spanDays - Number of days to look back for public announcements (default: 1 year)
|
|
53
|
+
* @returns {Promise<PublicAnnouncement[]|null>} Array of public announcement objects, or null if none found
|
|
54
|
+
*/
|
|
55
|
+
export async function fetchAllPublicAnnouncements(spanDays: number = 365): Promise<PublicAnnouncement[] | null> {
|
|
56
|
+
const rawNowDate = new Date()
|
|
57
|
+
const rawNow = new Date().getTime()
|
|
58
|
+
const nowDate = getSanityDate(rawNowDate)
|
|
59
|
+
const startDate = getSanityDate(new Date(rawNow - spanDays * 24 * 60 * 60 * 1000))
|
|
60
|
+
|
|
61
|
+
const query = `*[_type == 'public-announcement' && published_on >= '${startDate}' && published_on <= '${nowDate}']
|
|
62
|
+
{
|
|
63
|
+
title,
|
|
64
|
+
'slug': slug.current,
|
|
65
|
+
published_on
|
|
66
|
+
}
|
|
67
|
+
| order(published_on desc)`
|
|
68
|
+
|
|
69
|
+
return await fetchSanity(query, true, {processNeedAccess: false, processPageType: false})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ListStackEntry {
|
|
73
|
+
tag: 'ul' | 'ol'
|
|
74
|
+
level: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {Array<Object>} blocks - Sanity Portable Text / block content array
|
|
79
|
+
* @returns {string} HTML string
|
|
80
|
+
*/
|
|
81
|
+
export function blockContentToHtml(blocks: any[]): string {
|
|
82
|
+
if (!blocks) {
|
|
83
|
+
return ''
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let html = ''
|
|
87
|
+
const listStack: ListStackEntry[] = []
|
|
88
|
+
|
|
89
|
+
for (const block of blocks) {
|
|
90
|
+
const listItem = block.listItem ?? null
|
|
91
|
+
|
|
92
|
+
if (!listItem) {
|
|
93
|
+
html += closeLists(listStack, 0)
|
|
94
|
+
html += block._type === 'image' ? renderImage(block) : renderBlock(block)
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const level = block.level ?? 1
|
|
99
|
+
const tag: 'ul' | 'ol' = listItem === 'number' ? 'ol' : 'ul'
|
|
100
|
+
|
|
101
|
+
while (
|
|
102
|
+
listStack.length &&
|
|
103
|
+
(listStack[listStack.length - 1].level > level ||
|
|
104
|
+
(listStack[listStack.length - 1].level === level && listStack[listStack.length - 1].tag !== tag))
|
|
105
|
+
) {
|
|
106
|
+
const closed = listStack.pop()
|
|
107
|
+
html += `</${closed.tag}>`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
while (!listStack.length || listStack[listStack.length - 1].level < level) {
|
|
111
|
+
listStack.push({ tag, level: listStack.length + 1 })
|
|
112
|
+
html += `<${tag}>`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
html += `<li>${renderInline(block)}</li>`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
html += closeLists(listStack, 0)
|
|
119
|
+
return html
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function closeLists(listStack: ListStackEntry[], downToLevel: number): string {
|
|
123
|
+
let html = ''
|
|
124
|
+
while (listStack.length && listStack[listStack.length - 1].level > downToLevel) {
|
|
125
|
+
const closed = listStack.pop()
|
|
126
|
+
html += `</${closed.tag}>`
|
|
127
|
+
}
|
|
128
|
+
return html
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderInline(block: any): string {
|
|
132
|
+
const markDefsByKey: Record<string, any> = {}
|
|
133
|
+
for (const markDef of block.markDefs ?? []) {
|
|
134
|
+
markDefsByKey[markDef._key] = markDef
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let inner = ''
|
|
138
|
+
for (const child of block.children ?? []) {
|
|
139
|
+
inner += renderSpan(child, markDefsByKey)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return inner === '' ? ' ' : inner
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderBlock(block: any): string {
|
|
146
|
+
const tag = ((): string => {
|
|
147
|
+
switch (block.style ?? 'normal') {
|
|
148
|
+
case 'h1':
|
|
149
|
+
return 'h1'
|
|
150
|
+
case 'h2':
|
|
151
|
+
return 'h2'
|
|
152
|
+
case 'h3':
|
|
153
|
+
return 'h3'
|
|
154
|
+
case 'h4':
|
|
155
|
+
return 'h4'
|
|
156
|
+
case 'blockquote':
|
|
157
|
+
return 'blockquote'
|
|
158
|
+
default:
|
|
159
|
+
return 'p'
|
|
160
|
+
}
|
|
161
|
+
})()
|
|
162
|
+
|
|
163
|
+
const inner = renderInline(block)
|
|
164
|
+
|
|
165
|
+
return `<${tag}>${inner}</${tag}>`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function renderSpan(child: any, markDefsByKey: Record<string, any>): string {
|
|
169
|
+
let text = escapeHtml(child.text ?? '')
|
|
170
|
+
text = text.replace(/\n/g, '<br />')
|
|
171
|
+
|
|
172
|
+
for (const mark of child.marks ?? []) {
|
|
173
|
+
switch (true) {
|
|
174
|
+
case mark === 'strong':
|
|
175
|
+
text = `<strong>${text}</strong>`
|
|
176
|
+
break
|
|
177
|
+
case mark === 'em':
|
|
178
|
+
text = `<em>${text}</em>`
|
|
179
|
+
break
|
|
180
|
+
case mark === 'underline':
|
|
181
|
+
text = `<u>${text}</u>`
|
|
182
|
+
break
|
|
183
|
+
case mark === 'code':
|
|
184
|
+
text = `<code>${text}</code>`
|
|
185
|
+
break
|
|
186
|
+
case markDefsByKey[mark] !== undefined:
|
|
187
|
+
text = renderMarkDef(markDefsByKey[mark], text)
|
|
188
|
+
break
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return text
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function renderMarkDef(markDef: any, text: string): string {
|
|
196
|
+
if (markDef?._type !== 'link') {
|
|
197
|
+
return text
|
|
198
|
+
}
|
|
199
|
+
const href = escapeHtml(markDef.href ?? '')
|
|
200
|
+
return `<a href="${href}">${text}</a>`
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function renderImage(block: any): string {
|
|
204
|
+
const ref = block.asset?._ref ?? null
|
|
205
|
+
const percent = block.display_width_percent ?? 100
|
|
206
|
+
|
|
207
|
+
const url = sanityImageRefToUrl(ref)
|
|
208
|
+
if (!url) {
|
|
209
|
+
return ''
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return percent < 100
|
|
213
|
+
? `<img src="${url}" style="width:${percent}%;max-width:100%;" />`
|
|
214
|
+
: `<img src="${url}" />`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function escapeHtml(text: string): string {
|
|
218
|
+
return text
|
|
219
|
+
.replace(/&/g, '&')
|
|
220
|
+
.replace(/"/g, '"')
|
|
221
|
+
.replace(/'/g, ''')
|
|
222
|
+
.replace(/</g, '<')
|
|
223
|
+
.replace(/>/g, '>')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function sanityImageRefToUrl(ref: string | null | undefined): string | null {
|
|
227
|
+
const match = ref?.match(/^image-([a-f0-9]+)-(\d+x\d+)-(\w+)$/)
|
|
228
|
+
if (!match) {
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
const [, assetId, dimensions, format] = match
|
|
232
|
+
const { projectId, dataset } = globalConfig.sanityConfig
|
|
233
|
+
return `https://cdn.sanity.io/images/${projectId}/${dataset}/${assetId}-${dimensions}.${format}`
|
|
234
|
+
}
|
|
@@ -106,28 +106,4 @@ export abstract class PermissionsAdapter {
|
|
|
106
106
|
isModerator(userPermissions: UserPermissions): boolean {
|
|
107
107
|
return userPermissions?.isModerator ?? false
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
hasAllContentAccess(userPermissions: UserPermissions): boolean {
|
|
111
|
-
return userPermissions?.hasAllContentAccess ?? false
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Check if user is a forum moderator.
|
|
116
|
-
*
|
|
117
|
-
* @param userPermissions - The user's permissions
|
|
118
|
-
* @returns True if user is a forum moderator
|
|
119
|
-
*/
|
|
120
|
-
isForumModerator(userPermissions: UserPermissions): boolean {
|
|
121
|
-
return userPermissions?.isForumModerator ?? false
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Check if user is a comment moderator.
|
|
126
|
-
*
|
|
127
|
-
* @param userPermissions - The user's permissions
|
|
128
|
-
* @returns True if user is a comment moderator
|
|
129
|
-
*/
|
|
130
|
-
isCommentModerator(userPermissions: UserPermissions): boolean {
|
|
131
|
-
return userPermissions?.isCommentModerator ?? false
|
|
132
|
-
}
|
|
133
109
|
}
|
|
@@ -52,7 +52,7 @@ export class PermissionsV1Adapter extends PermissionsAdapter {
|
|
|
52
52
|
*/
|
|
53
53
|
doesUserNeedAccess(content: ContentItem, userPermissions: UserPermissions): boolean {
|
|
54
54
|
// Admins always have access
|
|
55
|
-
if (this.
|
|
55
|
+
if (this.isAdmin(userPermissions)) {
|
|
56
56
|
return false
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -102,7 +102,7 @@ export class PermissionsV1Adapter extends PermissionsAdapter {
|
|
|
102
102
|
} = options
|
|
103
103
|
|
|
104
104
|
// Admins bypass permission filter
|
|
105
|
-
if (this.
|
|
105
|
+
if (this.isAdmin(userPermissions)) {
|
|
106
106
|
return null
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -47,7 +47,7 @@ export class PermissionsV2Adapter extends PermissionsAdapter {
|
|
|
47
47
|
*/
|
|
48
48
|
doesUserNeedAccess(content: ContentItem, userPermissions: UserPermissions): boolean {
|
|
49
49
|
// Admins always have access
|
|
50
|
-
if (this.
|
|
50
|
+
if (this.isAdmin(userPermissions)) {
|
|
51
51
|
return false
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -108,7 +108,7 @@ export class PermissionsV2Adapter extends PermissionsAdapter {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// Admins bypass permission filter
|
|
111
|
-
if (this.
|
|
111
|
+
if (this.isAdmin(userPermissions)) {
|
|
112
112
|
return null
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -38,7 +38,7 @@ export async function reset(): Promise<void> {
|
|
|
38
38
|
export function isUserFreeTier(userPermissions: UserPermissions): boolean {
|
|
39
39
|
const permissions = userPermissions.permissions
|
|
40
40
|
|
|
41
|
-
if (userPermissions.
|
|
41
|
+
if (userPermissions.isAdmin) return false
|
|
42
42
|
|
|
43
43
|
return permissions.includes(MEMBERSHIP_PERMISSIONS.free)
|
|
44
44
|
&& !permissions.some(p => [MEMBERSHIP_PERMISSIONS.base, MEMBERSHIP_PERMISSIONS.plus].includes(p))
|
|
@@ -46,7 +46,7 @@ export function isUserFreeTier(userPermissions: UserPermissions): boolean {
|
|
|
46
46
|
|
|
47
47
|
export function doesUserHaveMembership(userPermissions: UserPermissions): boolean {
|
|
48
48
|
const permissions = userPermissions.permissions
|
|
49
|
-
return userPermissions.
|
|
49
|
+
return userPermissions.isAdmin
|
|
50
50
|
|| permissions.includes(MEMBERSHIP_PERMISSIONS.base)
|
|
51
51
|
|| permissions.includes(MEMBERSHIP_PERMISSIONS.plus)
|
|
52
52
|
}
|
|
@@ -3,7 +3,4 @@ export interface UserPermissions {
|
|
|
3
3
|
isAdmin: boolean // Whether the user is an admin
|
|
4
4
|
isModerator: boolean // Whether the user is a moderator
|
|
5
5
|
isABasicMember: boolean // Whether the user has basic membership
|
|
6
|
-
hasAllContentAccess: boolean
|
|
7
|
-
isForumModerator: boolean
|
|
8
|
-
isCommentModerator: boolean
|
|
9
6
|
}
|
package/src/services/sanity.js
CHANGED
|
@@ -899,14 +899,14 @@ export async function fetchAllFilterOptions(
|
|
|
899
899
|
const progressFilter = progressIds ? `&& railcontent_id in [${progressIds.join(',')}]` : ''
|
|
900
900
|
const adapter = getPermissionsAdapter()
|
|
901
901
|
const userPermissionsData = await adapter.fetchUserPermissions()
|
|
902
|
-
const
|
|
902
|
+
const isAdmin = adapter.isAdmin(userPermissionsData)
|
|
903
903
|
|
|
904
904
|
const constructCommonFilter = (excludeFilter) => {
|
|
905
905
|
const filterWithoutOption = excludeFilter
|
|
906
906
|
? filtersToGroq(filters, excludeFilter)
|
|
907
907
|
: includedFieldsFilter
|
|
908
908
|
const statusFilter = ' && status == "published"'
|
|
909
|
-
const includeStatusFilter = !
|
|
909
|
+
const includeStatusFilter = !isAdmin && !['instructor', 'artist', 'genre'].includes(contentType)
|
|
910
910
|
|
|
911
911
|
return coachId
|
|
912
912
|
? `brand == '${brand}' && status == "published" && references(*[_type=='instructor' && railcontent_id == ${coachId}]._id) ${filterWithoutOption || ''} ${term ? ` && (title match "${term}" || album match "${term}" || artist->name match "${term}" || genre[]->name match "${term}")` : ''}`
|
|
@@ -2117,14 +2117,14 @@ export async function fetchTabData(
|
|
|
2117
2117
|
// Check if user is admin to determine available content statuses
|
|
2118
2118
|
const adapter = getPermissionsAdapter()
|
|
2119
2119
|
const userData = await adapter.fetchUserPermissions()
|
|
2120
|
-
const
|
|
2120
|
+
const isAdminORModerator = adapter.isAdmin(userData) || adapter.isModerator(userData)
|
|
2121
2121
|
|
|
2122
2122
|
const filterWithRestrictions = await new FilterBuilder(filter, {
|
|
2123
2123
|
showMembershipRestrictedContent: true,
|
|
2124
|
-
availableContentStatuses:
|
|
2124
|
+
availableContentStatuses: isAdminORModerator
|
|
2125
2125
|
? CONTENT_STATUSES.ADMIN_ALL
|
|
2126
2126
|
: CONTENT_STATUSES.PUBLISHED_ONLY,
|
|
2127
|
-
pullFutureContent:
|
|
2127
|
+
pullFutureContent: isAdminORModerator ? true : false,
|
|
2128
2128
|
}).buildFilter()
|
|
2129
2129
|
query = buildEntityAndTotalQuery(filterWithRestrictions, entityFieldsString, {
|
|
2130
2130
|
sortOrder: sortOrder,
|
|
@@ -6,17 +6,23 @@ import { HttpError } from '../../infrastructure/http/interfaces/HttpError'
|
|
|
6
6
|
import { globalConfig } from '../config.js'
|
|
7
7
|
import { clearAllCachedData } from '../dataContext.js'
|
|
8
8
|
import { Onboarding } from './onboarding'
|
|
9
|
+
import { OAuthProvider } from './session'
|
|
9
10
|
import { AuthResponse } from './types'
|
|
10
11
|
|
|
12
|
+
export interface AccountStatus {
|
|
13
|
+
requires_setup: boolean
|
|
14
|
+
last_login_provider?: OAuthProvider
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
/**
|
|
12
18
|
* @param {string} email - The email address to check the account status for.
|
|
13
|
-
* @returns {Promise<
|
|
19
|
+
* @returns {Promise<OAuthProvider>} - A promise that resolves to an object indicating whether account setup is required, or an HttpError if the request fails.
|
|
14
20
|
*
|
|
15
21
|
* @throws {HttpError} - Throws HttpError if the request fails.
|
|
16
22
|
*/
|
|
17
|
-
export async function status(email: string): Promise<
|
|
23
|
+
export async function status(email: string): Promise<OAuthProvider> {
|
|
18
24
|
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
19
|
-
return await httpClient.post<
|
|
25
|
+
return await httpClient.post<OAuthProvider>(
|
|
20
26
|
`/api/user-management-system/v1/accounts/${encodeURIComponent(email)}/status`,
|
|
21
27
|
[]
|
|
22
28
|
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { globalConfig } from '../config.js'
|
|
2
|
+
import { HttpClient } from '@/infrastructure/http'
|
|
3
|
+
import { AuthResponse } from './types'
|
|
4
|
+
|
|
5
|
+
export interface VerifyOAuthTokenParams {
|
|
6
|
+
id_token: string
|
|
7
|
+
access_token?: string
|
|
8
|
+
device_name: string
|
|
9
|
+
device_token?: string
|
|
10
|
+
platform?: 'ios' | 'android'
|
|
11
|
+
user?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type OAuthProvider = 'google' | 'apple'
|
|
15
|
+
|
|
16
|
+
export async function verifyOAuthToken(
|
|
17
|
+
provider: OAuthProvider,
|
|
18
|
+
params: VerifyOAuthTokenParams
|
|
19
|
+
): Promise<AuthResponse> {
|
|
20
|
+
const apiUrl = `/api/user-management-system/v1/oauth/${encodeURIComponent(provider)}/verify`
|
|
21
|
+
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
22
|
+
return httpClient.post<AuthResponse>(apiUrl, params)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RedirectToOAuthProviderOptions {
|
|
26
|
+
redirect_to?: string
|
|
27
|
+
flow?: string
|
|
28
|
+
theme?: string
|
|
29
|
+
[key: string]: string | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function redirectToOAuthProvider(
|
|
33
|
+
provider: OAuthProvider,
|
|
34
|
+
options: RedirectToOAuthProviderOptions
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
const queryParams = new URLSearchParams(options as Record<string, string>).toString()
|
|
37
|
+
const apiUrl = `/api/user-management-system/v1/oauth/${encodeURIComponent(provider)}/redirect?${queryParams}`
|
|
38
|
+
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
39
|
+
return httpClient.get(apiUrl)
|
|
40
|
+
}
|
package/src/version-info.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCS_VERSION = '2.169.
|
|
1
|
+
export const MCS_VERSION = '2.169.1'
|