Skip to content

fix(oidc): don't trim trailing slash from issuer URL#79

Merged
agjmills merged 7 commits intomainfrom
fix/oidc-issuer-trailing-slash
Apr 5, 2026
Merged

fix(oidc): don't trim trailing slash from issuer URL#79
agjmills merged 7 commits intomainfrom
fix/oidc-issuer-trailing-slash

Conversation

@agjmills
Copy link
Copy Markdown
Owner

@agjmills agjmills commented Apr 5, 2026

Summary

  • Removes strings.TrimRight stripping of trailing slash from the OIDC issuer URL before discovery
  • Authentik's issuer URL includes a trailing slash; trimming it caused a mismatch with the issuer returned in the provider's discovery document, failing initialisation

Test plan

  • Deploy with Authentik as OIDC provider and confirm server starts with OIDC provider initialised log
  • Confirm login flow completes successfully

Summary by CodeRabbit

  • New Features

    • File sharing with public shareable links, optional expiry and usage limits, create/manage/revoke from file view, recipients can download without accounts.
  • UI

    • Added a Sharing panel to the file page and a copy-to-clipboard helper for share URLs.
  • Documentation

    • README updated with sharing API and security notes.
  • Tests

    • End-to-end tests covering share link lifecycle.
  • UX

    • Login/OIDC failures now surface via flash messages and redirect to the login page.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 5, 2026

Warning

Rate limit exceeded

@agjmills has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 58 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 14 minutes and 58 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 41f10d1d-3422-42ce-a506-54a10c95af5b

📥 Commits

Reviewing files that changed from the base of the PR and between 3815a24 and 7618c43.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • go.mod
  • internal/handlers/auth.go
  • internal/handlers/oidc_handler.go
  • internal/handlers/oidc_handler_test.go
  • internal/handlers/share_handler.go
  • internal/handlers/share_handler_test.go
  • internal/oidc/provider.go
📝 Walkthrough

Walkthrough

Adds file-sharing: new ShareLink DB model and migrations, ShareHandler with create/revoke/access endpoints, route registration, UI for managing shares, template helpers, updated README, tests, and minor OIDC and auth/template changes.

Changes

Cohort / File(s) Summary
Database Model & Migration
internal/database/models/models.go, internal/database/db.go
Add ShareLink GORM model (token, file/user FKs, optional ExpiresAt/MaxUses, Uses counter, CreatedAt, soft-delete) and register it in AutoMigrate.
Share Handler & Tests
internal/handlers/share_handler.go, internal/handlers/share_handler_test.go
New ShareHandler with CreateShareLink, RevokeShareLink, AccessShareLink (token generation, ownership checks, expiry/max-uses enforcement with atomic updates, file streaming); comprehensive end-to-end tests covering lifecycle and edge cases.
Routing
internal/routes/routes.go
Register public GET /s/{token} and authenticated POST /files/{id}/share, POST /share/{token}/revoke routes and instantiate ShareHandler.
File View & UI
internal/handlers/file_handler.go, web/templates/file_view.html
File view now queries share links and exposes them to the template; template adds Sharing panel with list, revoke forms, create-share form, and copyShareURL(btn, path) client helper.
Template Utilities
internal/templateutil/templateutil.go
Add DerefInt(*int) int and Today() string template functions and register under "deref" and "today".
Docs & Small Cleanups
README.md, internal/oidc/provider.go, internal/handlers/auth.go, internal/handlers/oidc_handler.go, go.mod
README documents sharing API and semantics; OIDC provider uses issuer verbatim (removes unused import); login handler exposes flash to template; OIDC callback uses flash+redirect on errors; indirect go-jose version bump.

Sequence Diagram

