From 85c1fbe22ca3be5cc9090a8778f9e7bc55e7da70 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 06:46:07 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`feat/au?= =?UTF-8?q?th`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @jasutiin. * https://github.com/jasutiin/envlink/pull/1#issuecomment-3942964012 The following files were modified: * `cmd/server/main.go` * `internal/cli/commands/register.go` * `internal/cli/root.go` * `internal/cli/utils/exchange.go` * `internal/server/api/auth/auth.go` * `internal/server/api/auth/controllers.go` * `internal/server/api/auth/router.go` * `internal/server/database/database.go` * `internal/server/utils/exchange.go` --- cmd/server/main.go | 6 ++- internal/cli/commands/register.go | 8 ++++ internal/cli/root.go | 6 +++ internal/cli/utils/exchange.go | 36 ++++++++--------- internal/server/api/auth/auth.go | 12 ++++-- internal/server/api/auth/controllers.go | 4 +- internal/server/api/auth/router.go | 1 + internal/server/database/database.go | 6 ++- internal/server/utils/exchange.go | 51 +++++++++---------------- 9 files changed, 71 insertions(+), 59 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 77ade66..1576b2c 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -14,6 +14,10 @@ import ( "github.com/joho/godotenv" ) +// main initializes application configuration and starts the HTTP server. +// It loads environment variables, validates required settings (PORT and COOKIE_SESSION_KEY), +// determines production mode from RAILWAY_ENVIRONMENT_NAME, initializes the database and auth, +// registers API routes, and runs the Gin server bound to 0.0.0.0 on the configured port. func main() { err := godotenv.Load() if err != nil { @@ -52,4 +56,4 @@ func main() { fmt.Printf("listening on port %s", port) server.Run("0.0.0.0:" + port) -} +} \ No newline at end of file diff --git a/internal/cli/commands/register.go b/internal/cli/commands/register.go index ebe1cbe..911ca4d 100644 --- a/internal/cli/commands/register.go +++ b/internal/cli/commands/register.go @@ -21,6 +21,9 @@ var RegisterCmd = &cobra.Command{ }, } +// register displays authentication provider options, reads the user's choice, +// and invokes the corresponding registration flow (email/password or Google), +// or prints "Cancelled." for any other input. func register() { var choice string fmt.Println("1) Email/Password") @@ -39,6 +42,8 @@ func register() { } } +// registerUsingEmailPassword reads email and password from standard input, POSTs them as JSON to http://localhost:8080/api/v1/auth/register, and reports the HTTP status. +// It prints whether each credential was provided, uses a 10-second HTTP client timeout, logs a fatal error if the request cannot be performed, and prints success for 2xx responses or failure otherwise. func registerUsingEmailPassword() { var email string var password string @@ -85,6 +90,9 @@ func registerUsingEmailPassword() { } } +// registerUsingGoogle initiates a local OAuth flow with Google, opens the user's browser for authentication, +// waits for a callback on a temporary local server, exchanges the received code for an access token, and prints the token. +// On failure (listener/startup error, browser open failure, callback error, timeout, or token exchange error) it prints an explanatory message and returns. func registerUsingGoogle() { state, err := cliutils.NewCLISessionID() if err != nil { diff --git a/internal/cli/root.go b/internal/cli/root.go index b0ac327..8b5d26a 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -27,6 +27,7 @@ func Execute() error { return rootCmd.Execute() } +// init initializes the root CLI command: registers initConfig to run on startup, defines persistent flags (config, author, license, viper), binds those flags to Viper keys and sets Viper defaults, and attaches exported subcommands from the commands package. func init() { cobra.OnInitialize(initConfig) @@ -47,6 +48,11 @@ func init() { rootCmd.AddCommand(commands.StoreCmd) } +// initConfig initializes Viper configuration for the CLI. +// If cfgFile is set, that file is used; otherwise the function configures Viper +// to look for a YAML config named .cobra in the user's home directory. +// It enables automatic environment variable support and, if a config file is found +// and read successfully, prints the path of the used config file. func initConfig() { if cfgFile != "" { // Use config file from the flag. diff --git a/internal/cli/utils/exchange.go b/internal/cli/utils/exchange.go index 0b72fe5..1108d8c 100644 --- a/internal/cli/utils/exchange.go +++ b/internal/cli/utils/exchange.go @@ -39,7 +39,9 @@ type GoogleTokenResult struct { // // The CLI includes this state in the browser auth request and validates that // the same value is returned on callback. This binds the callback to the -// original login attempt and helps prevent CSRF/callback-injection attacks. +// NewCLISessionID generates a cryptographically secure random hex-encoded string to use as the CLI OAuth session state. +// The value encodes googleAuthStateBytes random bytes (hex length 2*googleAuthStateBytes) and is suitable for mitigating CSRF and callback-injection attacks. +// It returns the encoded state or an error if random byte generation fails. func NewCLISessionID() (string, error) { b := make([]byte, googleAuthStateBytes) if _, err := rand.Read(b); err != nil { @@ -51,7 +53,8 @@ func NewCLISessionID() (string, error) { // BuildServerGoogleAuthURL builds the API auth endpoint URL for CLI login. // It attaches the local callback URL and CLI state so the server can redirect -// back to the CLI listener and preserve request integrity across the flow. +// BuildServerGoogleAuthURL constructs the local server URL that initiates Google OAuth for the CLI. +// The returned URL targets the local API auth endpoint and includes `cli_callback` and `cli_state` query parameters set to the provided callbackURL and state respectively. func BuildServerGoogleAuthURL(callbackURL, state string) string { baseURL := "http://localhost:8080/api/v1/auth/google" values := url.Values{} @@ -61,10 +64,11 @@ func BuildServerGoogleAuthURL(callbackURL, state string) string { return baseURL + "?" + values.Encode() } -/* -This function creates a local server on the machine. This is used for listening to the browser's callback -function, which will be called if the server returns successfully. -*/ +// CreateLocalServer returns an *http.Server configured to handle the OAuth browser callback at callbackPath. +// It validates that the returned state matches expectedState and that an exchange code is present. +// On a successful callback it writes a success HTML response and sends a CallbackResult with ExchangeCode and State to resultChan. +// On error or validation failure it writes an error HTML response and sends a CallbackResult containing an Err to resultChan. +// The returned server is configured with the handler but is not started (caller must call ListenAndServe or Start). func CreateLocalServer(callbackPath, expectedState string, resultChan chan<- CallbackResult) *http.Server { mux := http.NewServeMux() server := &http.Server{Handler: mux} @@ -102,18 +106,9 @@ func CreateLocalServer(callbackPath, expectedState string, resultChan chan<- Cal return server } -/* -ExchangeServerCode sends the one-time exchange code from the local OAuth callback to the API. - -The API validates that: -1) the exchange code exists and is still valid, -2) the state value matches what was originally issued, -3) the code has not already been consumed. - -If validation succeeds, the server returns an auth token for the CLI session. -If validation fails (expired/invalid code, state mismatch, or server rejection), -this function returns an error so the user can retry login. -*/ +// ExchangeServerCode posts the one-time exchange code and state to the local API and returns the CLI access token. +// It sends the code and state to the local exchange endpoint, validates the server response, and returns a GoogleTokenResult +// containing the access token, or an error if the server rejects the exchange or the response is invalid. func ExchangeServerCode(exchangeCode, state string) (*GoogleTokenResult, error) { // build the payload the API expects for code/state validation payload := tokenExchangeRequest{ExchangeCode: exchangeCode, State: state} @@ -161,11 +156,12 @@ func ExchangeServerCode(exchangeCode, state string) (*GoogleTokenResult, error) return &GoogleTokenResult{AccessToken: exchangeResp.Token}, nil } -// OpenInBrowser opens a targetURL in a browser. +// OpenInBrowser opens targetURL in the system default web browser on Windows. +// It returns any error encountered while attempting to launch the browser. func OpenInBrowser(targetURL string) error { if err := exec.Command("rundll32", "url.dll,FileProtocolHandler", targetURL).Start(); err == nil { return nil } return exec.Command("cmd", "/c", "start", "", fmt.Sprintf("\"%s\"", targetURL)).Start() -} +} \ No newline at end of file diff --git a/internal/server/api/auth/auth.go b/internal/server/api/auth/auth.go index c0a08f7..48ddc40 100644 --- a/internal/server/api/auth/auth.go +++ b/internal/server/api/auth/auth.go @@ -12,10 +12,14 @@ import ( "github.com/markbates/goth/providers/google" ) -/* -This function is called to initialize the gothic package with the external -providers we will be using for OAuth. -*/ +// NewAuth initializes the Gothic OAuth configuration with Google as the provider. +// It reads GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from the environment and returns +// an error if either is missing. NewAuth creates a cookie store using key and +// configures session options (path "/", 30-day max age, HttpOnly, Secure set +// from isProd, SameSite Lax). It sets the provider callback URL to +// "http://localhost:/api/v1/auth/google/callback" when domain is empty, +// otherwise "https:///api/v1/auth/google/callback", registers the Google +// provider with Goth, and assigns the cookie store to gothic.Store. func NewAuth(port string, domain string, key string, isProd bool) error { googleClientId := os.Getenv("GOOGLE_CLIENT_ID") if googleClientId == "" { diff --git a/internal/server/api/auth/controllers.go b/internal/server/api/auth/controllers.go index e04ca73..ddda490 100644 --- a/internal/server/api/auth/controllers.go +++ b/internal/server/api/auth/controllers.go @@ -25,6 +25,8 @@ type AuthController struct { repo AuthRepository } +// NewController creates a new AuthController configured with the provided AuthRepository. +// The controller uses the repository to perform authentication operations. func NewController(repo AuthRepository) *AuthController { return &AuthController{repo: repo} } @@ -174,4 +176,4 @@ func (controller *AuthController) getLogoutProvider(c *gin.Context) { gothic.Logout(c.Writer, c.Request) c.Writer.Header().Set("Location", "/") c.Writer.WriteHeader(http.StatusTemporaryRedirect) -} +} \ No newline at end of file diff --git a/internal/server/api/auth/router.go b/internal/server/api/auth/router.go index 94aca5c..1899564 100644 --- a/internal/server/api/auth/router.go +++ b/internal/server/api/auth/router.go @@ -5,6 +5,7 @@ import ( "gorm.io/gorm" ) +// - GET /auth/:provider/logout -> provider logout func AuthRouter(router *gin.RouterGroup, db *gorm.DB) { repo := NewRepository(db) controller := NewController(repo) diff --git a/internal/server/database/database.go b/internal/server/database/database.go index cd05903..d38ff40 100644 --- a/internal/server/database/database.go +++ b/internal/server/database/database.go @@ -10,6 +10,8 @@ import ( "gorm.io/gorm" ) +// CreateDB creates a GORM DB connection configured from environment variables. +// It attempts to load a .env file and reads DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, and DB_PORT; errors are printed and not returned, and the function may return nil if the connection cannot be opened. func CreateDB() *gorm.DB { err := godotenv.Load() if err != nil { @@ -32,8 +34,10 @@ func CreateDB() *gorm.DB { return db } +// AutoMigrate applies schema migrations for the auth.User model to the provided database. +// If migration fails, the error is printed to standard output and no error is returned. func AutoMigrate(db *gorm.DB) { if err := db.AutoMigrate(&auth.User{}); err != nil { fmt.Println("migrate failed:", err) } -} +} \ No newline at end of file diff --git a/internal/server/utils/exchange.go b/internal/server/utils/exchange.go index 86073e4..2c9e045 100644 --- a/internal/server/utils/exchange.go +++ b/internal/server/utils/exchange.go @@ -30,11 +30,7 @@ const ( cliCookieTTLSeconds = 300 ) -/* -newCLIExchangeStore initializes a new store and creates a new cliExchangeEntry map -so it is allocated. It doesn't create a mutex because the mutex's default value of 0 means -that it is unlocked. -*/ +// The store's mutex is left as the zero value (unlocked). func newCLIExchangeStore() *cliExchangeStore { return &cliExchangeStore{entries: make(map[string]cliExchangeEntry)} } @@ -84,12 +80,8 @@ func (store *cliExchangeStore) Consume(exchangeCode, state string) (string, bool var PendingCLIExchanges = newCLIExchangeStore() -/* -isAllowedCLICallback checks if the callback url is something valid that a user -initiated themselves. This prevents the server from returning a different -callback url that the user expects. If we did not check this, the user may -be taken to a malicious website. -*/ +// IsAllowedCLICallback reports whether rawCallbackURL is an allowed local HTTP callback URL. +// It accepts only URLs with scheme "http" and hostname "localhost", "127.0.0.1", or "::1". func IsAllowedCLICallback(rawCallbackURL string) bool { parsedURL, err := url.Parse(rawCallbackURL) if err != nil { @@ -104,19 +96,20 @@ func IsAllowedCLICallback(rawCallbackURL string) bool { return hostName == "localhost" || hostName == "127.0.0.1" || hostName == "::1" } -/* -writeCLIAuthContext sets httpOnly cookies for the callback url and state separately, both with an expiration time. -It adds it to the Gin context object which adds it to the response the server sends back. From there, -the browser would be sending these cookies to the server upon each subsequent request. -*/ +// WriteCLIAuthContext sets HTTP-only cookies on the provided Gin context to persist a URL-escaped +// callback URL and the state value for a CLI authentication flow. The cookies are set for path "/", +// use the package TTL (cliCookieTTLSeconds), and are not marked as secure. The callback value is URL-escaped +// before being stored. func WriteCLIAuthContext(c *gin.Context, callbackURL, state string) { c.SetCookie(cliCallbackCookieName, url.QueryEscape(callbackURL), cliCookieTTLSeconds, "/", "", false, true) c.SetCookie(cliStateCookieName, state, cliCookieTTLSeconds, "/", "", false, true) } -/* -readCLIAuthContext checks if the caller has cookies storing the callback url and state. -*/ +// ReadCLIAuthContext reads the CLI callback URL and state from HTTP cookies, validates and URL-decodes the callback, and returns them when valid. +// +// It expects cookies named "envlink_cli_callback" and "envlink_cli_state". The callback value is URL-decoded and must be an allowed local HTTP callback (for example, localhost, 127.0.0.1, or ::1). +// +// Returns the decoded callback URL, the state value, and `true` on success; otherwise returns empty strings and `false`. func ReadCLIAuthContext(c *gin.Context) (string, string, bool) { callbackCookie, callbackErr := c.Cookie(cliCallbackCookieName) stateCookie, stateErr := c.Cookie(cliStateCookieName) @@ -132,19 +125,14 @@ func ReadCLIAuthContext(c *gin.Context) (string, string, bool) { return decodedCallback, stateCookie, true } -/* -clearCLIAuthContext clears cookies from the response, signalling that we have successfully -received the user's credentials. -*/ +// ClearCLIAuthContext clears the CLI authentication cookies on the response, removing the stored callback URL and state by expiring their cookies. func ClearCLIAuthContext(c *gin.Context) { c.SetCookie(cliCallbackCookieName, "", -1, "/", "", false, true) c.SetCookie(cliStateCookieName, "", -1, "/", "", false, true) } -/* -newExchangeCode creates a new exchange code that the browser will use to verify -against the server. -*/ +// NewExchangeCode returns a new random exchange code encoded as a 48-character hex string. +// It reads 24 cryptographically secure random bytes and returns their hex encoding, or an error if random generation fails. func NewExchangeCode() (string, error) { b := make([]byte, 24) if _, err := rand.Read(b); err != nil { @@ -154,10 +142,9 @@ func NewExchangeCode() (string, error) { return hex.EncodeToString(b), nil } -/* -buildCLIRedirectURL creates a redirect URL with the callback URL and exchange code that we -received after logging in with an auth provider. -*/ +// BuildCLIRedirectURL constructs a redirect URL by adding the "exchange_code" and "state" +// query parameters to the provided callbackURL. +// It returns the resulting URL string, or an error if callbackURL cannot be parsed. func BuildCLIRedirectURL(callbackURL, exchangeCode, state string) (string, error) { parsedURL, err := url.Parse(callbackURL) if err != nil { @@ -170,4 +157,4 @@ func BuildCLIRedirectURL(callbackURL, exchangeCode, state string) (string, error parsedURL.RawQuery = queryValues.Encode() return parsedURL.String(), nil -} +} \ No newline at end of file