From 0302b72af6f1032b67a7ba7dc88e6f5353b51c27 Mon Sep 17 00:00:00 2001 From: Kaj Magnus Lindberg Date: Mon, 9 Sep 2024 09:20:56 +0200 Subject: [PATCH 01/28] Bump version to v0.2024.007. --- relchans/tyse-v0-dev | 2 +- version.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/relchans/tyse-v0-dev b/relchans/tyse-v0-dev index 9b9f590da..7ffd6aa86 160000 --- a/relchans/tyse-v0-dev +++ b/relchans/tyse-v0-dev @@ -1 +1 @@ -Subproject commit 9b9f590da473869c6bb9336b1226eaad545c23bc +Subproject commit 7ffd6aa86153045ab663ed1450f8c4dc4940c1c6 diff --git a/version.txt b/version.txt index 798315efb..3a75eb0bc 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v0.2024.006 +v0.2024.007 From 6abaeef0f8531749179922a0edf1697e878d21fb Mon Sep 17 00:00:00 2001 From: Kaj Magnus Lindberg Date: Mon, 9 Sep 2024 13:21:57 +0200 Subject: [PATCH 02/28] Temp fix flappy e2e test? --- .../specs/embcom.sort-order-op-likes-btn-txt.2br.ec.e2e.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/e2e-wdio7/specs/embcom.sort-order-op-likes-btn-txt.2br.ec.e2e.ts b/tests/e2e-wdio7/specs/embcom.sort-order-op-likes-btn-txt.2br.ec.e2e.ts index df0507455..d4b9ce0a8 100644 --- a/tests/e2e-wdio7/specs/embcom.sort-order-op-likes-btn-txt.2br.ec.e2e.ts +++ b/tests/e2e-wdio7/specs/embcom.sort-order-op-likes-btn-txt.2br.ec.e2e.ts @@ -246,6 +246,9 @@ describe("embcom.sort-order-op-likes-btn-txt.2br.ec TyTEMBSORTLIKETXT", () => { // 2024-06-18: Happened again: "Waiting for visible: .s_MB_Name" repeated until timeout. // 2024-06-20: Again // await mariasBrowser.metabar.waitUntilLoggedIn(); // not needed, let's skip? + // 2024-09-09: Again. Annoying. + await mariasBrowser.pause(2000); // let's just do this, for now. There're more + // important things to do. await mariasBrowser.complex.replyToEmbeddingBlogPost("Hi I am here"); }); From 111f3269a108b94cae4e2fa3d2ff9e1ee34bde3e Mon Sep 17 00:00:00 2001 From: Kaj Magnus Lindberg Date: Fri, 6 Sep 2024 16:53:55 +0200 Subject: [PATCH 03/28] Fix paseto-cmd build err. Add build docs. --- modules/paseto-cmd/README.txt | 12 +++++++++++- modules/paseto-cmd/src/main.rs | 6 +++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/modules/paseto-cmd/README.txt b/modules/paseto-cmd/README.txt index b3dde9d6a..7a000b0bf 100644 --- a/modules/paseto-cmd/README.txt +++ b/modules/paseto-cmd/README.txt @@ -7,7 +7,17 @@ The secret should be a hex string. If there's any error, then, right now, the program exists and prints just nothing (crickets!), or maybe some cryptic Rust error message. -But what's this for? + +Install Rust and Cargo, see: https://www.rust-lang.org/tools/install (as of Aug 2024). +Then, do: (in this folder, i.e. modules/paseto-cmd/) + + cargo build + +That generates `modules/paseto-cmd/target/debug/paseto-cmd`. The debug build is +all we need, see `encryptLocalPasetoV2Token()` in ../../tests/e2e-wdio7/utils/utils.ts . + + +But why? This is for blog comments Single Sign-On (SSO) e2e tests. Then, need to generate PASETO tokens, but the only Javascript library that supports v2.local tokens diff --git a/modules/paseto-cmd/src/main.rs b/modules/paseto-cmd/src/main.rs index 6b373083b..4769f7ac7 100644 --- a/modules/paseto-cmd/src/main.rs +++ b/modules/paseto-cmd/src/main.rs @@ -10,9 +10,9 @@ fn main() { let args: Vec = env::args().collect(); // Later, handle & print errors: (rather than just unwrap & crash) - if let Err(e) = gen_token(args) { - eprintln!("{}", e); - } + //if let Err(e) = gen_token(args) { + // eprintln!("{}", e); + //} let key_as_hex: &String = &args[1]; let key: Vec = decode_hex(key_as_hex).unwrap(); From 8ed1ac648c2b5da2b2298e04e0f7474bf41bcc8f Mon Sep 17 00:00:00 2001 From: Kaj Magnus Lindberg Date: Mon, 16 Sep 2024 09:08:56 +0200 Subject: [PATCH 04/28] Upgr Chromedriver to 128.0.3. --- tests/e2e-wdio7/package.json | 2 +- tests/e2e-wdio7/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/e2e-wdio7/package.json b/tests/e2e-wdio7/package.json index fb14c395a..5b373d886 100644 --- a/tests/e2e-wdio7/package.json +++ b/tests/e2e-wdio7/package.json @@ -14,7 +14,7 @@ "@wdio/spec-reporter": "^7.20.3", "@wdio/types": "^7.20.3", "axios": "^0.26.1", - "chromedriver": "^128.0.0", + "chromedriver": "^128.0.3", "paseto.js": "^0.1.7", "ts-node": "^10.9.1", "wdio-chromedriver-service": "^7.3.2" diff --git a/tests/e2e-wdio7/yarn.lock b/tests/e2e-wdio7/yarn.lock index ea7b9e43a..d07cb7d58 100644 --- a/tests/e2e-wdio7/yarn.lock +++ b/tests/e2e-wdio7/yarn.lock @@ -1251,10 +1251,10 @@ chrome-launcher@^0.15.0: is-wsl "^2.2.0" lighthouse-logger "^1.0.0" -chromedriver@^128.0.0: - version "128.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-128.0.0.tgz#7f75a984101199e0bcc2c92fe9f91917fcd1f918" - integrity sha512-Ggo21z/dFQxTOTgU0vm0V59Mi79yyR+9AUk/KiVAsRfbDRdVZQYQWfgxnIvD/x8KOKn0oB7haRzDO/KfrKyvOA== +chromedriver@^128.0.3: + version "128.0.3" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-128.0.3.tgz#7c2cd2d160f269e78f40840ee7a043dac3687148" + integrity sha512-Xn/bknOpGlY9tKinwS/hVWeNblSeZvbbJbF8XZ73X1jeWfAFPRXx3fMLdNNz8DqruDbx3cKEJ5wR3mnst6G3iw== dependencies: "@testim/chrome-version" "^1.1.4" axios "^1.7.4" From ab47f367fd502b1c0ea09ee47dd94dc7ae0e19ff Mon Sep 17 00:00:00 2001 From: Kaj Magnus Lindberg Date: Tue, 17 Sep 2024 07:35:30 +0200 Subject: [PATCH 05/28] Don't send unnecessary fields when updating a SASite The server rejects the request, if request body too large. --- .../src/main/scala/com/debiki/core/Site.scala | 2 +- client/app-slim/model.ts | 15 +++++++ .../superadmin/superadmin-app.staff.ts | 42 +++++++++++++------ 3 files changed, 46 insertions(+), 13 deletions(-) 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 c3b9902d9..d2e412c24 100644 --- a/appsv/model/src/main/scala/com/debiki/core/Site.scala +++ b/appsv/model/src/main/scala/com/debiki/core/Site.scala @@ -138,7 +138,7 @@ sealed abstract class SiteStatus(val IntValue: i32) { } -case class SuperAdminSitePatch( +case class SuperAdminSitePatch( // ts: interface SASitePatch siteId: SiteId, newStatus: SiteStatus, newNotes: Opt[St], diff --git a/client/app-slim/model.ts b/client/app-slim/model.ts index 291ba0261..02cda6cc4 100644 --- a/client/app-slim/model.ts +++ b/client/app-slim/model.ts @@ -2963,6 +2963,21 @@ interface SASite { } +/// For updating an SASite (but not for purging it). +/// +interface SASitePatch { // Scala: case class SuperAdminSitePatch + id: SiteId + status: SiteStatus + superStaffNotes?: St + rdbQuotaMiBs?: Nr + fileQuotaMiBs?: Nr + readLimsMult: Nr | N + logLimsMult: Nr | N + createLimsMult: Nr | N + featureFlags: St +} + + interface Rect { top: number; left: number; diff --git a/client/app-staff/superadmin/superadmin-app.staff.ts b/client/app-staff/superadmin/superadmin-app.staff.ts index c36f2d2fc..6a70748dc 100644 --- a/client/app-staff/superadmin/superadmin-app.staff.ts +++ b/client/app-staff/superadmin/superadmin-app.staff.ts @@ -273,9 +273,10 @@ const SiteTableRow = createComponent({ }, changeStatus: function(newStatus: SiteStatus) { - const site: SASite = _.clone(this.props.site); - site.status = newStatus; - Server.updateSites([site]); + const site: SASite = this.props.site; + const patch: SASitePatch = pluckPatch(site); + patch.status = newStatus; + Server.updateSites([patch]); }, reindex: function() { @@ -284,16 +285,17 @@ const SiteTableRow = createComponent({ }, saveNotesAndFlags: function() { - const site: SASite = _.clone(this.props.site); + const site: SASite = this.props.site; + const patch: SASitePatch = pluckPatch(site); const state: SiteTableRowState = this.state; - site.rdbQuotaMiBs = state.rdbQuotaMiBs; - site.fileQuotaMiBs = state.fileQuotaMiBs; - site.readLimsMult = state.readLimsMult; - site.logLimsMult = state.logLimsMult; - site.createLimsMult = state.createLimsMult; - site.superStaffNotes = state.newNotes; - site.featureFlags = state.newFeatureFlags; - Server.updateSites([site]); + patch.rdbQuotaMiBs = state.rdbQuotaMiBs; + patch.fileQuotaMiBs = state.fileQuotaMiBs; + patch.readLimsMult = state.readLimsMult; + patch.logLimsMult = state.logLimsMult; + patch.createLimsMult = state.createLimsMult; + patch.superStaffNotes = state.newNotes; + patch.featureFlags = state.newFeatureFlags; + Server.updateSites([patch]); }, render: function() { @@ -493,6 +495,22 @@ const SiteTableRow = createComponent({ } }); + +function pluckPatch(site: SASite): SASitePatch { + return { + id: site.id, + status: site.status, + superStaffNotes: site.superStaffNotes, + rdbQuotaMiBs: site.rdbQuotaMiBs, + fileQuotaMiBs: site.fileQuotaMiBs, + readLimsMult: site.readLimsMult, + logLimsMult: site.logLimsMult, + createLimsMult: site.createLimsMult, + featureFlags: site.featureFlags, + }; +} + + //------------------------------------------------------------------------------ } //------------------------------------------------------------------------------ From b5fb54b5b0681c5d0e0238a4af05a193394ddac4 Mon Sep 17 00:00:00 2001 From: Kaj Magnus Lindberg Date: Thu, 19 Sep 2024 01:47:52 +0200 Subject: [PATCH 06/28] Feature flag for disabling the "That's a short comment" tips. --- appsv/model/src/main/scala/com/debiki/core/user.scala | 3 +++ client/app-editor/editor/editor.editor.ts | 5 ++++- client/app-slim/oop-methods.ts | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/appsv/model/src/main/scala/com/debiki/core/user.scala b/appsv/model/src/main/scala/com/debiki/core/user.scala index c9881001a..f5cb16822 100644 --- a/appsv/model/src/main/scala/com/debiki/core/user.scala +++ b/appsv/model/src/main/scala/com/debiki/core/user.scala @@ -1209,6 +1209,9 @@ sealed trait MemberInclDetails extends ParticipantInclDetails { RENAME // to Me */ def uiPrefs: Option[JsObject] + // Later: + //def modConf: Opt[JsObject] // the pats_t/users3.mod_conf_c column + def copyPrefs(uiPrefs: Opt[JsObject] = null, privPrefs: MemberPrivacyPrefs = null): MemberVb = { this match { case thiz: GroupVb => diff --git a/client/app-editor/editor/editor.editor.ts b/client/app-editor/editor/editor.editor.ts index 425f2301f..91a31fb80 100644 --- a/client/app-editor/editor/editor.editor.ts +++ b/client/app-editor/editor/editor.editor.ts @@ -2159,6 +2159,8 @@ export const Editor = createFactory({ ifNewPostLooksOk: function(titleErrorMessage, textErrorMessage, ifOkFn: () => Vo) { const state: EditorState = this.state; + const store: Store = state.store; + let errors = ''; if (titleErrorMessage && isBlank(state.title)) { errors += titleErrorMessage; @@ -2177,7 +2179,8 @@ export const Editor = createFactory({ // Haven't updated the tests — many would fail, if "That's a short ..." dialogs pop up. // Also, skip for staff users (if they write something short, it's probably ok) // — later, this'll be per group settings; see pats_t.mod_conf_c. - const skipProbl = isAutoTestSite() || user_isStaffOrCoreMember(state.store.me); + const skipProbl = isAutoTestSite() || user_isStaffOrCoreMember(state.store.me) || + !store_isFeatFlagOn(store, 'ffShortPostTips', true) const titleLen = state.title.trim().length; const textLen = state.text.trim().length; diff --git a/client/app-slim/oop-methods.ts b/client/app-slim/oop-methods.ts index 769107a00..5c2aac07b 100644 --- a/client/app-slim/oop-methods.ts +++ b/client/app-slim/oop-methods.ts @@ -917,6 +917,8 @@ export function settings_selectTopicType(settings: SettingsVisibleClientSide, me export function store_isFeatFlagOn(store: Store, featureFlag: St, defaultOn?: Bo): Bo { + // A bit weird: If a flag is off, `_.includes(..)` will set both `isOn` and `isOff`. But + // the last line: `isOn && !isOff` becomes false, so it works. Oh well. const offFlag = '0' + featureFlag; const isOn = defaultOn || _.includes(store.siteFeatureFlags, featureFlag) || From a05fee5bf78d3eae2a9acc742046437a739dd468 Mon Sep 17 00:00:00 2001 From: Kaj Magnus Lindberg Date: Wed, 11 Sep 2024 09:00:18 +0200 Subject: [PATCH 07/28] Refactor loginIfNeeded(): Create shared impl fn. --- .../app-more/login/create-user-dialog.more.ts | 1 + client/app-slim/form/form.ts | 2 +- client/app-slim/forum/forum.ts | 8 +- client/app-slim/login/login-if-needed.ts | 131 +++++++++++++----- client/app-slim/model.ts | 1 + client/app-slim/page/post-actions.ts | 10 +- .../react-elements/name-login-btns.ts | 5 +- client/app-slim/slim-bundle.d.ts | 2 +- client/app-slim/watchbar/watchbar.ts | 2 +- client/embedded-comments/blog-comments.ts | 4 + docs/tests-map.txt | 5 + 11 files changed, 120 insertions(+), 51 deletions(-) diff --git a/client/app-more/login/create-user-dialog.more.ts b/client/app-more/login/create-user-dialog.more.ts index ef09d1b3c..afdfa6afa 100644 --- a/client/app-more/login/create-user-dialog.more.ts +++ b/client/app-more/login/create-user-dialog.more.ts @@ -322,6 +322,7 @@ export var CreateUserDialogContent = createClassAndFactory({ } else if (props.afterLoginCallback || ( anyReturnToUrl && !eds.isInLoginPopup && + // We should not redirect here, if we should redirect from verification emails *only*. anyReturnToUrl.indexOf('_RedirFromVerifEmailOnly_') === -1)) { const returnToUrl = anyReturnToUrl.replace(/__dwHash__/, '#'); const currentUrl = window.location.toString(); diff --git a/client/app-slim/form/form.ts b/client/app-slim/form/form.ts index 81f9f759a..4f9dcf29f 100644 --- a/client/app-slim/form/form.ts +++ b/client/app-slim/form/form.ts @@ -45,7 +45,7 @@ export function activateAnyCustomForm() { Server.submitCustomFormAsNewTopic(formData); } else if (doWhat.value === 'SignUp') { - login.loginIfNeeded(LoginReason.SignUp, location.toString()); + login.loginIfNeeded(LoginReason.SignUp, ''); } else if (doWhat.value === 'SignUpSubmitUtx') { // [plugin] login.loginIfNeeded(LoginReason.SignUp, '/-/redir-to-my-last-topic', function() { diff --git a/client/app-slim/forum/forum.ts b/client/app-slim/forum/forum.ts index 8c9d035b6..aff11d4e0 100644 --- a/client/app-slim/forum/forum.ts +++ b/client/app-slim/forum/forum.ts @@ -550,8 +550,12 @@ const ForumButtons = createComponent({ }, createTopic: function(category: Category) { - const anyReturnToUrl = window.location.toString().replace(/#/, '__dwHash__'); - login.loginIfNeeded(LoginReason.CreateTopic, anyReturnToUrl, () => { + // Remember any #composeTopic action (FragActionType.ComposeTopic), so we'll + // continue composing, after having signed up (if needed) and clicked any + // email verification link. TESTS_MISSING TyTFRAGCOMPTO + const loc = window.location; + const returnToRelativeUrl = loc.pathname + loc.search + loc.hash.replace(/#/, '__dwHash__'); + login.loginIfNeeded(LoginReason.CreateTopic, returnToRelativeUrl, () => { if (this.isGone) return; const newTopicTypes = category.newTopicTypes || []; if (newTopicTypes.length === 0) { diff --git a/client/app-slim/login/login-if-needed.ts b/client/app-slim/login/login-if-needed.ts index 6a9ef70a2..6bd278328 100644 --- a/client/app-slim/login/login-if-needed.ts +++ b/client/app-slim/login/login-if-needed.ts @@ -16,8 +16,8 @@ */ /// +// (Why does this behave as -already-loaded.ts? Oh well. [_5BKRF020]) /// -// or should be ...already-loaded ? (5BKRF020) //------------------------------------------------------------------------------ namespace debiki2.login { @@ -48,70 +48,115 @@ export function getAuthnNonce(): St | U { // From before React.js. Gah! This needs to be refactored :-/ Try to remove this field. -export let anyContinueAfterLoginCallback = null; +export let anyContinueAfterLoginCallback: (() => V) | U; +/// If login needed, redirects to `postNr` only if the user was signing up and had +/// to click an email verification link (the redirect link is then in the email addr +/// verification email). Otherwise, runs `onOk()`. +/// export function loginIfNeededReturnToPost( - loginReason: LoginReason, postNr: PostNr, success: () => void, - willCompose?: boolean) { + loginReason: LoginReason, postNr: PostNr, onOk: () => V, willCompose?: Bo) { // If posting a progress post, then, after login, scroll to the bottom, so one // can click that button again — it's at the bottom. const anchor = loginReason === LoginReason.PostProgressPost ? FragActionHashScrollToBottom - : (postNr < FirstReplyNr ? '' : ( + : (postNr < FirstReplyNr + ? + // UX COULD: Here it could be nice, if in embedded comments, scroll down to + // the comments section? [scroll_to_emb_comts] + '' + : ( // We use 'comment-' for embedded comments; they start on nr 1 = post 2. [2PAWC0] - eds.isInEmbeddedCommentsIframe + // (Hopefully the embedding website has no elems with ids like 'comment-NNN'.) + eds.isInIframe ? FragParamCommentNr + (postNr - 1) : FragParamPostNr + postNr)); - loginIfNeededReturnToAnchor(loginReason, anchor, success, willCompose); + loginIfNeededImpl(loginReason, anchor, true, onOk, willCompose); } +/// Same as `loginIfNeededReturnToPost()` above, but goes to `anchor` (a #hash-fragment) +/// after any signup, instead of to a post nr. +/// export function loginIfNeededReturnToAnchor( - loginReason: LoginReason, anchor: string, success?: () => void, willCompose?: boolean) { - const returnToUrl = makeReturnToPageHashForVerifEmail(anchor); - success = success || function() {}; + loginReason: LoginReason, anchor: St, onOk?: () => V, willCompose?: Bo) { + loginIfNeededImpl(loginReason, anchor, true, onOk, willCompose); +} + + +/// If login needed, always redirects to `path` afterwards and ignores `onOk()`. +/// +export function loginIfNeeded(loginReason: LoginReason, path: St, onOk?: () => V, + willCompose?: Bo) { + loginIfNeededImpl(loginReason, path, false, onOk, willCompose); +} + + +function loginIfNeededImpl(loginReason: LoginReason, pathOrHash: St, redirFromEmailOnly: Bo, + onOk?: () => V, willCompose?: Bo) { + + onOk = onOk || function() {}; const store: Store = ReactStore.allData(); const me: Myself = store.me; + + // No login needed, or not until later when submitting any comment? if (me.isLoggedIn || (willCompose && ReactStore.mayComposeBeforeSignup())) { - success(); + onOk(); + return; } - else if (eds.isInIframe) { - // ... or only if isInSomeEmbCommentsIframe()? - anyContinueAfterLoginCallback = success; + const returnToUrl: St = redirFromEmailOnly + ? makeReturnToPageHashForVerifEmail(pathOrHash) + : (eds.embeddingUrl || location.toString()) + pathOrHash; + + // _Make_SSO url here? And if sth, then, can redir. + // Pass anchor or pathOrHash (just path?) and if there's a SSO url, + // then can use %-encoded return-to paths + hash: + // + // returnToOrigin + // returnToRelUrl (relative origin) + + // But prefix the returnToOrigin with "check_is_legit:" to force the SSO + // server to verify that the origin is legit, before redirecting anyone to there + // — so there won't be any pishing attacks. + + // And if ssoHow = 'RedirEmbeddingPage', then, do that the 1st thing. + + + if (eds.isInIframe) { + // TESTS_MISSING: Compose comment before logging in? Then, we'd be TyTEMBCOMPBEFLGI + // in the *editor* iframe, now, rather than the *comments* iframe. + + anyContinueAfterLoginCallback = onOk; // Don't open a dialog inside the iframe; open a popup instead. - // Need to open the popup here immediately, before loading any scripts, because if - // not done immediately after mouse click, the popup gets blocked (in Chrome at least). - // And when opening in a popup, we don't need any more scripts here in the main win anyway. + // Need to open the popup here immediately, because if not done immediately after + // mouse click, the popup gets blocked (in Chrome at least). + // + // (This'll call `LoginController.showLoginPopup()` in the app server, to show: + // ../../../appsv/server/views/authn/authnPage.scala.html in a popup. + // The popup calls `debiki2.login.getLoginDialog().openToSignUp()`. That's similar to + // the else case below, but in a popup, [_popup_or_not], with no SSO and not admin area.) + // const url = origin() + '/-/login-popup?mode=' + loginReason + // [2ABKW24T] '&isInLoginPopup&returnToUrl=' + returnToUrl; d.i.createLoginPopup(url); } - else { - loginIfNeeded(loginReason, returnToUrl, success); - } -} - - -// Later, merge with loginIfNeededReturnToAnchor() above, and rename to loginIfNeeded, and use only -// that fn always — then will work also in iframe (will open popup). -export function loginIfNeeded(loginReason: LoginReason, returnToUrl: St, onOk?: () => Vo, - willCompose?: Bo) { - if (ReactStore.getMe().isLoggedIn || (willCompose && ReactStore.mayComposeBeforeSignup())) { - if (onOk) onOk(); - } else { goToSsoPageOrElse(returnToUrl, loginReason, onOk, function() { Server.loadMoreScriptsBundle(() => { + // (This is similar to above [_popup_or_not], but in the main win, not in a popup.) + // People with an account, are typically logged in already, and won't get to here often. // Instead, most people here, are new users, so show the signup dialog. // But when creating a new site, one logs in as admin (NeedToBeAdmin) if one // clicked the link (verified one's admin email), but then tries to log in in // another browser. - // (Why won't this result in a compil err? (5BKRF020)) + + // (Why won't this result in a compil err? We're including: + // ../more-bundle-not-yet-loaded.ts only, not ...-already-loaded.ts. [_5BKRF020]) const diag = debiki2.login.getLoginDialog(); const logInOrSignUp = loginReason === LoginReason.NeedToBeAdmin ? diag.openToLogIn : diag.openToSignUp; @@ -124,6 +169,7 @@ export function loginIfNeeded(loginReason: LoginReason, returnToUrl: St, onOk?: export function openLoginDialogToSignUp(purpose: LoginReason) { + // _Make_SSO url here? And pass to goToSsoPage...() ? goToSsoPageOrElse(location.toString(), purpose, null, function() { Server.loadMoreScriptsBundle(() => { debiki2.login.getLoginDialog().openToSignUp(purpose); @@ -133,6 +179,7 @@ export function openLoginDialogToSignUp(purpose: LoginReason) { export function openLoginDialog(purpose: LoginReason) { + // _Make_SSO url here? And pass to goToSsoPage...() ? goToSsoPageOrElse(location.toString(), purpose, null, function() { Server.loadMoreScriptsBundle(() => { debiki2.login.getLoginDialog().openToLogIn(purpose); @@ -142,7 +189,7 @@ export function openLoginDialog(purpose: LoginReason) { function goToSsoPageOrElse(returnToUrl: St, toDoWhat: LoginReason | U, - doAfterLogin: () => void | U, orElse: () => void) { + doAfterLogin: (() => V) | U, orElse: () => V): V { // Dupl code? [SSOINSTAREDIR] const store: Store = ReactStore.allData(); const anySsoUrl: St | U = makeSsoUrl(store, returnToUrl); @@ -156,11 +203,15 @@ function goToSsoPageOrElse(returnToUrl: St, toDoWhat: LoginReason | U, // stay on the same page, and navigate away to the IDP only in a popup win, // so the editor stays open and one can submit the reply, after login. if (store.settings.enableSso) { + // This is Ty's own SSO. + // Harmless bug: If session & local storage don't work, this redirect will // destroy the browser authn nonce. [br_authn_nonce] location.assign(anySsoUrl); // backw compat, see above } else { + // This is SSO too, but using some standard like OAuth2 (not Ty's own). + // This'll trigger the [SSOINSTAREDIR] code in login-dialog.more.ts — the // SSO url then gets reconstructed, so we don't need to include it here. // BUT, sleeping BUG: we should incl the authn nonce! [br_authn_nonce] @@ -181,6 +232,10 @@ function goToSsoPageOrElse(returnToUrl: St, toDoWhat: LoginReason | U, } +// Constructs a url to an external SSO server to which the browser should be sent +// to log in. Included in this url, is a return-to-url, so, after login, +// the exteral SSO server knows where to send the user next. +// // forTySsoTest: If we're on the Ty SSO test page, and should only generate // a SSO url if Talkyard's own SSO is in use (but not any external OIDC or OAuth2 IDP). // @@ -207,13 +262,13 @@ export function makeSsoUrl(store: Store, returnToUrlMaybeMagicRedir: St, // The SSO endpoint needs to check the return to full URL or origin against a white list // to verify that the request isn't a phishing attack — i.e. someone who sets up a site // that looks exactly like the external website where Single Sign-On happens, - // or looks exactly like the Talkyard forum, and uses $[returnTo...} to redirect + // or looks exactly like the Talkyard forum, and uses ${returnTo...} to redirect // to the phishing site. — That's why the full url and the origin params have // Dangerous in their names. // Usually there'd be just one entry in the "white list", namely the address to the // Talkyard forum. And then, better use `${talkyardPathQueryEscHash}` instead. However, - // can be many Talkyard origins, if there's also a blog with embedded comments, - // or more than one forum, which all use the same SSO login page. + // can be many origins, if there's also a blog with embedded comments (e.g. blog.company.com), + // or more than one forum (e.g. forum.company.com), which all use the same SSO login page. const ssoUrlWithReturn = talkyardSsoUrl ? (talkyardSsoUrl .replace('${talkyardUrlDangerous}', returnToUrl) @@ -229,14 +284,14 @@ export function makeSsoUrl(store: Store, returnToUrlMaybeMagicRedir: St, } -function makeReturnToPageHashForVerifEmail(hash) { +function makeReturnToPageHashForVerifEmail(hash: St): St { // The magic '__Redir...' string tells the server to use the return-to-URL only if it // needs to send an email address verification email (it'd include the return // to URL on a welcome page show via a link in the email). // '__dwHash__' is an encoded hash that won't be lost when included in a GET URL. // The server replaces it with '#' later on. // If we're showing embedded comments in an