sequenceDiagram
    actor Client as Anonymous Client
    participant ShareHandler as ShareHandler
    participant DB as Database
    participant Storage as Storage Backend
    participant HTTP as HTTP Response

    Client->>ShareHandler: GET /s/{token}
    ShareHandler->>DB: SELECT ShareLink WHERE token = ? PRELOAD File
    DB-->>ShareHandler: ShareLink (or none)
    alt missing/invalid/soft-deleted
        ShareHandler-->>Client: 404 Not Found
    else expired
        ShareHandler-->>Client: 404 Not Found
    else max uses set
        ShareHandler->>DB: UPDATE ... SET uses = uses+1 WHERE id=? AND uses < max_uses
        DB-->>ShareHandler: rowsAffected==0?
        alt exhausted
            ShareHandler-->>Client: 404 Not Found
        else success
            ShareHandler->>Storage: Open(file.Path)
            Storage-->>ShareHandler: Reader, size, content-type
            ShareHandler->>HTTP: Set headers (Content-Type, Content-Length, Content-Disposition)
            ShareHandler->>HTTP: Stream content
            HTTP-->>Client: 200 OK + file bytes
        end
    else unlimited uses
        ShareHandler->>DB: Atomic increment uses
        DB-->>ShareHandler: Success
        ShareHandler->>Storage: Open(file.Path)
        Storage-->>ShareHandler: Reader, size, content-type
        ShareHandler->>HTTP: Set headers and stream
        HTTP-->>Client: 200 OK + file bytes
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I dug a tiny tunnel, bright,

Tokens hide there out of sight.
Share a file, hop quick and free,
Click, copy, download—whee! 🥕✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description follows the template structure with Summary and Test plan sections completed. However, the description does not match the actual scope of the PR, which includes multiple commits covering flash messages, dependency updates, and template changes beyond just the OIDC issuer URL fix. Update the description to clarify the full scope of changes in this PR, including the flash messaging improvements, dependency bump, and file sharing implementation, or confirm that only the OIDC issuer fix should be merged.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix in the PR: removing the trailing slash trimming from the OIDC issuer URL, which is the primary issue addressed in internal/oidc/provider.go.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/oidc-issuer-trailing-slash

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
internal/database/models/models.go (1)

66-80: Consider adding ON DELETE CASCADE constraint for the File foreign key.

The User and Folder models in this file use constraint:OnDelete:CASCADE for their relations. The ShareLink model's File relation doesn't specify this, which means share links may become orphaned if a file is permanently deleted.

♻️ Add cascade delete constraint
-	File File `gorm:"foreignKey:FileID" json:"-"`
-	User User `gorm:"foreignKey:UserID" json:"-"`
+	File File `gorm:"foreignKey:FileID;constraint:OnDelete:CASCADE" json:"-"`
+	User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`

This ensures share links are automatically cleaned up when the referenced file or user is permanently deleted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/database/models/models.go` around lines 66 - 80, The ShareLink
model's File relation lacks a cascade delete constraint so share links can be
orphaned when a File is permanently removed; update the ShareLink struct to add
a GORM constraint on the File relation (e.g., add
`gorm:"foreignKey:FileID;constraint:OnDelete:CASCADE"` to the File field) so
records tied to FileID are removed automatically, and apply the same
`constraint:OnDelete:CASCADE` to the User field if you want share links cleaned
up when users are deleted.
internal/handlers/share_handler.go (1)

99-104: Consider adding a flash message on successful share link creation.

Other handlers in this codebase use flash messages to provide user feedback. The redirect after creating a share link doesn't inform the user that creation succeeded.

♻️ Add success flash message
+	"github.com/agjmills/trove/internal/flash"
 	if err := h.db.Create(&link).Error; err != nil {
 		http.Error(w, "Internal server error", http.StatusInternalServerError)
 		return
 	}

