Skip to content

refactor: rework app logging, dependency injection and cancellation#844

Merged
steveiliop56 merged 22 commits intomainfrom
refactor/app-state
May 10, 2026
Merged

refactor: rework app logging, dependency injection and cancellation#844
steveiliop56 merged 22 commits intomainfrom
refactor/app-state

Conversation

@steveiliop56
Copy link
Copy Markdown
Member

@steveiliop56 steveiliop56 commented May 8, 2026


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Summary by CodeRabbit

  • New Features

    • Added ConcurrentListenersEnabled server option.
    • TOTP generation now shows clearer secret/QR instructions.
  • Refactor

    • Major bootstrap/services/controllers/middleware restructuring for improved startup, lifecycle, and configuration handling.
    • Unified logging subsystem with structured application and audit logging; logging behavior and messages standardized across CLI and web endpoints.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f11bd23f-c7f2-4fbc-9d5e-425e05eff448

📥 Commits

Reviewing files that changed from the base of the PR and between e739aa8 and d387847.

📒 Files selected for processing (1)
  • internal/bootstrap/app_bootstrap.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/bootstrap/app_bootstrap.go

📝 Walkthrough

Walkthrough

Migrates logging to a new internal logger, introduces RuntimeConfig and sentinel errors, refactors bootstrap/services/controllers/middleware to dependency-injected constructors with context/WaitGroup lifecycles, updates CLI logging calls, and standardizes tests with shared helpers and require assertions.

Changes

Logger, runtime, and contracts

Layer / File(s) Summary
Models and Logger Contracts
internal/utils/logger/*, internal/model/*, removed internal/utils/tlog/*
Adds Logger with audit/HTTP/App streams and tests; introduces RuntimeConfig, Provider, and ErrUserContextNotFound; adds Server.ConcurrentListenersEnabled; removes tlog implementation and tests.

Services

Layer / File(s) Summary
Constructors and Lifecycles
internal/service/*, internal/bootstrap/service_bootstrap.go
Services now accept *logger.Logger, model.Config, model.RuntimeConfig, context.Context, and *sync.WaitGroup; drop Init/IsConfigured patterns; background goroutines started via WaitGroup; Docker/Kubernetes/LDAP/OAuth/OIDC/Auth refactored.

Controllers

Layer / File(s) Summary
DI and Route Registration
internal/controller/*
Controllers accept injected logger/runtime/config, register routes in constructors (remove SetupRoutes), and update handlers to use runtime cookie/domain names and controller-owned logging.

Middleware

Layer / File(s) Summary
Injection and Behavior
internal/middleware/*
Context/Zerolog middleware accept injected *logger.Logger and model.RuntimeConfig, remove Init methods; NewUIMiddleware() returns error on asset load; request/session behavior preserved but logs use injected logger.

Bootstrap

Layer / File(s) Summary
App/DB/Router Wiring
internal/bootstrap/*
BootstrapApp stores runtime, ctx/cancel, db, queries, router, wg, and log; Setup() initializes logger/runtime, runs SetupDatabase(), constructs services/controllers with DI, installs router, and runs HTTP/Unix servers tied to app context; dbCleanup and heartbeat routines updated.

CLI

Layer / File(s) Summary
Logger Migration
cmd/tinyauth/*
CLI commands switch from tlog to internal/utils/logger initialization and update log calls; startup tlog init removed in main entrypoint.

Tests

Layer / File(s) Summary
Helpers and Updates
internal/test/test.go, **/*_test.go
Adds CreateTestConfigs and TestingTOTPSecret; tests initialize logger.NewLogger().WithTestConfig().Init(), use bootstrap DB via app.SetupDatabase()/app.GetDB(), adapt to new constructors, and replace many assert calls with require for fail-fast checks.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Rycochet
  • scottmckendry

"A rabbit logs with ears held high,
Old tunnels closed, new paths apply.
Contexts hum and services sing,
Tests hop tight as changes spring.
We thump a beat and ship the code—hooray! 🥕"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/app-state

Comment thread internal/service/oidc_service.go Fixed
@steveiliop56 steveiliop56 marked this pull request as ready for review May 9, 2026 11:03
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label May 9, 2026
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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
internal/bootstrap/db_bootstrap.go (1)

24-58: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

*sql.DB is leaked on every error path after sql.Open.

If iofs.New, sqlite3.WithInstance, migrate.NewWithInstance, or migrator.Up fails, the function returns without calling db.Close(). The connection pool (and underlying file handle in modernc/sqlite) is not released, and on retry/restart loops this can accumulate. Defer-close on the *sql.DB until success, then transfer ownership to app.db.

🛡️ Proposed fix
 	db, err := sql.Open("sqlite", app.config.Database.Path)
-
 	if err != nil {
 		return fmt.Errorf("failed to open database: %w", err)
 	}
 
