update dependencies, devDependencies, copyright
[websub-hub] / src / manager.js
index 9d4a282c2aac17864d3e330ba4e655cd424493fc..d33d345d280fc3a4bf2b422faf9e66abd4f39e23 100644 (file)
@@ -354,7 +354,7 @@ class Manager {
 
     // Parse and validate all the topics in the request.
     data.publishTopics = await this._publishTopics(dbCtx, data, requestId);
-    if (!data.publishTopics || !data.publishTopics.length) {
+    if (!data?.publishTopics?.length) {
       const details = Manager._prettyDetails(['no valid topic urls to publish'], []);
       throw new ResponseError(Enum.ErrorResponse.BadRequest, details);
     }
@@ -579,7 +579,7 @@ class Manager {
    * @param {object} ctx
    */
   async getHistorySVG(res, ctx) {
-    const _scope = _fileScope('getHist');
+    const _scope = _fileScope('getHistorySVG');
     this.logger.debug(_scope, 'called', { ctx });
 
     const days = Math.min(parseInt(ctx.queryParams.days) || this.options.manager.publishHistoryDays, 365);
@@ -606,6 +606,21 @@ class Manager {
     this.logger.info(_scope, 'finished', { ctx });
   }
 
+
+  /**
+   * Determine if a profile url matches enough of a topic url to describe control over it.
+   * Topic must match hostname and start with the profile's path.
+   * @param {URL} profileUrlObj
+   * @param {URL} topicUrlObj
+   * @returns {Boolean}
+   */
+  static _profileControlsTopic(profileUrlObj, topicUrlObj) {
+    const hostnameMatches = profileUrlObj.hostname === topicUrlObj.hostname;
+    const pathIsPrefix = topicUrlObj.pathname.startsWith(profileUrlObj.pathname);
+    return hostnameMatches && pathIsPrefix;
+  }
+
+
   /**
    * GET request for authorized /admin information.
    * @param {http.ServerResponse} res
@@ -621,11 +636,11 @@ class Manager {
     this.logger.debug(_scope, 'got topics', { topics: ctx.topics });
 
     // Profile users can only see related topics.
-    if (ctx.session && ctx.session.authenticatedProfile) {
+    if (ctx?.session?.authenticatedProfile) {
       const profileUrlObj = new URL(ctx.session.authenticatedProfile);
       ctx.topics = ctx.topics.filter((topic) => {
         const topicUrlObj = new URL(topic.url);
-        return (topicUrlObj.hostname === profileUrlObj.hostname);
+        return Manager._profileControlsTopic(profileUrlObj, topicUrlObj);
       });
     }
 
@@ -643,8 +658,7 @@ class Manager {
     const _scope = _fileScope('getTopicDetails');
     this.logger.debug(_scope, 'called', { ctx });
 
-
-    ctx.publishSpan = 60;
+    ctx.publishSpan = 60; // FIXME: configurable
     const topicId = ctx.params.topicId;
     let publishHistory;
     await this.db.context(async (dbCtx) => {
@@ -653,20 +667,23 @@ class Manager {
       publishHistory = await this.db.topicPublishHistory(dbCtx, topicId, ctx.publishSpan);
     });
     ctx.publishCount = publishHistory.reduce((a, b) => a + b, 0);
+    ctx.subscriptionsDelivered = ctx.subscriptions.filter((subscription) => {
+      return subscription.latestContentDelivered >= ctx.topic.contentUpdated;
+    }).length;
     this.logger.debug(_scope, 'got topic details', { topic: ctx.topic, subscriptions: ctx.subscriptions, updates: ctx.publishCount });
 
     // Profile users can only see related topics.
-    if (ctx.session && ctx.session.authenticatedProfile) {
+    if (ctx?.session?.authenticatedProfile) {
       const profileUrlObj = new URL(ctx.session.authenticatedProfile);
       const topicUrlObj = new URL(ctx.topic.url);
-      if (topicUrlObj.hostname !== profileUrlObj.hostname) {
+      if (!Manager._profileControlsTopic(profileUrlObj, topicUrlObj)) {
         ctx.topic = null;
         ctx.subscriptions = [];
       }
     }
 
     res.end(Template.adminTopicDetailsHTML(ctx, this.options));
-    this.logger.info(_scope, 'finished', { ctx, subscriptions: ctx.subscriptions.length, topic: ctx.topic && ctx.topic.id || ctx.topic });
+    this.logger.info(_scope, 'finished', { ctx, subscriptions: ctx.subscriptions.length, topic: ctx?.topic?.id || ctx.topic });
   }