+	flash.Success(w, "Share link created")
 	http.Redirect(w, r, "/files/"+strconv.FormatUint(fileID, 10), http.StatusSeeOther)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handlers/share_handler.go` around lines 99 - 104, The share-link
creation currently redirects after h.db.Create(&link).Error without user
feedback; before calling http.Redirect(w, r,
"/files/"+strconv.FormatUint(fileID, 10), http.StatusSeeOther) add the same
flash-success code used in other handlers (e.g., session.AddFlash(...) or
setFlash(w,r,...) and session.Save or Save(r,w)) so a success message is stored
in the session for the next page render; ensure you call that flash API with a
clear message like "Share link created" and persist the session before
performing the redirect.
internal/handlers/file_handler.go (1)

868-871: Consider filtering out expired and soft-deleted share links.

The current query fetches all share links regardless of expiry or soft-deletion status. Users may see links in the UI that no longer work.

♻️ Optional: Filter active links only
 	// Fetch active share links for this file
 	var shareLinks []models.ShareLink
-	h.db.Where("file_id = ? AND user_id = ?", file.ID, user.ID).Find(&shareLinks)
+	h.db.Where("file_id = ? AND user_id = ? AND deleted_at IS NULL", file.ID, user.ID).
+		Where("expires_at IS NULL OR expires_at > ?", time.Now()).
+		Find(&shareLinks)

Alternatively, if showing expired links is intentional (so users can see history and revoke stale entries), consider adding visual indicators in the template to distinguish expired/exhausted links.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handlers/file_handler.go` around lines 868 - 871, The query that
loads shareLinks currently returns all rows; update the query that populates
shareLinks (the h.db.Where(...).Find(&shareLinks) call in file_handler.go) to
exclude expired and soft-deleted links by adding conditions like "AND
(expires_at IS NULL OR expires_at > ?)" with time.Now() and "AND deleted_at IS
NULL" (or rely on GORM soft-delete behavior and remove Unscoped if present);
pass time.Now() as the argument and ensure the models.ShareLink struct's
DeletedAt field is respected so only active, non-expired links are returned.
web/templates/file_view.html (1)

358-369: Add error handling for clipboard API.

The copyShareURL function doesn't handle the case where clipboard access fails (e.g., non-HTTPS contexts, permission denied). This could leave users without feedback.