+	// Ensure the connection is released on any error path below.
+	success := false
+	defer func() {
+		if !success {
+			_ = db.Close()
+		}
+	}()
+
 	// Limit to 1 connection to sequence writes, this may need to be revisited in the future
 	// if the sqlite connection starts being a bottleneck
 	db.SetMaxOpenConns(1)
 
 	migrations, err := iofs.New(assets.Migrations, "migrations")
-
 	if err != nil {
 		return fmt.Errorf("failed to create migrations: %w", err)
 	}
 
 	target, err := sqlite3.WithInstance(db, &sqlite3.Config{})
-
 	if err != nil {
 		return fmt.Errorf("failed to create sqlite3 instance: %w", err)
 	}
 
 	migrator, err := migrate.NewWithInstance("iofs", migrations, "sqlite3", target)
-
 	if err != nil {
 		return fmt.Errorf("failed to create migrator: %w", err)
 	}
 
-	if err := migrator.Up(); err != nil && err != migrate.ErrNoChange {
+	if err := migrator.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
 		return fmt.Errorf("failed to migrate database: %w", err)
 	}
 
 	app.db = db
+	success = true
 	return nil
 }

(Also switched the migrate.ErrNoChange check to errors.Is for wrap-safety — minor.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/bootstrap/db_bootstrap.go` around lines 24 - 58, The *sql.DB opened
by sql.Open must be closed on all error paths until we successfully attach it to
the app; modify the db_bootstrap logic so that immediately after db, err :=
sql.Open(...) you schedule a deferred close (e.g., defer func() { if !success {
db.Close() } }()) or similar, set a local success flag only after all steps
(iofs.New, sqlite3.WithInstance, migrate.NewWithInstance, migrator.Up) succeed,
then assign app.db = db and flip the flag to avoid closing the live DB; also
replace the direct equality check for migrate.ErrNoChange with errors.Is(err,
migrate.ErrNoChange) when evaluating migrator.Up() errors to preserve wrapped
errors.
internal/controller/oauth_controller.go (1)

134-277: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update oauth_controller redirects to use runtime.AppURL for consistency.

oauth_controller.go uses controller.config.AppURL for all /error, /login, /unauthorized, /authorize, and /continue redirects, while proxy_controller.go, context_controller.go, and tests consistently use controller.runtime.AppURL. Since AppURL exists in both Config and RuntimeConfig, and the broader codebase standardizes on runtime.AppURL, migrate all 11+ redirect calls in this method to controller.runtime.AppURL to maintain a single source of truth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/oauth_controller.go` around lines 134 - 277, The redirect
URLs in the OAuth callback flow use controller.config.AppURL but must use the
canonical controller.runtime.AppURL; update every redirect call in the OAuth
handler (references around oauthPendingSession, the state/checks, CreateSession
and the final redirects) to replace controller.config.AppURL with
controller.runtime.AppURL for all paths (/error, /unauthorized, /authorize,
/continue, and the base AppURL) so the controller uses the runtime single source
of truth.
internal/controller/oidc_controller.go (2)

469-513: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

authorizeError still assumes controller.oidc is non-nil.

Authorize now calls this helper for the "OIDC not configured" path with an empty callback, but the fallback branch still uses controller.oidc.GetIssuer(). That turns the new guard into a nil-pointer panic instead of an error response.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/oidc_controller.go` around lines 469 - 513,
authorizeError assumes controller.oidc is non-nil and calls
controller.oidc.GetIssuer() in the fallback branch, which can panic when
authorize calls this helper for the "OIDC not configured" path; update
authorizeError to nil-check controller.oidc before calling GetIssuer and use a
safe fallback (e.g. a predefined error URL or an empty string) or return an
appropriate error response when controller.oidc is nil; locate the logic in
authorizeError and change the fallback that builds the redirect_uri (where it
creates ErrorScreen, encodes queries and uses controller.oidc.GetIssuer()) to
first verify controller.oidc != nil and handle the nil case without
dereferencing it.

77-90: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard GetClientInfo when OIDC is disabled.

This route is still registered even when oidcService is nil, and controller.oidc.GetClient(...) will panic in that case. Please short-circuit with the same disabled/not-found response used in Token and Userinfo.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/oidc_controller.go` around lines 77 - 90, GetClientInfo
must guard against a nil OIDC service to avoid panics: in the
OIDCController.GetClientInfo method, check if controller.oidc == nil (the same
guard used in Token and Userinfo) and return the same disabled/not-found JSON
response before calling controller.oidc.GetClient(req.ClientID); update the
method to short-circuit when oidc is nil so controller.oidc.GetClient(...) is
never invoked on a nil receiver.
🧹 Nitpick comments (6)
internal/model/context_test.go (1)

234-242: 💤 Low value

Prefer errors.Is for sentinel error assertion.

Comparing err.Error() against model.ErrUserContextNotFound.Error() works for now, but it bypasses the actual benefit of a sentinel (wrap-safe identity checks via errors.Is). Consider adding a dedicated assertion path for this case, or asserting with errors.Is directly inside the run closure and returning a bool.

Example sketch (separate from the table-based cases)
t.Run("NewFromGin returns ErrUserContextNotFound when context value is missing", func(t *testing.T) {
    var c model.UserContext
    _, err := c.NewFromGin(newGinCtx(nil, false))
    require.ErrorIs(t, err, model.ErrUserContextNotFound)
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/model/context_test.go` around lines 234 - 242, The test case
currently compares error strings; change it to use errors.Is (or
require.ErrorIs) so sentinel comparisons are wrap-safe: inside the table case
for the NewFromGin scenario (or in a dedicated t.Run), have the run closure call
c.NewFromGin(newGinCtx(nil, false)), check err with errors.Is(err,
model.ErrUserContextNotFound) (or use require.ErrorIs) and return a bool (or
make the test use require.ErrorIs directly) instead of returning err.Error();
reference model.UserContext.NewFromGin and model.ErrUserContextNotFound when
updating the test.
internal/utils/logger/logger.go (1)

123-155: CallerSkipFrame(1) is appropriate for current audit method usage patterns.

All audit method calls (AuditLoginSuccess, AuditLoginFailure, AuditLogout) are direct invocations from handler methods in controllers. The concern about future wrapping (e.g., middleware, service helpers) is architecturally valid; if audit logging is later extracted into shared helpers, the caller frame offset will need adjustment. Consider documenting this assumption or revisiting the implementation strategy if the codebase introduces audit logging wrappers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/utils/logger/logger.go` around lines 123 - 155, The audit methods
AuditLoginSuccess, AuditLoginFailure and AuditLogout rely on CallerSkipFrame(1)
assuming they are called directly from controller handlers; add a short inline
comment/TODO above these methods documenting this assumption and that the
CallerSkipFrame value must be increased if audit logging is moved into
middleware/wrappers or helper functions, so future refactors adjust
CallerSkipFrame accordingly (or make the skip configurable at that time).
internal/service/ldap_service.go (1)

253-278: 💤 Low value

Pass ldap.context to backoff.Retry so reconnect honors shutdown.

backoff.Retry(context.TODO(), ...) ignores the service context, so an in-flight reconnect (up to 3 tries × multiplier 1.5 starting at 500 ms) keeps running after ctx is cancelled, delaying clean shutdown. The reconnect routine is the only path here, and it's already invoked from the heartbeat goroutine that owns ldap.context.

♻️ Proposed change
-	_, err := backoff.Retry(context.TODO(), operation, backoff.WithBackOff(exp), backoff.WithMaxTries(3))
+	_, err := backoff.Retry(ldap.context, operation, backoff.WithBackOff(exp), backoff.WithMaxTries(3))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/ldap_service.go` around lines 253 - 278, The reconnect
function currently calls backoff.Retry with context.TODO(), which prevents the
retry loop from honoring shutdown; update the call in LdapService.reconnect to
pass the service context (ldap.context) instead of context.TODO() so retries are
cancelled when the service context is done; locate the backoff.Retry invocation
in reconnect and replace the context argument with ldap.context (no other
behavioral changes required).
internal/service/access_controls_service.go (1)

14-65: ⚡ Quick win

Drop the pointer-to-interface for labelProvider.

*LabelProvider is a pointer to an interface, which is an antipattern in Go: interface values are already nil-able and carry their own indirection, so wrapping them in *T adds unnecessary overhead and forces awkward (*acls.labelProvider).GetLabels(domain) call sites. Removing the pointer wrapper allows the bootstrap code to pass the interface value directly instead of &labelProvider, and the nil check remains unchanged.

♻️ Proposed change
 type AccessControlsService struct {
 	log           *logger.Logger
-	labelProvider *LabelProvider
+	labelProvider LabelProvider
 	static        map[string]model.App
 }

 func NewAccessControlsService(
 	log *logger.Logger,
-	labelProvider *LabelProvider,
+	labelProvider LabelProvider,
 	static        map[string]model.App) *AccessControlsService {
 	return &AccessControlsService{
 		log:           log,
 		labelProvider: labelProvider,
 		static:        static,
 	}
 }
 	// If we have a label provider configured, try to get ACLs from it
 	if acls.labelProvider != nil {
-		return (*acls.labelProvider).GetLabels(domain)
+		return acls.labelProvider.GetLabels(domain)
 	}

Update the bootstrap call site at internal/bootstrap/service_bootstrap.go:48 from &labelProvider to labelProvider.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/access_controls_service.go` around lines 14 - 65, The
AccessControlsService currently stores labelProvider as a pointer-to-interface
(*LabelProvider); change the field type in AccessControlsService to
LabelProvider (remove the *), update NewAccessControlsService signature to
accept labelProvider LabelProvider, and update all uses: in GetAccessControls
call labelProvider.GetLabels(domain) (remove the (*...).GetLabels dereference)
and keep the nil check as before (if acls.labelProvider != nil). Also update the
bootstrap/constructor call site where NewAccessControlsService is invoked to
pass labelProvider (not &labelProvider).
internal/controller/user_controller_test.go (1)

29-30: ⚡ Quick win

Finish the runtime-driven migration for cookie-domain assertions.

This test now builds runtime, but several cookie checks later in the table still hardcode "example.com". Switching those expectations to runtime.CookieDomain will keep the suite aligned with createTestConfigs instead of the helper’s current default.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/user_controller_test.go` around lines 29 - 30, The tests
in user_controller_test.go build a runtime via createTestConfigs(t) but still
assert cookie domain strings using the hardcoded "example.com"; update those
assertions to use runtime.CookieDomain instead so expectations follow the test
runtime configuration. Locate assertions in the test table that compare or
expect cookie domains (e.g., header/cookie checks) and replace the literal
"example.com" with runtime.CookieDomain, ensuring any related helper calls or
expected values reference runtime.CookieDomain consistently.
internal/controller/proxy_controller_test.go (1)

24-25: ⚡ Quick win

Use runtime.AppURL for expected redirect URLs.

This test is now config-driven, but the expected login redirects still embed https://tinyauth.example.com. Deriving those expectations from runtime.AppURL will keep the assertions synchronized with createTestConfigs and avoid unrelated breakage when the helper changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/proxy_controller_test.go` around lines 24 - 25, The test
currently asserts hardcoded redirect URLs ("https://tinyauth.example.com...")
even though createTestConfigs(t) returns a runtime with the authoritative
AppURL; update the assertions to derive expected redirect URLs from
runtime.AppURL (returned by createTestConfigs) instead of the literal string.
Locate the test uses around cfg, runtime := createTestConfigs(t) and replace
occurrences of the hardcoded base URL with runtime.AppURL (e.g., build expected
strings with runtime.AppURL + path or fmt.Sprintf("%s%s", runtime.AppURL,
"/login") ) so the expected redirects stay in sync with the test config.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/bootstrap/app_bootstrap.go`:
- Around line 278-290: When a listener error is received from errChan, initiate
the normal shutdown before returning the error: invoke the application's cancel
function (call the app's cancel method/func used to cancel app.ctx), wait for
background goroutines to finish (app.wg.Wait()), perform the DB close/logging
path (app.db.Close() and the existing log messages), and only then return
fmt.Errorf("server error: %w", err); replace the current immediate return inside
the case err := <-errChan block with this shutdown sequence using the same
symbols (app.ctx, app.cancel(), app.wg.Wait(), app.db.Close(), errChan).
- Around line 305-308: The shutdown goroutines currently call server.Close(),
which drops active connections; replace those calls in serveHTTP() and
serveUnix() with server.Shutdown(ctx) using a cancellable context (e.g.,
context.WithTimeout(app.ctx, <reasonable timeout>)) so in-flight requests drain
gracefully, handle and log any Shutdown error via app.log.App, and ensure you
cancel the timeout context after Shutdown returns; update the goroutine bodies
that reference server.Close() to create the timeout context, call
server.Shutdown(timeoutCtx), log errors, and defer timeout cancel().

In `@internal/controller/controller_test.go`:
- Around line 14-106: Extract the duplicated createTestConfigs helper into a
shared test package (e.g., package testutil) and export it as
CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) plus export
the TOTP secret as TestingTOTPSecret; move the full implementation (including
bcrypt.GenerateFromPassword, model.Config, model.RuntimeConfig, OIDC client
conversion, CookieDomain/AppURL, etc.) into internal/testutil/config.go, update
internal/controller/controller_test.go and
internal/middleware/middleware_test.go to call testutil.CreateTestConfigs(t) and
replace testingTOTPSecret with testutil.TestingTOTPSecret, and add the new
import for the testutil package in both test files.

In `@internal/controller/oauth_controller.go`:
- Around line 166-172: GetOAuthUserinfo's error is ignored and user may be nil,
causing a panic when accessing user.Email; change the handler around
controller.auth.GetOAuthUserinfo(sessionIdCookie) to check err (and that user !=
nil) immediately after the call, log the error with controller.log.App (use
Warn/Error) and perform the same redirect to fmt.Sprintf("%s/error",
controller.config.AppURL) and return if there is an error or nil user before
accessing user.Email; update any existing controller.log.App.Warn("OAuth
provider did not return an email") usage to only run when user is non-nil but
Email is empty.

In `@internal/controller/well_known_controller_test.go`:
- Around line 101-102: The test calls service.NewOIDCService(log, cfg, runtime,
queries, ctx, wg) and continues even if it returns an error; immediately after
the call (where oidcService, err := service.NewOIDCService(...) is assigned) add
a require.NoError(t, err) to fail fast on setup errors so the test stops before
registering routes or exercising handlers when oidcService construction failed.

In `@internal/service/docker_service.go`:
- Line 53: The call wg.Go(service.watchAndClose) is invalid because
sync.WaitGroup has no Go method; update the caller and function: either change
the parameter type from *sync.WaitGroup to *errgroup.Group (import
golang.org/x/sync/errgroup), change service.watchAndClose signature to return
error and keep wg.Go(service.watchAndClose), or keep sync.WaitGroup and replace
the call with the standard pattern wg.Add(1); go func() { defer wg.Done(); _ =
service.watchAndClose() } (or handle its returned error inside the goroutine if
you alter its signature to return error); reference the symbols wg and
service.watchAndClose when making the change.

In `@internal/service/oidc_service_test.go`:
- Around line 65-76: The test builds a RuntimeConfig without any OIDCClients so
NewOIDCService returns (nil, nil) and the subsequent CompileUserinfo call runs
on a nil receiver; populate runtime.OIDCClients with at least one valid client
entry (or otherwise provide cfg values that cause NewOIDCService to construct a
non-nil service) before calling service.NewOIDCService, then assert svc is
non-nil (e.g., require.NotNil(t, svc)) so CompileUserinfo is invoked on a real
OIDCService instance; refer to RuntimeConfig.OIDCClients, NewOIDCService, and
CompileUserinfo to locate and fix the test setup.

In `@internal/service/oidc_service.go`:
- Around line 789-800: The loop over expiredCodes uses GetOidcTokenBySub and
logs non-sql.ErrNoRows errors but then continues using the zero-value token,
which can trigger DeleteOldSession incorrectly; update the error handling inside
the loop (the block around GetOidcTokenBySub in the same function) so that after
logging any non-sql.ErrNoRows error you skip to the next expiredCode (e.g.,
continue the loop) instead of proceeding to check token.TokenExpiresAt and
calling DeleteOldSession for that subject.

In `@internal/utils/logger/logger_test.go`:
- Line 165: The test calls assert.NotContains with arguments reversed; change
the call to use the haystack first and needle second so that
assert.NotContains(t, buf.String(), "test_nop") is used instead of
assert.NotContains(t, "test_nop", buf.String()); update the assertion in
logger_test.go to pass buf.String() as the second parameter and "test_nop" as
the third.

In `@internal/utils/logger/logger.go`:
- Line 36: Update the comment in internal/utils/logger/logger.go that currently
reads "No reason to enabled audit by default since it will be suppressed by the
log level" to correct the grammar by replacing "enabled" with "enable" so it
reads "No reason to enable audit by default since it will be suppressed by the
log level"; locate the comment near the audit-related logging logic in the
logger implementation to ensure the change is applied to the right comment.

---

Outside diff comments:
In `@internal/bootstrap/db_bootstrap.go`:
- Around line 24-58: The *sql.DB opened by sql.Open must be closed on all error
paths until we successfully attach it to the app; modify the db_bootstrap logic
so that immediately after db, err := sql.Open(...) you schedule a deferred close
(e.g., defer func() { if !success { db.Close() } }()) or similar, set a local
success flag only after all steps (iofs.New, sqlite3.WithInstance,
migrate.NewWithInstance, migrator.Up) succeed, then assign app.db = db and flip
the flag to avoid closing the live DB; also replace the direct equality check
for migrate.ErrNoChange with errors.Is(err, migrate.ErrNoChange) when evaluating
migrator.Up() errors to preserve wrapped errors.

In `@internal/controller/oauth_controller.go`:
- Around line 134-277: The redirect URLs in the OAuth callback flow use
controller.config.AppURL but must use the canonical controller.runtime.AppURL;
update every redirect call in the OAuth handler (references around
oauthPendingSession, the state/checks, CreateSession and the final redirects) to
replace controller.config.AppURL with controller.runtime.AppURL for all paths
(/error, /unauthorized, /authorize, /continue, and the base AppURL) so the
controller uses the runtime single source of truth.

In `@internal/controller/oidc_controller.go`:
- Around line 469-513: authorizeError assumes controller.oidc is non-nil and
calls controller.oidc.GetIssuer() in the fallback branch, which can panic when
authorize calls this helper for the "OIDC not configured" path; update
authorizeError to nil-check controller.oidc before calling GetIssuer and use a
safe fallback (e.g. a predefined error URL or an empty string) or return an
appropriate error response when controller.oidc is nil; locate the logic in
authorizeError and change the fallback that builds the redirect_uri (where it
creates ErrorScreen, encodes queries and uses controller.oidc.GetIssuer()) to
first verify controller.oidc != nil and handle the nil case without
dereferencing it.
- Around line 77-90: GetClientInfo must guard against a nil OIDC service to
avoid panics: in the OIDCController.GetClientInfo method, check if
controller.oidc == nil (the same guard used in Token and Userinfo) and return
the same disabled/not-found JSON response before calling
controller.oidc.GetClient(req.ClientID); update the method to short-circuit when
oidc is nil so controller.oidc.GetClient(...) is never invoked on a nil
receiver.

---

Nitpick comments:
In `@internal/controller/proxy_controller_test.go`:
- Around line 24-25: The test currently asserts hardcoded redirect URLs
("https://tinyauth.example.com...") even though createTestConfigs(t) returns a
runtime with the authoritative AppURL; update the assertions to derive expected
redirect URLs from runtime.AppURL (returned by createTestConfigs) instead of the
literal string. Locate the test uses around cfg, runtime := createTestConfigs(t)
and replace occurrences of the hardcoded base URL with runtime.AppURL (e.g.,
build expected strings with runtime.AppURL + path or fmt.Sprintf("%s%s",
runtime.AppURL, "/login") ) so the expected redirects stay in sync with the test
config.

In `@internal/controller/user_controller_test.go`:
- Around line 29-30: The tests in user_controller_test.go build a runtime via
createTestConfigs(t) but still assert cookie domain strings using the hardcoded
"example.com"; update those assertions to use runtime.CookieDomain instead so
expectations follow the test runtime configuration. Locate assertions in the
test table that compare or expect cookie domains (e.g., header/cookie checks)
and replace the literal "example.com" with runtime.CookieDomain, ensuring any
related helper calls or expected values reference runtime.CookieDomain
consistently.

In `@internal/model/context_test.go`:
- Around line 234-242: The test case currently compares error strings; change it
to use errors.Is (or require.ErrorIs) so sentinel comparisons are wrap-safe:
inside the table case for the NewFromGin scenario (or in a dedicated t.Run),
have the run closure call c.NewFromGin(newGinCtx(nil, false)), check err with
errors.Is(err, model.ErrUserContextNotFound) (or use require.ErrorIs) and return
a bool (or make the test use require.ErrorIs directly) instead of returning
err.Error(); reference model.UserContext.NewFromGin and
model.ErrUserContextNotFound when updating the test.

In `@internal/service/access_controls_service.go`:
- Around line 14-65: The AccessControlsService currently stores labelProvider as
a pointer-to-interface (*LabelProvider); change the field type in
AccessControlsService to LabelProvider (remove the *), update
NewAccessControlsService signature to accept labelProvider LabelProvider, and
update all uses: in GetAccessControls call labelProvider.GetLabels(domain)
(remove the (*...).GetLabels dereference) and keep the nil check as before (if
acls.labelProvider != nil). Also update the bootstrap/constructor call site
where NewAccessControlsService is invoked to pass labelProvider (not
&labelProvider).

In `@internal/service/ldap_service.go`:
- Around line 253-278: The reconnect function currently calls backoff.Retry with
context.TODO(), which prevents the retry loop from honoring shutdown; update the
call in LdapService.reconnect to pass the service context (ldap.context) instead
of context.TODO() so retries are cancelled when the service context is done;
locate the backoff.Retry invocation in reconnect and replace the context
argument with ldap.context (no other behavioral changes required).

In `@internal/utils/logger/logger.go`:
- Around line 123-155: The audit methods AuditLoginSuccess, AuditLoginFailure
and AuditLogout rely on CallerSkipFrame(1) assuming they are called directly
from controller handlers; add a short inline comment/TODO above these methods
documenting this assumption and that the CallerSkipFrame value must be increased
if audit logging is moved into middleware/wrappers or helper functions, so
future refactors adjust CallerSkipFrame accordingly (or make the skip
configurable at that time).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 24420aa6-3528-4ecd-8954-61373dfb1aab

📥 Commits

Reviewing files that changed from the base of the PR and between 1b18e68 and 548d97f.

📒 Files selected for processing (51)
  • cmd/tinyauth/create_user.go
  • cmd/tinyauth/generate_totp.go
  • cmd/tinyauth/healthcheck.go
  • cmd/tinyauth/tinyauth.go
  • cmd/tinyauth/verify_user.go
  • internal/bootstrap/app_bootstrap.go
  • internal/bootstrap/db_bootstrap.go
  • internal/bootstrap/router_bootstrap.go
  • internal/bootstrap/service_bootstrap.go
  • internal/controller/context_controller.go
  • internal/controller/context_controller_test.go
  • internal/controller/controller_test.go
  • internal/controller/health_controller.go
  • internal/controller/health_controller_test.go
  • internal/controller/oauth_controller.go
  • internal/controller/oidc_controller.go
  • internal/controller/oidc_controller_test.go
  • internal/controller/proxy_controller.go
  • internal/controller/proxy_controller_test.go
  • internal/controller/resources_controller.go
  • internal/controller/resources_controller_test.go
  • internal/controller/user_controller.go
  • internal/controller/user_controller_test.go
  • internal/controller/well_known_controller.go
  • internal/controller/well_known_controller_test.go
  • internal/middleware/context_middleware.go
  • internal/middleware/context_middleware_test.go
  • internal/middleware/middleware_test.go
  • internal/middleware/ui_middleware.go
  • internal/middleware/zerolog_middleware.go
  • internal/model/config.go
  • internal/model/context.go
  • internal/model/context_test.go
  • internal/model/runtime.go
  • internal/service/access_controls_service.go
  • internal/service/auth_service.go
  • internal/service/docker_service.go
  • internal/service/kubernetes_service.go
  • internal/service/kubernetes_service_test.go
  • internal/service/ldap_service.go
  • internal/service/oauth_broker_service.go
  • internal/service/oauth_presets.go
  • internal/service/oauth_service.go
  • internal/service/oidc_service.go
  • internal/service/oidc_service_test.go
  • internal/utils/app_utils.go
  • internal/utils/logger/logger.go
  • internal/utils/logger/logger_test.go
  • internal/utils/tlog/log_audit.go
  • internal/utils/tlog/log_wrapper.go
  • internal/utils/tlog/log_wrapper_test.go
💤 Files with no reviewable changes (5)
  • internal/utils/tlog/log_wrapper_test.go
  • internal/utils/app_utils.go
  • internal/utils/tlog/log_audit.go
  • cmd/tinyauth/tinyauth.go
  • internal/utils/tlog/log_wrapper.go

Comment thread internal/bootstrap/app_bootstrap.go
Comment thread internal/bootstrap/app_bootstrap.go Outdated
Comment thread internal/controller/controller_test.go Outdated
Comment thread internal/controller/oauth_controller.go
Comment thread internal/controller/well_known_controller_test.go
Comment thread internal/service/docker_service.go
Comment thread internal/service/oidc_service_test.go
Comment thread internal/service/oidc_service.go
Comment thread internal/utils/logger/logger_test.go Outdated
Comment thread internal/utils/logger/logger.go Outdated
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 (1)
internal/controller/oidc_controller.go (1)

146-150: ⚡ Quick win

Pass a proper error instead of nil to authorizeError.

At line 149, err is from the BindJSON call at line 140, which succeeded (otherwise the function would have returned at line 143). Passing nil or an unrelated error to authorizeError reduces log quality.

🔧 Suggested fix
 	client, ok := controller.oidc.GetClient(req.ClientID)

 	if !ok {
-		controller.authorizeError(c, err, "Client not found", "The client ID is invalid", "", "", "")
+		controller.authorizeError(c, errors.New("client_not_found"), "Client not found", "The client ID is invalid", "", "", "")
 		return
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/oidc_controller.go` around lines 146 - 150, When
GetClient returns ok == false, don't pass the unrelated/previous err to
authorizeError; create and pass a new descriptive error (e.g.,
errors.New("client not found") or fmt.Errorf with the req.ClientID) so logs show
the real cause. Update the call in the block after
controller.oidc.GetClient(req.ClientID) to construct a proper error value and
pass it into controller.authorizeError along with the existing message
parameters to improve diagnostics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/test/test.go`:
- Around line 36-37: Replace uses of path.Join with filepath.Join for filesystem
paths created from t.TempDir(); specifically update the assignments to
PrivateKeyPath and PublicKeyPath (and the other occurrences noted) so they use
filepath.Join(tempDir, "...") instead of path.Join to ensure correct path
separators on Windows and other platforms; import "path/filepath" if not already
present and remove or keep "path" only if still needed for non-filesystem joins.
- Around line 95-102: The OIDCClients slice is built by ranging over
config.OIDC.Clients (a map) which is non-deterministic; change the
implementation in the OIDCClients anonymous constructor to collect and sort the
map keys (e.g., into a []string), then iterate the sorted keys to set client.ID
and append model.OIDCClientConfig entries so the resulting slice order is
stable; update the code that references OIDCClients to rely on this
deterministic ordering if needed.

---

Nitpick comments:
In `@internal/controller/oidc_controller.go`:
- Around line 146-150: When GetClient returns ok == false, don't pass the
unrelated/previous err to authorizeError; create and pass a new descriptive
error (e.g., errors.New("client not found") or fmt.Errorf with the req.ClientID)
so logs show the real cause. Update the call in the block after
controller.oidc.GetClient(req.ClientID) to construct a proper error value and
pass it into controller.authorizeError along with the existing message
parameters to improve diagnostics.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a8ae39a4-d99c-4ed7-b665-543530ef2213

📥 Commits

Reviewing files that changed from the base of the PR and between 548d97f and d500907.

📒 Files selected for processing (16)
  • internal/bootstrap/app_bootstrap.go
  • internal/bootstrap/db_bootstrap.go
  • internal/bootstrap/router_bootstrap.go
  • internal/controller/context_controller_test.go
  • internal/controller/oauth_controller.go
  • internal/controller/oidc_controller.go
  • internal/controller/oidc_controller_test.go
  • internal/controller/proxy_controller_test.go
  • internal/controller/resources_controller_test.go
  • internal/controller/user_controller_test.go
  • internal/controller/well_known_controller_test.go
  • internal/middleware/context_middleware_test.go
  • internal/service/oidc_service.go
  • internal/test/test.go
  • internal/utils/logger/logger.go
  • internal/utils/logger/logger_test.go
🚧 Files skipped from review as they are similar to previous changes (14)
  • internal/controller/resources_controller_test.go
  • internal/utils/logger/logger_test.go
  • internal/controller/well_known_controller_test.go
  • internal/bootstrap/db_bootstrap.go
  • internal/bootstrap/router_bootstrap.go
  • internal/controller/proxy_controller_test.go
  • internal/controller/oauth_controller.go
  • internal/controller/context_controller_test.go
  • internal/middleware/context_middleware_test.go
  • internal/controller/oidc_controller_test.go
  • internal/utils/logger/logger.go
  • internal/controller/user_controller_test.go
  • internal/service/oidc_service.go
  • internal/bootstrap/app_bootstrap.go

Comment thread internal/test/test.go Outdated
Comment thread internal/test/test.go
Rycochet
Rycochet previously approved these changes May 9, 2026
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label May 9, 2026
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.

♻️ Duplicate comments (1)
internal/test/test.go (1)

95-102: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make OIDCClients construction deterministic (Line 97).

Ranging over a map produces non-deterministic order, so this can introduce test flakiness when runtime.OIDCClients is compared or serialized. Build the slice from sorted keys.

Suggested patch
 import (
+	"sort"
 	"path/filepath"
 	"testing"
@@
 		OIDCClients: func() []model.OIDCClientConfig {
-			var clients []model.OIDCClientConfig
-			for id, client := range config.OIDC.Clients {
+			keys := make([]string, 0, len(config.OIDC.Clients))
+			for id := range config.OIDC.Clients {
+				keys = append(keys, id)
+			}
+			sort.Strings(keys)
+
+			clients := make([]model.OIDCClientConfig, 0, len(keys))
+			for _, id := range keys {
+				client := config.OIDC.Clients[id]
 				client.ID = id
 				clients = append(clients, client)
 			}
 			return clients
 		}(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/test/test.go` around lines 95 - 102, The OIDCClients slice is built
by ranging over the map config.OIDC.Clients which yields non-deterministic
order; to fix, collect the map keys into a []string, sort them (e.g.
sort.Strings), then iterate the sorted keys to populate client.ID and append to
the []model.OIDCClientConfig so OIDCClients is produced deterministically
(references: OIDCClients, config.OIDC.Clients, model.OIDCClientConfig).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@internal/test/test.go`:
- Around line 95-102: The OIDCClients slice is built by ranging over the map
config.OIDC.Clients which yields non-deterministic order; to fix, collect the
map keys into a []string, sort them (e.g. sort.Strings), then iterate the sorted
keys to populate client.ID and append to the []model.OIDCClientConfig so
OIDCClients is produced deterministically (references: OIDCClients,
config.OIDC.Clients, model.OIDCClientConfig).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: df1dd065-ebc7-4c6e-8a8b-6ec487bd60bb

📥 Commits

Reviewing files that changed from the base of the PR and between d500907 and e739aa8.

📒 Files selected for processing (1)
  • internal/test/test.go

scottmckendry
scottmckendry previously approved these changes May 9, 2026
Copy link
Copy Markdown
Member

@scottmckendry scottmckendry left a comment

Choose a reason for hiding this comment

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

tiny work - couple minor things

Image

Comment thread internal/bootstrap/app_bootstrap.go Outdated
Comment thread internal/bootstrap/app_bootstrap.go
Comment thread internal/bootstrap/app_bootstrap.go
@steveiliop56 steveiliop56 dismissed stale reviews from scottmckendry and Rycochet via 11b6155 May 10, 2026 13:03
@steveiliop56 steveiliop56 merged commit 4f7335e into main May 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants