diff --git a/appsv/model/src/main/scala/com/debiki/core/EditedSettings.scala b/appsv/model/src/main/scala/com/debiki/core/EditedSettings.scala index ef5f36fef..501b7024e 100644 --- a/appsv/model/src/main/scala/com/debiki/core/EditedSettings.scala +++ b/appsv/model/src/main/scala/com/debiki/core/EditedSettings.scala @@ -60,6 +60,7 @@ object ContentLicense { * Because only edited settings need to be saved to the database. */ case class EditedSettings( + authnDiagConf: Opt[JsObject], userMustBeAuthenticated: Option[Boolean], userMustBeApproved: Option[Boolean], expireIdleAfterMins: Option[Int], @@ -159,6 +160,8 @@ case class EditedSettings( enableTags: Option[Boolean], enableChat: Option[Boolean], enableDirectMessages: Option[Boolean], + enableAnonSens: Opt[Bo], + enablePresence: Opt[Bo], enableSimilarTopics: Option[Boolean], enableCors: Option[Boolean], allowCorsFrom: Option[String], @@ -166,6 +169,8 @@ case class EditedSettings( showSubCommunities: Option[Boolean], showExperimental: Option[Boolean], featureFlags: Option[String], + ownDomains: Opt[St], + followLinksTo: Opt[St], allowEmbeddingFrom: Option[String], embeddedCommentsCategoryId: Option[CategoryId], htmlTagCssClasses: Option[String], @@ -211,6 +216,7 @@ object EditedSettings { val MaxNumFirstPosts = 10 val empty: EditedSettings = EditedSettings( + authnDiagConf = None, userMustBeAuthenticated = None, userMustBeApproved = None, expireIdleAfterMins = None, @@ -310,6 +316,8 @@ object EditedSettings { enableTags = None, enableChat = None, enableDirectMessages = None, + enableAnonSens = None, + enablePresence = None, enableSimilarTopics = None, enableCors = None, allowCorsFrom = None, @@ -317,6 +325,8 @@ object EditedSettings { showSubCommunities = None, showExperimental = None, featureFlags = None, + ownDomains = None, + followLinksTo = None, allowEmbeddingFrom = None, embeddedCommentsCategoryId = None, htmlTagCssClasses = None, @@ -338,6 +348,7 @@ object EditedSettings { * settingsToSave.title.get.get. */ case class SettingsToSave( + authnDiagConf: Opt[Opt[JsObject]] = None, userMustBeAuthenticated: Option[Option[Boolean]] = None, userMustBeApproved: Option[Option[Boolean]] = None, expireIdleAfterMins: Option[Option[Int]] = None, @@ -437,6 +448,8 @@ case class SettingsToSave( enableTags: Option[Option[Boolean]] = None, enableChat: Option[Option[Boolean]] = None, enableDirectMessages: Option[Option[Boolean]] = None, + enableAnonSens: Opt[Opt[Bo]] = None, + enablePresence: Opt[Opt[Bo]] = None, enableSimilarTopics: Option[Option[Boolean]] = None, enableCors: Option[Option[Boolean]] = None, allowCorsFrom: Option[Option[String]] = None, @@ -444,6 +457,8 @@ case class SettingsToSave( showSubCommunities: Option[Option[Boolean]] = None, showExperimental: Option[Option[Boolean]] = None, featureFlags: Option[Option[String]] = None, + ownDomains: Opt[Opt[St]] = None, + followLinksTo: Opt[Opt[St]] = None, allowEmbeddingFrom: Option[Option[String]] = None, embeddedCommentsCategoryId: Option[Option[CategoryId]] = None, htmlTagCssClasses: Option[Option[String]] = None, diff --git a/appsv/model/src/main/scala/com/debiki/core/Site.scala b/appsv/model/src/main/scala/com/debiki/core/Site.scala index d2e412c24..9a789b1cb 100644 --- a/appsv/model/src/main/scala/com/debiki/core/Site.scala +++ b/appsv/model/src/main/scala/com/debiki/core/Site.scala @@ -80,12 +80,22 @@ trait SiteTrait { def isTestSite: Bo = id <= Site.MaxTestSiteId - def isFeatureEnabled(ffName: St, serverFeatureFlags: St, onByDefault: Bo = false): Bo = { + def isFeatureEnabled(ffName: St, serverFeatureFlags: St, onByDefault: Bo = false): Bo = + SiteTrait.isFeatureEnabled( + ffName = ffName, siteFeatureFlags = featureFlags, + serverFeatureFlags = serverFeatureFlags, onByDefault = onByDefault) +} + + +// Move to somewhere better? +object SiteTrait { + def isFeatureEnabled(ffName: St, siteFeatureFlags: St, serverFeatureFlags: St, + onByDefault: Bo = false): Bo = { val offName = "0" + ffName // zero — same as when disabling options in Vim val enabledWholeServer = serverFeatureFlags.contains(ffName) val disabledWholeServer = serverFeatureFlags.contains(offName) - val enabledThisSite = featureFlags.contains(ffName) - val disabledThisSite = featureFlags.contains(offName) + val enabledThisSite = siteFeatureFlags.contains(ffName) + val disabledThisSite = siteFeatureFlags.contains(offName) val enabledSomewhere = onByDefault || enabledWholeServer || enabledThisSite val disabledSomewhere = disabledWholeServer || disabledThisSite diff --git a/appsv/model/src/main/scala/com/debiki/core/package.scala b/appsv/model/src/main/scala/com/debiki/core/package.scala index e8ff6a387..92cb556c4 100644 --- a/appsv/model/src/main/scala/com/debiki/core/package.scala +++ b/appsv/model/src/main/scala/com/debiki/core/package.scala @@ -1115,6 +1115,7 @@ package object core { */ sealed trait WhichAliasPat { def anyPat: Opt[Pat] + def anyAnonStatus: Opt[AnonStatus] } @@ -1127,6 +1128,7 @@ package object core { case class SameAnon(anon: Anonym) extends WhichAliasPat { def anyPat: Opt[Pat] = Some(anon) + def anyAnonStatus: Opt[AnonStatus] = Some(anon.anonStatus) } /** Reuses any already existing anonym with the same anon status, @@ -1137,6 +1139,7 @@ package object core { */ case class LazyCreatedAnon(anonStatus: AnonStatus) extends WhichAliasPat { def anyPat: Opt[Pat] = None // might not yet exist + def anyAnonStatus: Opt[AnonStatus] = Some(anonStatus) } // Let's not support creating more than one anonym per user & page, for now. diff --git a/appsv/rdb/src/main/resources/db/migration/db-wip.sql b/appsv/rdb/src/main/resources/db/migration/db-wip.sql index 09a25cd53..acd34449c 100644 --- a/appsv/rdb/src/main/resources/db/migration/db-wip.sql +++ b/appsv/rdb/src/main/resources/db/migration/db-wip.sql @@ -722,6 +722,7 @@ Add?: page_html_cache_t — restrict col lengths +-- ? add: param_logged_in_c bool, to inc/exclude authn diag stuff? [cached_html_loggedin_param] -- Remove email "identities" from identities3? diff --git a/appsv/rdb/src/main/scala/com/debiki/dao/rdb/SettingsSiteDaoMixin.scala b/appsv/rdb/src/main/scala/com/debiki/dao/rdb/SettingsSiteDaoMixin.scala index 6fa4884cc..87e675598 100644 --- a/appsv/rdb/src/main/scala/com/debiki/dao/rdb/SettingsSiteDaoMixin.scala +++ b/appsv/rdb/src/main/scala/com/debiki/dao/rdb/SettingsSiteDaoMixin.scala @@ -59,6 +59,7 @@ trait SettingsSiteDaoMixin extends SiteTransaction { site_id, category_id, page_id, + authn_diag_conf_c, user_must_be_auth, user_must_be_approved, expire_idle_after_mins, @@ -157,6 +158,8 @@ trait SettingsSiteDaoMixin extends SiteTransaction { enable_tags, enable_chat, enable_direct_messages, + enable_anon_posts_c, + enable_online_status_c, enable_similar_topics, enable_cors, allow_cors_from, @@ -164,21 +167,24 @@ trait SettingsSiteDaoMixin extends SiteTransaction { show_sub_communities, experimental, feature_flags, + own_domains_c, + follow_links_to_c, allow_embedding_from, embedded_comments_category_id, html_tag_css_classes) - values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, + values (?, ?, ?, ?::jsonb, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?, ?, ?, ?, ?, ?, - ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ val values = List( siteId.asAnyRef, NullInt, NullVarchar, + editedSettings2.authnDiagConf.getOrElse(None).orNullJson, editedSettings2.userMustBeAuthenticated.getOrElse(None).orNullBoolean, editedSettings2.userMustBeApproved.getOrElse(None).orNullBoolean, editedSettings2.expireIdleAfterMins.getOrElse(None).orNullInt, @@ -277,6 +283,8 @@ trait SettingsSiteDaoMixin extends SiteTransaction { editedSettings2.enableTags.getOrElse(None).orNullBoolean, editedSettings2.enableChat.getOrElse(None).orNullBoolean, editedSettings2.enableDirectMessages.getOrElse(None).orNullBoolean, + editedSettings2.enableAnonSens.getOrElse(None).orNullBoolean, + editedSettings2.enablePresence.getOrElse(None).orNullBoolean, editedSettings2.enableSimilarTopics.getOrElse(None).orNullBoolean, editedSettings2.enableCors.getOrElse(None).orNullBoolean, editedSettings2.allowCorsFrom.getOrElse(None).trimOrNullVarchar, @@ -284,6 +292,8 @@ trait SettingsSiteDaoMixin extends SiteTransaction { editedSettings2.showSubCommunities.getOrElse(None).orNullBoolean, editedSettings2.showExperimental.getOrElse(None).orNullBoolean, editedSettings2.featureFlags.getOrElse(None).trimOrNullVarchar, + editedSettings2.ownDomains.getOrElse(None).trimOrNullVarchar, + editedSettings2.followLinksTo.getOrElse(None).trimOrNullVarchar, editedSettings2.allowEmbeddingFrom.getOrElse(None).trimOrNullVarchar, editedSettings2.embeddedCommentsCategoryId.getOrElse(None).orNullInt, editedSettings2.htmlTagCssClasses.getOrElse(None).trimOrNullVarchar) @@ -308,6 +318,7 @@ trait SettingsSiteDaoMixin extends SiteTransaction { } val s = editedSettings2 + maybeSet("authn_diag_conf_c", s.authnDiagConf.map(_.orNullJson)) maybeSet("user_must_be_auth", s.userMustBeAuthenticated.map(_.orNullBoolean)) maybeSet("user_must_be_approved", s.userMustBeApproved.map(_.orNullBoolean)) maybeSet("expire_idle_after_mins", s.expireIdleAfterMins.map(_.orNullInt)) @@ -406,6 +417,8 @@ trait SettingsSiteDaoMixin extends SiteTransaction { maybeSet("enable_tags", s.enableTags.map(_.orNullBoolean)) maybeSet("enable_chat", s.enableChat.map(_.orNullBoolean)) maybeSet("enable_direct_messages", s.enableDirectMessages.map(_.orNullBoolean)) + maybeSet("enable_anon_posts_c", s.enableAnonSens.map(_.orNullBoolean)) + maybeSet("enable_online_status_c", s.enablePresence.map(_.orNullBoolean)) maybeSet("enable_similar_topics", s.enableSimilarTopics.map(_.orNullBoolean)) maybeSet("enable_cors", s.enableCors.map(_.orNullBoolean)) maybeSet("allow_cors_from", s.allowCorsFrom.map(_.orNullVarchar)) @@ -413,6 +426,8 @@ trait SettingsSiteDaoMixin extends SiteTransaction { maybeSet("show_sub_communities", s.showSubCommunities.map(_.orNullBoolean)) maybeSet("experimental", s.showExperimental.map(_.orNullBoolean)) maybeSet("feature_flags", s.featureFlags.map(_.trimOrNullVarchar)) + maybeSet("own_domains_c", s.ownDomains.map(_.trimOrNullVarchar)) + maybeSet("follow_links_to_c", s.followLinksTo.map(_.trimOrNullVarchar)) maybeSet("allow_embedding_from", s.allowEmbeddingFrom.map(_.trimOrNullVarchar)) maybeSet("embedded_comments_category_id", s.embeddedCommentsCategoryId.map(_.orNullInt)) maybeSet("html_tag_css_classes", s.htmlTagCssClasses.map(_.trimOrNullVarchar)) @@ -436,6 +451,7 @@ trait SettingsSiteDaoMixin extends SiteTransaction { private def readSettingsFromResultSet(rs: ResultSet): EditedSettings = { EditedSettings( + authnDiagConf = getOptJsObject(rs, "authn_diag_conf_c"), userMustBeAuthenticated = getOptBoolean(rs, "user_must_be_auth"), userMustBeApproved = getOptBoolean(rs, "user_must_be_approved"), expireIdleAfterMins = getOptInt(rs, "expire_idle_after_mins"), @@ -535,6 +551,8 @@ trait SettingsSiteDaoMixin extends SiteTransaction { enableTags = getOptBool(rs, "enable_tags"), enableChat = getOptBool(rs, "enable_chat"), enableDirectMessages = getOptBool(rs, "enable_direct_messages"), + enableAnonSens = getOptBool(rs, "enable_anon_posts_c"), + enablePresence = getOptBool(rs, "enable_online_status_c"), enableSimilarTopics = getOptBool(rs, "enable_similar_topics"), enableCors = getOptBool(rs, "enable_cors"), allowCorsFrom = getOptString(rs, "allow_cors_from"), @@ -542,6 +560,8 @@ trait SettingsSiteDaoMixin extends SiteTransaction { showSubCommunities = getOptBool(rs, "show_sub_communities"), showExperimental = getOptBool(rs, "experimental"), featureFlags = getOptString(rs, "feature_flags"), + ownDomains = getOptString(rs, "own_domains_c"), + followLinksTo = getOptString(rs, "follow_links_to_c"), allowEmbeddingFrom = getOptString(rs, "allow_embedding_from"), embeddedCommentsCategoryId = getOptInt(rs, "embedded_comments_category_id"), htmlTagCssClasses = getOptString(rs, "html_tag_css_classes"), diff --git a/appsv/server/controllers/AdminController.scala b/appsv/server/controllers/AdminController.scala index f7d7e7d45..d94257889 100644 --- a/appsv/server/controllers/AdminController.scala +++ b/appsv/server/controllers/AdminController.scala @@ -45,12 +45,12 @@ class AdminController @Inject()(cc: ControllerComponents, edContext: TyContext) // See /-/edit-member controllers.UserController.editMember - def redirectToAdminPage(): Action[Unit] = GetAction { _ => + def redirectToAdminPage(): Action[Unit] = GetActionIsLogin { _ => Redirect(routes.AdminController.viewAdminPage("").url) } - def viewAdminPage(whatever: String): Action[Unit] = AsyncGetAction { apiReq => + def viewAdminPage(whatever: String): Action[Unit] = AsyncGetActionIsLogin { apiReq => dieIfAssetsMissingIfDevTest() if (!apiReq.user.exists(_.isStaff)) { diff --git a/appsv/server/controllers/LoginController.scala b/appsv/server/controllers/LoginController.scala index 44b219a7e..835f52530 100644 --- a/appsv/server/controllers/LoginController.scala +++ b/appsv/server/controllers/LoginController.scala @@ -26,7 +26,7 @@ import javax.inject.Inject import play.api.libs.json.{JsNull, JsString, Json} import play.api.mvc._ import talkyard.server.TyLogging -import talkyard.server.authn.LoginReason +import talkyard.server.authn.{LoginReason, MinAuthnStrength} import talkyard.server.JsX @@ -37,6 +37,7 @@ class LoginController @Inject()(cc: ControllerComponents, edContext: TyContext) import context.globals import context.security.DiscardingSessionCookies + import context.plainApiActions.PlainApiAction import LoginController._ @@ -124,18 +125,22 @@ class LoginController @Inject()(cc: ControllerComponents, edContext: TyContext) /** Clears session cookies and ends the session server side too; unsubscribes * from any websockets channel. + * + * (Using `PlainApiAction` instead of `GetActionAllowAnyone` so can set `ignoreAlias`.) */ - def logout(currentUrlPath: Opt[St]): Action[U] = GetActionAllowAnyone { request => - SECURITY // optionally log out from all devices? + def logout_get_post(currentUrlPath: Opt[St]): Action[U] = PlainApiAction( + cc.parsers.empty, RateLimits.NoRateLimits, MinAuthnStrength.EmbeddingStorageSid12, + allowAnyone = true, ignoreAlias = true) { request => doLogout(request, redirectIfMayNotSeeUrlPath = currentUrlPath, wasImpersonating = false) } - def doLogout(request: GetRequest, redirectIfMayNotSeeUrlPath: Opt[St], + def doLogout(request: ApiRequest[_], redirectIfMayNotSeeUrlPath: Opt[St], wasImpersonating: Bo): Result = { import request.{dao, requester, siteSettings} + SECURITY // optionally log out from all devices? AUDIT_LOG // session id destruction requester foreach { theRequester => diff --git a/appsv/server/controllers/SuperAdminController.scala b/appsv/server/controllers/SuperAdminController.scala index 08be7b693..86ba3ba08 100644 --- a/appsv/server/controllers/SuperAdminController.scala +++ b/appsv/server/controllers/SuperAdminController.scala @@ -188,7 +188,8 @@ class SuperAdminController @Inject()(cc: ControllerComponents, edContext: TyCont if (inclStaff) { json += "staffUsers" -> JsArray(siteStuff.staff.map { staffUser => JsUserInclDetails( - staffUser, usersById = Map.empty, groups = Nil, callerIsAdmin = true) + staffUser, usersById = Map.empty, groups = Nil, callerIsAdmin = true, + maySeePresence = true, sensitiveAnonDisc = false) }) } json diff --git a/appsv/server/controllers/UserController.scala b/appsv/server/controllers/UserController.scala index 6db2ee96d..1f44ca5f3 100644 --- a/appsv/server/controllers/UserController.scala +++ b/appsv/server/controllers/UserController.scala @@ -54,8 +54,9 @@ class UserController @Inject()(cc: ControllerComponents, edContext: TyContext) val MaxEmailsPerUser: Int = 5 // also in js [4GKRDF0] - def listCompleteUsers(whichUsers: String): Action[Unit] = StaffGetAction { request => - val settings = request.dao.getWholeSiteSettings() + def listCompleteUsers(whichUsers: St): Action[Unit] = StaffGetAction { req => + import req.dao + val settings = dao.getWholeSiteSettings() var orderOffset: PeopleOrderOffset = PeopleOrderOffset.BySignedUpAtDesc var peopleFilter = PeopleFilter() @@ -85,7 +86,7 @@ class UserController @Inject()(cc: ControllerComponents, edContext: TyContext) val peopleQuery = PeopleQuery(orderOffset, peopleFilter) - request.dao.readOnlyTransaction { tx => + dao.readTx { tx => // Ok to load also deactivated users — the requester is staff. val membersAndStats = tx.loadUsersInclDetailsAndStats(peopleQuery) val members = membersAndStats.map(_._1) @@ -96,8 +97,11 @@ class UserController @Inject()(cc: ControllerComponents, edContext: TyContext) val usersJson = JsArray(membersAndStats.map(memberAndStats => { val member: UserInclDetails = memberAndStats._1 val anyStats: Option[UserStats] = memberAndStats._2 - JsUserInclDetails(member, usersById, groups = Nil, callerIsAdmin = request.theUser.isAdmin, - callerIsStaff = true, anyStats = anyStats) + JsUserInclDetails(member, usersById, groups = Nil, + callerIsAdmin = req.theUser.isAdmin, + sensitiveAnonDisc = settings.enableAnonSens, + maySeePresence = settings.enablePresence, + callerIsStaff = true, anyStats = anyStats) })) OkSafeJson(Json.obj("users" -> usersJson)) } @@ -158,6 +162,7 @@ class UserController @Inject()(cc: ControllerComponents, edContext: TyContext) request: DebikiRequest[_]): (JsObject, JsValue, Pat) = { import request.dao + val settings = dao.getWholeSiteSettings() val callerIsStaff = request.user.exists(_.isStaff) val callerIsAdmin = request.user.exists(_.isAdmin) val callerIsUserHerself = request.user.exists(_.id == userId) @@ -165,7 +170,7 @@ class UserController @Inject()(cc: ControllerComponents, edContext: TyContext) val reqrPerms: EffPatPerms = dao.deriveEffPatPerms(request.authzContext.groupIdsEveryoneLast) - request.dao.readOnlyTransaction { tx => + request.dao.readTx { tx => val stats = includeStats ? tx.loadUserStats(userId) | None val (pptJson, pat) = if (Participant.isRoleId(userId)) { @@ -175,6 +180,8 @@ class UserController @Inject()(cc: ControllerComponents, edContext: TyContext) case m: UserInclDetails => JsUserInclDetails(m, Map.empty, groups, callerIsAdmin = callerIsAdmin, callerIsStaff = callerIsStaff, callerIsUserHerself = callerIsUserHerself, + maySeePresence = settings.enablePresence, + sensitiveAnonDisc = settings.enableAnonSens, reqrPerms = Some(reqrPerms)) case g: Group => jsonForGroupInclDetails(g, callerIsAdmin = callerIsAdmin, @@ -193,7 +200,10 @@ class UserController @Inject()(cc: ControllerComponents, edContext: TyContext) } dieIf(pat.id != userId, "TyE36WKDJ03") (pptJson, - stats.map(JsUserStats(_, isStaffOrSelf, Some(reqrPerms))).getOrElse(JsNull), + stats.map(JsUserStats(_, Some(reqrPerms), callerIsStaff = callerIsStaff, + callerIsUserHerself = callerIsUserHerself, + callerIsAdmin = callerIsAdmin, maySeePresence = settings.enablePresence, + sensitiveAnonDisc = settings.enableAnonSens)).getOrElse(JsNull), pat.noDetails) } } @@ -205,6 +215,7 @@ class UserController @Inject()(cc: ControllerComponents, edContext: TyContext) : (JsObject, JsValue, Participant) = { import request.{dao} + val settings = dao.getWholeSiteSettings() val callerIsStaff = request.user.exists(_.isStaff) val callerIsAdmin = request.user.exists(_.isAdmin) val reqrPerms: EffPatPerms = @@ -254,9 +265,16 @@ class UserController @Inject()(cc: ControllerComponents, edContext: TyContext) val userJson = JsUserInclDetails( user, Map.empty, groups, callerIsAdmin = callerIsAdmin, callerIsStaff = callerIsStaff, callerIsUserHerself = callerIsUserHerself, + maySeePresence = settings.enablePresence, + sensitiveAnonDisc = settings.enableAnonSens, reqrPerms = Some(reqrPerms)) (userJson, - stats.map(JsUserStats(_, isStaffOrSelf, Some(reqrPerms))).getOrElse(JsNull), + stats.map(JsUserStats(_, Some(reqrPerms), + callerIsStaff = callerIsStaff, callerIsAdmin = callerIsAdmin, + callerIsUserHerself = callerIsUserHerself, + maySeePresence = settings.enablePresence, + sensitiveAnonDisc = settings.enableAnonSens, + )).getOrElse(JsNull), user.noDetails) case group: GroupVb => val groupJson = jsonForGroupInclDetails( @@ -358,7 +376,10 @@ class UserController @Inject()(cc: ControllerComponents, edContext: TyContext) } val anyStats: Option[UserStats] = tx.loadUserStats(userId) - val statsJson = anyStats.map(JsUserStats(_, isStaffOrSelf = true, reqrPerms = None) + val statsJson = anyStats.map(JsUserStats(_, reqrPerms = None, + callerIsStaff = false, callerIsAdmin = false, // does'nt matter + callerIsUserHerself = true, + maySeePresence = true, sensitiveAnonDisc = false) ) getOrElse JsNull val otherEmailAddresses = diff --git a/appsv/server/controllers/ViewPageController.scala b/appsv/server/controllers/ViewPageController.scala index 513aa2983..22dd9418c 100644 --- a/appsv/server/controllers/ViewPageController.scala +++ b/appsv/server/controllers/ViewPageController.scala @@ -21,6 +21,7 @@ import com.debiki.core._ import com.debiki.core.Prelude._ import debiki.RateLimits.NoRateLimits import debiki._ +import debiki.dao.UsersOnlineStuff import debiki.EdHttp._ import talkyard.server.http._ import play.api.libs.json._ @@ -428,12 +429,14 @@ object ViewPageController { embeddingUrl: Option[String] = None, skipUsersOnline: Boolean = false, xsrfTokenIfNoCookies: Option[String] = None): Future[Result] = { import request.{dao, requester} + val settings = dao.getWholeSiteSettings() // Could do asynchronously later. COULD avoid sending back empty json fields // — first verify that then nothing will break though. - val usersOnlineStuff = - if (skipUsersOnline) NoUsersOnlineStuff - else dao.getUsersOnlineStuff() + // In some situations, can be good if [admins_see_presence]. Related to [joint_decisions]? + val usersOnlineStuff: Opt[UsersOnlineStuff] = + if (skipUsersOnline || !settings.enablePresence) None + else Some(dao.getUsersOnlineStuff()) val anyMeAndRestrStuff: Opt[MeAndStuff] = request match { @@ -444,8 +447,8 @@ object ViewPageController { } var volatileJson = Json.obj( // ts: VolatileDataFromServer - "usersOnline" -> usersOnlineStuff.cachedUsersJson, - "numStrangersOnline" -> usersOnlineStuff.numStrangers, + "usersOnline" -> usersOnlineStuff.map(_.cachedUsersJson), + "numStrangersOnline" -> usersOnlineStuff.map(_.numStrangers), "me" -> JsObjOrNull(anyMeAndRestrStuff.map(_.me.meJsOb)), "stuffForMe" -> JsObjOrNull(anyMeAndRestrStuff.map(_.stuffForMe.toJson(dao))), ) diff --git a/appsv/server/debiki/RateLimits.scala b/appsv/server/debiki/RateLimits.scala index 6f01115eb..0d2ffe361 100644 --- a/appsv/server/debiki/RateLimits.scala +++ b/appsv/server/debiki/RateLimits.scala @@ -313,6 +313,9 @@ object RateLimits { val ReadsFromCache = ReadsFromDb // for now + val ReadsFromCacheALot = ReadsFromDb // for now + + object AdminWritesToDb extends RateLimits { val key = "AdWr" val what = "sent too many resource consuming HTTP requests" diff --git a/appsv/server/debiki/ReactJson.scala b/appsv/server/debiki/ReactJson.scala index 42b05beaa..5ef90a896 100644 --- a/appsv/server/debiki/ReactJson.scala +++ b/appsv/server/debiki/ReactJson.scala @@ -544,6 +544,16 @@ class JsonMaker(dao: SiteDao) { anyCurCatId.foreach(id => jsonObj += "listingCatId" -> JsNumber(id)) } + COULD_OPTIMIZE; SAVE_BANDWIDTH // Add only if needed [authn_diag_bandw], that is, + // if user not logged in. Maybe inject at the bottom in a new