♻️ Add error handling for clipboard failure
 	function copyShareURL(btn, path) {
 		const url = window.location.origin + path;
 		navigator.clipboard.writeText(url).then(function() {
 			const original = btn.innerHTML;
 			btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
 			btn.classList.add('text-green-500');
 			setTimeout(function() {
 				btn.innerHTML = original;
 				btn.classList.remove('text-green-500');
 			}, 2000);
+		}).catch(function() {
+			// Fallback: select the input for manual copy
+			const input = btn.parentElement.querySelector('input[readonly]');
+			if (input) {
+				input.select();
+				input.focus();
+			}
 		});
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/templates/file_view.html` around lines 358 - 369, The copyShareURL
function lacks error handling for navigator.clipboard.writeText; wrap the
clipboard call in a promise catch (or use async/await try/catch) inside
copyShareURL and handle failures by restoring the button state and providing
user feedback (e.g., change btn.innerHTML to an error icon/text, add a
'text-red-500' class, and revert after timeout). Also consider a fallback
(prompt with the URL or document.execCommand('copy')) when writeText rejects,
and ensure you reference navigator.clipboard.writeText and the copyShareURL
function when locating the code to update.
internal/handlers/share_handler_test.go (1)

47-47: Fail fast on fixture insert errors.

These db.Create(...) calls ignore .Error; setup can silently fail and produce misleading assertions later. In test helpers, treat fixture creation failures as fatal.

🧪 Suggested patch
-	db.Create(user)
+	if err := db.Create(user).Error; err != nil {
+		t.Fatalf("create user: %v", err)
+	}
...
-	db.Create(f)
+	if err := db.Create(f).Error; err != nil {
+		t.Fatalf("create file: %v", err)
+	}
...
-	db.Create(other)
+	if err := db.Create(other).Error; err != nil {
+		t.Fatalf("create other user: %v", err)
+	}
...
-	db.Create(alice)
+	if err := db.Create(alice).Error; err != nil {
+		t.Fatalf("create alice: %v", err)
+	}
...
-	db.Create(user)
+	if err := db.Create(user).Error; err != nil {
+		t.Fatalf("create user: %v", err)
+	}
...
-	db.Create(link)
+	if err := db.Create(link).Error; err != nil {
+		t.Fatalf("create share link: %v", err)
+	}

Also applies to: 65-65, 161-161, 166-166, 176-176, 207-207

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handlers/share_handler_test.go` at line 47, Fixture inserts using
db.Create(...) in share_handler_test.go currently ignore the returned error;
update each call (e.g., the db.Create(user) and other db.Create(...)
occurrences) to capture the result and fail fast on error—assign the call to a
variable (res := db.Create(obj)), check res.Error, and call t.Fatalf or
t.Helper()+t.Fatalf with a clear message including res.Error if non-nil so test
setup fails immediately instead of producing misleading assertions later.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/handlers/share_handler_test.go`:
- Around line 31-39: The in-memory SQLite used in setupShareTest currently opens
":memory:" which can create separate DBs across connections; change the DSN
passed to sqlite.Open in the setupShareTest function to the shared-cache pattern
(e.g., "file::memory:?cache=shared") so all connections see the same in-memory
database, leaving the rest of the gorm.Open and AutoMigrate logic intact.

In `@internal/handlers/share_handler.go`:
- Around line 164-168: The code increments the share link's use count before
verifying the file exists (link.File.ID and link.File.SoftDeletedAt), which lets
unusable-file requests consume uses; move the file availability check so it runs
before any modification to the link use count and short-circuits with
http.NotFound if the file is trashed or deleted, and then only after confirming
availability perform the use-count increment/update (ideally within the same DB
transaction or with appropriate locking) in the same handler where link and
link.File are accessed.

---

Nitpick comments:
In `@internal/database/models/models.go`:
- Around line 66-80: The ShareLink model's File relation lacks a cascade delete
constraint so share links can be orphaned when a File is permanently removed;
update the ShareLink struct to add a GORM constraint on the File relation (e.g.,
add `gorm:"foreignKey:FileID;constraint:OnDelete:CASCADE"` to the File field) so
records tied to FileID are removed automatically, and apply the same
`constraint:OnDelete:CASCADE` to the User field if you want share links cleaned
up when users are deleted.

In `@internal/handlers/file_handler.go`:
- Around line 868-871: The query that loads shareLinks currently returns all
rows; update the query that populates shareLinks (the
h.db.Where(...).Find(&shareLinks) call in file_handler.go) to exclude expired
and soft-deleted links by adding conditions like "AND (expires_at IS NULL OR
expires_at > ?)" with time.Now() and "AND deleted_at IS NULL" (or rely on GORM
soft-delete behavior and remove Unscoped if present); pass time.Now() as the
argument and ensure the models.ShareLink struct's DeletedAt field is respected
so only active, non-expired links are returned.

In `@internal/handlers/share_handler_test.go`:
- Line 47: Fixture inserts using db.Create(...) in share_handler_test.go
currently ignore the returned error; update each call (e.g., the db.Create(user)
and other db.Create(...) occurrences) to capture the result and fail fast on
error—assign the call to a variable (res := db.Create(obj)), check res.Error,
and call t.Fatalf or t.Helper()+t.Fatalf with a clear message including
res.Error if non-nil so test setup fails immediately instead of producing
misleading assertions later.

In `@internal/handlers/share_handler.go`:
- Around line 99-104: The share-link creation currently redirects after
h.db.Create(&link).Error without user feedback; before calling http.Redirect(w,
r, "/files/"+strconv.FormatUint(fileID, 10), http.StatusSeeOther) add the same
flash-success code used in other handlers (e.g., session.AddFlash(...) or
setFlash(w,r,...) and session.Save or Save(r,w)) so a success message is stored
in the session for the next page render; ensure you call that flash API with a
clear message like "Share link created" and persist the session before
performing the redirect.

In `@web/templates/file_view.html`:
- Around line 358-369: The copyShareURL function lacks error handling for
navigator.clipboard.writeText; wrap the clipboard call in a promise catch (or
use async/await try/catch) inside copyShareURL and handle failures by restoring
the button state and providing user feedback (e.g., change btn.innerHTML to an
error icon/text, add a 'text-red-500' class, and revert after timeout). Also
consider a fallback (prompt with the URL or document.execCommand('copy')) when
writeText rejects, and ensure you reference navigator.clipboard.writeText and
the copyShareURL function when locating the code to update.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93bff949-cb5e-4fe7-aab9-e6b91863824e

📥 Commits

Reviewing files that changed from the base of the PR and between 75b204d and 14bdc81.

📒 Files selected for processing (10)
  • README.md
  • internal/database/db.go
  • internal/database/models/models.go
  • internal/handlers/file_handler.go
  • internal/handlers/share_handler.go
  • internal/handlers/share_handler_test.go
  • internal/oidc/provider.go
  • internal/routes/routes.go
  • internal/templateutil/templateutil.go
  • web/templates/file_view.html

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/handlers/oidc_handler.go (1)

55-57: Consider adding server logs on the remaining silent failure paths.

A few branches now only set flash+redirect without structured logs, which may make incident debugging harder.

Suggested logging additions
 if sessionState == "" || r.URL.Query().Get("state") != sessionState {
+    logger.Error("oidc callback state validation failed")
     flash.Error(w, "Login failed: invalid state parameter.")
     http.Redirect(w, r, "/login", http.StatusSeeOther)
     return
 }

 rawIDToken, ok := token.Extra("id_token").(string)
 if !ok {
+    logger.Error("oidc callback missing id_token in token response")
     flash.Error(w, "Authentication failed: no identity token in response.")
     http.Redirect(w, r, "/login", http.StatusSeeOther)
     return
 }

 if err := idToken.Claims(&rawClaims); err != nil {
+    logger.Error("oidc claims parsing failed", "error", err)
     flash.Error(w, "Authentication failed: could not read identity claims.")
     http.Redirect(w, r, "/login", http.StatusSeeOther)
     return
 }

 if claims.Subject == "" || claims.Email == "" {
+    logger.Error("oidc required claims missing", "subject_present", claims.Subject != "", "email_present", claims.Email != "")
     flash.Error(w, "Authentication failed: missing required claims.")
     http.Redirect(w, r, "/login", http.StatusSeeOther)
     return
 }

 if err := h.sessionManager.RenewToken(r.Context()); err != nil {
+    logger.Error("oidc session renewal failed", "error", err)
     flash.Error(w, "Failed to create session. Please try again.")
     http.Redirect(w, r, "/login", http.StatusSeeOther)
     return
 }

Also applies to: 73-75, 88-90, 95-97, 111-113

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/handlers/oidc_handler.go` around lines 55 - 57, Several branches in
internal/handlers/oidc_handler.go currently only call flash.Error +
http.Redirect (e.g., the blocks containing flash.Error and http.Redirect at the
lines shown and the similar branches at 73-75, 88-90, 95-97, 111-113) and need
structured server logging; for each of those failure paths add a log entry
(using the project's logger or log.Printf) immediately before the flash/redirect
that records the handler name (OIDC callback/handler), the failure reason,
relevant contextual info (request URL, r.RemoteAddr, user/session id if present)
and any error value available (e.g., token exchange error or userinfo error), so
the code paths that call flash.Error(...) then http.Redirect(...) also call
logger.Errorf/log.Printf(...) with the error details.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/handlers/oidc_handler.go`:
- Around line 55-57: Several branches in internal/handlers/oidc_handler.go
currently only call flash.Error + http.Redirect (e.g., the blocks containing
flash.Error and http.Redirect at the lines shown and the similar branches at
73-75, 88-90, 95-97, 111-113) and need structured server logging; for each of
those failure paths add a log entry (using the project's logger or log.Printf)
immediately before the flash/redirect that records the handler name (OIDC
callback/handler), the failure reason, relevant contextual info (request URL,
r.RemoteAddr, user/session id if present) and any error value available (e.g.,
token exchange error or userinfo error), so the code paths that call
flash.Error(...) then http.Redirect(...) also call logger.Errorf/log.Printf(...)
with the error details.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1c00a0bb-d5c4-4714-9335-f1089b2f2a5b

📥 Commits

Reviewing files that changed from the base of the PR and between 14bdc81 and 3815a24.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (3)
  • go.mod
  • internal/handlers/auth.go
  • internal/handlers/oidc_handler.go
✅ Files skipped from review due to trivial changes (1)
  • go.mod

agjmills added 7 commits April 5, 2026 19:23
Authentik's issuer URL includes a trailing slash; trimming it caused a
mismatch between the URL passed to go-oidc and the issuer returned by
the provider's discovery document.
- Add structured logging to OIDC callback error paths
- Check file availability before incrementing share link use count
- Use file::memory:?cache=shared DSN in share handler tests
@agjmills agjmills force-pushed the fix/oidc-issuer-trailing-slash branch from 8186c37 to 7618c43 Compare April 5, 2026 19:23
@agjmills agjmills merged commit dfee7ec into main Apr 5, 2026
5 checks passed
@agjmills agjmills deleted the fix/oidc-issuer-trailing-slash branch April 5, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant