Files
inventory-plus-plus/server/api/auth/router.go
T
angel ccc00d5c89 Claude-assisted improvements (untested)
- dev auth flow (side-step OAuth)
- db event processing integration tests
- dev scripts (eg Makefile)
- db / test db migration setup scripts.
2026-08-05 17:56:21 -06:00

138 lines
3.6 KiB
Go

package auth
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"ruben/inventory2/domains/authentication"
"ruben/inventory2/logging"
"ruben/inventory2/server/cookies"
"ruben/inventory2/server/response"
)
type loginSubrouter struct {
log *logging.Logger
auth *authentication.Authenticator
}
func Routes(
r *gin.RouterGroup,
logger *logging.Logger,
auth *authentication.Authenticator,
devAuthEnabled bool,
) {
ls := &loginSubrouter{
log: logger,
auth: auth,
}
r.GET("/login", response.Handler(ls.loginPage))
r.GET("/login/callback", response.Handler(ls.loginCallback))
r.GET("/logout", response.Handler(ls.logoutPage))
if devAuthEnabled {
logger.Warn("DEV_AUTH_ENABLED is set: /api/auth/dev-login is live and lets any caller authenticate as any user_id with no credentials. Never enable this outside local development.")
r.GET("/dev-login", response.Handler(ls.devLoginPage))
}
}
func (s *loginSubrouter) loginPage(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
u, err := NewLoginURL(ctx, s.auth, "/")
if err != nil {
return nil, err
}
return response.TemporaryRedirect(u), nil
}
func NewLoginURL(ctx context.Context, auth *authentication.Authenticator, targetURI string) (string, error) {
state, err := auth.NewState(ctx, targetURI)
if err != nil {
return "", fmt.Errorf("failed to generate random state: %w", err)
}
base64EncodedState := fmt.Sprintf("%x", state[:])
return auth.AuthCodeURL(base64EncodedState), nil
}
func (s *loginSubrouter) loginCallback(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
q := r.URL.Query()
// obtain token and profile
accessToken, targetURI, expiration, err := s.auth.Exchange(ctx, q.Get("state"), q.Get("code"))
if err != nil {
return nil, response.Unauthorized().
Msg(fmt.Sprintf("Failed to exchange an authorization code for a token")).
Wrap(err)
}
// set access_token cookie and redirect to a reasonable place
return response.TemporaryRedirect(targetURI).
Cookie(cookies.AccessToken(accessToken, expiration)), nil
}
// devLoginPage mints a local session for a user_id, skipping the real Auth0
// OAuth round-trip. Only registered when devAuthEnabled is passed to Routes.
//
// Query params:
// - user_id: identity to log in as (default "dev-user"); use different
// values to test multiple accounts side by side.
// - name: display name for the identity (default derived from user_id).
// - target: where to redirect after login (default "/").
func (s *loginSubrouter) devLoginPage(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
q := r.URL.Query()
userID := q.Get("user_id")
if userID == "" {
userID = "dev-user"
}
name := q.Get("name")
if name == "" {
name = "Dev User (" + userID + ")"
}
targetURI := q.Get("target")
if targetURI == "" {
targetURI = "/"
}
accessToken, expiration, err := s.auth.DevLogin(ctx, userID, name)
if err != nil {
return nil, response.Errorf("failed to create dev session: %w", err)
}
return response.TemporaryRedirect(targetURI).
Cookie(cookies.AccessToken(accessToken, expiration)), nil
}
func (s *loginSubrouter) logoutPage(c *gin.Context) (response.Response, error) {
r := c.Request
host := r.Header.Get("X-Forwarded-Host")
if host == "" {
host = r.Host
}
if ck, err := r.Cookie("access_token"); err == nil && ck != nil {
if err := s.auth.DeleteOAuthTokens(r.Context(), ck.Value); err != nil {
s.log.Error("failed to delete auth token", "error", err)
}
}
return response.TemporaryRedirect(s.auth.GetLogoutURL(host).String()).
Cookie(cookies.Expired("access_token")), nil
}