feat(cli): add auth status command to verify session validity
- commit
- a66330d9f9c33bdad397e12caa62de36127cdebd
- author
- bnema · · 2026-01-28 21:33 UTC
- parent
- c5d94e6
- signature
- unsigned
22 files · +299 -21
M .mockery.yaml +2 -2
| @@ -7,7 +7,7 @@ structname: 'Mock{{.InterfaceName}}' | |||
| 7 | 7 | pkgname: 'mocks' | |
| 8 | 8 | ||
| 9 | 9 | packages: | |
| 10 | - | gordon/internal/boundaries/out: | |
| 10 | + | github.com/bnema/gordon/internal/boundaries/out: | |
| 11 | 11 | interfaces: | |
| 12 | 12 | ContainerRuntime: | |
| 13 | 13 | BlobStorage: | |
| @@ -22,7 +22,7 @@ packages: | |||
| 22 | 22 | TokenStore: | |
| 23 | 23 | DomainSecretStore: | |
| 24 | 24 | RateLimiter: | |
| 25 | - | gordon/internal/boundaries/in: | |
| 25 | + | github.com/bnema/gordon/internal/boundaries/in: | |
| 26 | 26 | interfaces: | |
| 27 | 27 | ContainerService: | |
| 28 | 28 | RegistryService: | |
| @@ -7,7 +7,7 @@ structname: 'Mock{{.InterfaceName}}' | |||||
| 7 | pkgname: 'mocks' | 7 | pkgname: 'mocks' | ||
| 8 | 8 | ||||
| 9 | packages: | 9 | packages: | ||
| 10 | - | gordon/internal/boundaries/out: | 10 | + | github.com/bnema/gordon/internal/boundaries/out: |
| 11 | interfaces: | 11 | interfaces: | ||
| 12 | ContainerRuntime: | 12 | ContainerRuntime: | ||
| 13 | BlobStorage: | 13 | BlobStorage: | ||
| @@ -22,7 +22,7 @@ packages: | |||||
| 22 | TokenStore: | 22 | TokenStore: | ||
| 23 | DomainSecretStore: | 23 | DomainSecretStore: | ||
| 24 | RateLimiter: | 24 | RateLimiter: | ||
| 25 | - | gordon/internal/boundaries/in: | 25 | + | github.com/bnema/gordon/internal/boundaries/in: |
| 26 | interfaces: | 26 | interfaces: | ||
| 27 | ContainerService: | 27 | ContainerService: | ||
| 28 | RegistryService: | 28 | RegistryService: | ||
A internal/adapters/dto/admin_auth.go +10 -0
| @@ -0,0 +1,10 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | // AuthVerifyResponse represents response from /admin/auth/verify. | |
| 4 | + | type AuthVerifyResponse struct { | |
| 5 | + | Valid bool `json:"valid"` | |
| 6 | + | Subject string `json:"subject,omitempty"` | |
| 7 | + | Scopes []string `json:"scopes,omitempty"` | |
| 8 | + | ExpiresAt int64 `json:"expires_at,omitempty"` | |
| 9 | + | IssuedAt int64 `json:"issued_at,omitempty"` | |
| 10 | + | } | |
| @@ -0,0 +1,10 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | // AuthVerifyResponse represents response from /admin/auth/verify. | |||
| 4 | + | type AuthVerifyResponse struct { | |||
| 5 | + | Valid bool `json:"valid"` | |||
| 6 | + | Subject string `json:"subject,omitempty"` | |||
| 7 | + | Scopes []string `json:"scopes,omitempty"` | |||
| 8 | + | ExpiresAt int64 `json:"expires_at,omitempty"` | |||
| 9 | + | IssuedAt int64 `json:"issued_at,omitempty"` | |||
| 10 | + | } | |||
M internal/adapters/in/cli/auth.go +129 -0
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "github.com/spf13/viper" | |
| 14 | 14 | "golang.org/x/term" | |
| 15 | 15 | ||
| 16 | + | "github.com/bnema/gordon/internal/adapters/dto" | |
| 16 | 17 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | |
| 17 | 18 | "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles" | |
| 18 | 19 | "github.com/bnema/gordon/internal/adapters/out/secrets" | |
| @@ -35,6 +36,7 @@ func newAuthCmd() *cobra.Command { | |||
| 35 | 36 | cmd.AddCommand(newAuthPasswordCmd()) | |
| 36 | 37 | cmd.AddCommand(newAuthInternalCmd()) | |
| 37 | 38 | cmd.AddCommand(newAuthLoginCmd()) | |
| 39 | + | cmd.AddCommand(newAuthStatusCmd()) | |
| 38 | 40 | ||
| 39 | 41 | return cmd | |
| 40 | 42 | } | |
| @@ -651,3 +653,130 @@ func createAuthServiceForCLI(cfg *cliConfig, log zerowrap.Logger) (*auth.Service | |||
| 651 | 653 | ||
| 652 | 654 | return auth.NewService(authConfig, store, log), nil | |
| 653 | 655 | } | |
| 656 | + | ||
| 657 | + | // newAuthStatusCmd creates the auth status command. | |
| 658 | + | func newAuthStatusCmd() *cobra.Command { | |
| 659 | + | return &cobra.Command{ | |
| 660 | + | Use: "status", | |
| 661 | + | Short: "Check authentication session status", | |
| 662 | + | Long: `Verify if stored authentication session is still valid. | |
| 663 | + | ||
| 664 | + | Shows token validity, expiry, and connection status for the active remote. | |
| 665 | + | ||
| 666 | + | Examples: | |
| 667 | + | gordon auth status Check status of active remote | |
| 668 | + | gordon auth status --remote prod Check specific remote`, | |
| 669 | + | RunE: func(cmd *cobra.Command, args []string) error { | |
| 670 | + | return runAuthStatus() | |
| 671 | + | }, | |
| 672 | + | } | |
| 673 | + | } | |
| 674 | + | ||
| 675 | + | // runAuthStatus checks authentication status for the active remote. | |
| 676 | + | func runAuthStatus() error { | |
| 677 | + | // Load remotes config | |
| 678 | + | config, err := remote.LoadRemotes("") | |
| 679 | + | if err != nil { | |
| 680 | + | return fmt.Errorf("failed to load remotes: %w", err) | |
| 681 | + | } | |
| 682 | + | ||
| 683 | + | // Determine active remote | |
| 684 | + | remoteName := config.Active | |
| 685 | + | if remoteName == "" { | |
| 686 | + | return fmt.Errorf("No active remote configured. Use 'gordon remote use <name>' or --remote flag") | |
| 687 | + | } | |
| 688 | + | ||
| 689 | + | remoteConfig, ok := config.Remotes[remoteName] | |
| 690 | + | if !ok { | |
| 691 | + | return fmt.Errorf("Active remote '%s' not found", remoteName) | |
| 692 | + | } | |
| 693 | + | ||
| 694 | + | // Create client | |
| 695 | + | client := remote.NewClient(remoteConfig.URL, remote.WithToken(remoteConfig.Token)) | |
| 696 | + | ||
| 697 | + | // Verify auth | |
| 698 | + | ctx := context.Background() | |
| 699 | + | fmt.Printf("Checking authentication status for '%s'...\n", remoteName) | |
| 700 | + | ||
| 701 | + | status, err := client.VerifyAuth(ctx) | |
| 702 | + | if err != nil { | |
| 703 | + | return handleAuthVerifyError(err, remoteName, remoteConfig.URL) | |
| 704 | + | } | |
| 705 | + | ||
| 706 | + | // Display status | |
| 707 | + | displayAuthStatus(remoteName, remoteConfig.URL, status) | |
| 708 | + | return nil | |
| 709 | + | } | |
| 710 | + | ||
| 711 | + | // handleAuthVerifyError handles errors from auth verification. | |
| 712 | + | func handleAuthVerifyError(err error, remoteName, url string) error { | |
| 713 | + | errStr := err.Error() | |
| 714 | + | ||
| 715 | + | // Connection errors | |
| 716 | + | if strings.Contains(errStr, "connection refused") || | |
| 717 | + | strings.Contains(errStr, "no such host") || | |
| 718 | + | strings.Contains(errStr, "timeout") { | |
| 719 | + | fmt.Println(styles.Theme.Bold.Render("Remote: " + url)) | |
| 720 | + | fmt.Println() | |
| 721 | + | fmt.Println(styles.Theme.Error.Render(styles.IconError + " Connection Failed")) | |
| 722 | + | fmt.Printf("Error: %s\n\n", errStr) | |
| 723 | + | fmt.Println(styles.Theme.Muted.Render("Check that Gordon server is running and accessible.")) | |
| 724 | + | return nil | |
| 725 | + | } | |
| 726 | + | ||
| 727 | + | // Auth errors | |
| 728 | + | if strings.Contains(errStr, "401") || | |
| 729 | + | strings.Contains(errStr, "unauthorized") || | |
| 730 | + | strings.Contains(errStr, "invalid token") { | |
| 731 | + | fmt.Println(styles.Theme.Bold.Render("Remote: " + url)) | |
| 732 | + | fmt.Println() | |
| 733 | + | fmt.Println(styles.Theme.Error.Render(styles.IconError + " Authentication Failed")) | |
| 734 | + | fmt.Println() | |
| 735 | + | fmt.Println(styles.Theme.Muted.Render("Reason: Token is invalid or expired")) | |
| 736 | + | fmt.Println() | |
| 737 | + | fmt.Println(styles.Theme.Bold.Render("Action: Re-authenticate with:")) | |
| 738 | + | fmt.Printf(" gordon auth login --remote %s\n", remoteName) | |
| 739 | + | return nil | |
| 740 | + | } | |
| 741 | + | ||
| 742 | + | return fmt.Errorf("failed to verify auth: %w", err) | |
| 743 | + | } | |
| 744 | + | ||
| 745 | + | // displayAuthStatus displays authentication status information. | |
| 746 | + | func displayAuthStatus(remoteName, url string, status *dto.AuthVerifyResponse) { | |
| 747 | + | fmt.Println(styles.Theme.Bold.Render("Remote: " + url)) | |
| 748 | + | fmt.Println() | |
| 749 | + | ||
| 750 | + | if status.Valid { | |
| 751 | + | // Valid authentication | |
| 752 | + | fmt.Println(styles.Theme.Success.Render(styles.IconSuccess + " Authenticated")) | |
| 753 | + | fmt.Println() | |
| 754 | + | ||
| 755 | + | if status.Subject != "" { | |
| 756 | + | fmt.Printf("%s %s\n", styles.Theme.Bold.Render("Subject:"), status.Subject) | |
| 757 | + | } | |
| 758 | + | ||
| 759 | + | if status.IssuedAt > 0 { | |
| 760 | + | issuedAt := time.Unix(status.IssuedAt, 0) | |
| 761 | + | fmt.Printf("%s %s\n", styles.Theme.Bold.Render("Issued:"), issuedAt.Format("2006-01-02 15:04:05")) | |
| 762 | + | } | |
| 763 | + | ||
| 764 | + | if status.ExpiresAt > 0 { | |
| 765 | + | expiresAt := time.Unix(status.ExpiresAt, 0) | |
| 766 | + | remaining := time.Until(expiresAt) | |
| 767 | + | fmt.Printf("%s %s (%s)\n", styles.Theme.Bold.Render("Expires:"), | |
| 768 | + | expiresAt.Format("2006-01-02 15:04:05"), | |
| 769 | + | styles.Theme.Muted.Render(remaining.Round(time.Second).String())) | |
| 770 | + | } | |
| 771 | + | ||
| 772 | + | if len(status.Scopes) > 0 { | |
| 773 | + | fmt.Printf("%s %s\n", styles.Theme.Bold.Render("Scopes:"), strings.Join(status.Scopes, ", ")) | |
| 774 | + | } | |
| 775 | + | } else { | |
| 776 | + | // Invalid or no authentication | |
| 777 | + | fmt.Println(styles.Theme.Error.Render(styles.IconError + " Not Authenticated")) | |
| 778 | + | fmt.Println() | |
| 779 | + | fmt.Println(styles.Theme.Muted.Render("Action: Re-authenticate with:")) | |
| 780 | + | fmt.Printf(" gordon auth login --remote %s\n", remoteName) | |
| 781 | + | } | |
| 782 | + | } | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "github.com/spf13/viper" | 13 | "github.com/spf13/viper" | ||
| 14 | "golang.org/x/term" | 14 | "golang.org/x/term" | ||
| 15 | 15 | ||||
| 16 | + | "github.com/bnema/gordon/internal/adapters/dto" | |||
| 16 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | 17 | "github.com/bnema/gordon/internal/adapters/in/cli/remote" | ||
| 17 | "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles" | 18 | "github.com/bnema/gordon/internal/adapters/in/cli/ui/styles" | ||
| 18 | "github.com/bnema/gordon/internal/adapters/out/secrets" | 19 | "github.com/bnema/gordon/internal/adapters/out/secrets" | ||
| @@ -35,6 +36,7 @@ func newAuthCmd() *cobra.Command { | |||||
| 35 | cmd.AddCommand(newAuthPasswordCmd()) | 36 | cmd.AddCommand(newAuthPasswordCmd()) | ||
| 36 | cmd.AddCommand(newAuthInternalCmd()) | 37 | cmd.AddCommand(newAuthInternalCmd()) | ||
| 37 | cmd.AddCommand(newAuthLoginCmd()) | 38 | cmd.AddCommand(newAuthLoginCmd()) | ||
| 39 | + | cmd.AddCommand(newAuthStatusCmd()) | |||
| 38 | 40 | ||||
| 39 | return cmd | 41 | return cmd | ||
| 40 | } | 42 | } | ||
| @@ -651,3 +653,130 @@ func createAuthServiceForCLI(cfg *cliConfig, log zerowrap.Logger) (*auth.Service | |||||
| 651 | 653 | ||||
| 652 | return auth.NewService(authConfig, store, log), nil | 654 | return auth.NewService(authConfig, store, log), nil | ||
| 653 | } | 655 | } | ||
| 656 | + | ||||
| 657 | + | // newAuthStatusCmd creates the auth status command. | |||
| 658 | + | func newAuthStatusCmd() *cobra.Command { | |||
| 659 | + | return &cobra.Command{ | |||
| 660 | + | Use: "status", | |||
| 661 | + | Short: "Check authentication session status", | |||
| 662 | + | Long: `Verify if stored authentication session is still valid. | |||
| 663 | + | ||||
| 664 | + | Shows token validity, expiry, and connection status for the active remote. | |||
| 665 | + | ||||
| 666 | + | Examples: | |||
| 667 | + | gordon auth status Check status of active remote | |||
| 668 | + | gordon auth status --remote prod Check specific remote`, | |||
| 669 | + | RunE: func(cmd *cobra.Command, args []string) error { | |||
| 670 | + | return runAuthStatus() | |||
| 671 | + | }, | |||
| 672 | + | } | |||
| 673 | + | } | |||
| 674 | + | ||||
| 675 | + | // runAuthStatus checks authentication status for the active remote. | |||
| 676 | + | func runAuthStatus() error { | |||
| 677 | + | // Load remotes config | |||
| 678 | + | config, err := remote.LoadRemotes("") | |||
| 679 | + | if err != nil { | |||
| 680 | + | return fmt.Errorf("failed to load remotes: %w", err) | |||
| 681 | + | } | |||
| 682 | + | ||||
| 683 | + | // Determine active remote | |||
| 684 | + | remoteName := config.Active | |||
| 685 | + | if remoteName == "" { | |||
| 686 | + | return fmt.Errorf("No active remote configured. Use 'gordon remote use <name>' or --remote flag") | |||
| 687 | + | } | |||
| 688 | + | ||||
| 689 | + | remoteConfig, ok := config.Remotes[remoteName] | |||
| 690 | + | if !ok { | |||
| 691 | + | return fmt.Errorf("Active remote '%s' not found", remoteName) | |||
| 692 | + | } | |||
| 693 | + | ||||
| 694 | + | // Create client | |||
| 695 | + | client := remote.NewClient(remoteConfig.URL, remote.WithToken(remoteConfig.Token)) | |||
| 696 | + | ||||
| 697 | + | // Verify auth | |||
| 698 | + | ctx := context.Background() | |||
| 699 | + | fmt.Printf("Checking authentication status for '%s'...\n", remoteName) | |||
| 700 | + | ||||
| 701 | + | status, err := client.VerifyAuth(ctx) | |||
| 702 | + | if err != nil { | |||
| 703 | + | return handleAuthVerifyError(err, remoteName, remoteConfig.URL) | |||
| 704 | + | } | |||
| 705 | + | ||||
| 706 | + | // Display status | |||
| 707 | + | displayAuthStatus(remoteName, remoteConfig.URL, status) | |||
| 708 | + | return nil | |||
| 709 | + | } | |||
| 710 | + | ||||
| 711 | + | // handleAuthVerifyError handles errors from auth verification. | |||
| 712 | + | func handleAuthVerifyError(err error, remoteName, url string) error { | |||
| 713 | + | errStr := err.Error() | |||
| 714 | + | ||||
| 715 | + | // Connection errors | |||
| 716 | + | if strings.Contains(errStr, "connection refused") || | |||
| 717 | + | strings.Contains(errStr, "no such host") || | |||
| 718 | + | strings.Contains(errStr, "timeout") { | |||
| 719 | + | fmt.Println(styles.Theme.Bold.Render("Remote: " + url)) | |||
| 720 | + | fmt.Println() | |||
| 721 | + | fmt.Println(styles.Theme.Error.Render(styles.IconError + " Connection Failed")) | |||
| 722 | + | fmt.Printf("Error: %s\n\n", errStr) | |||
| 723 | + | fmt.Println(styles.Theme.Muted.Render("Check that Gordon server is running and accessible.")) | |||
| 724 | + | return nil | |||
| 725 | + | } | |||
| 726 | + | ||||
| 727 | + | // Auth errors | |||
| 728 | + | if strings.Contains(errStr, "401") || | |||
| 729 | + | strings.Contains(errStr, "unauthorized") || | |||
| 730 | + | strings.Contains(errStr, "invalid token") { | |||
| 731 | + | fmt.Println(styles.Theme.Bold.Render("Remote: " + url)) | |||
| 732 | + | fmt.Println() | |||
| 733 | + | fmt.Println(styles.Theme.Error.Render(styles.IconError + " Authentication Failed")) | |||
| 734 | + | fmt.Println() | |||
| 735 | + | fmt.Println(styles.Theme.Muted.Render("Reason: Token is invalid or expired")) | |||
| 736 | + | fmt.Println() | |||
| 737 | + | fmt.Println(styles.Theme.Bold.Render("Action: Re-authenticate with:")) | |||
| 738 | + | fmt.Printf(" gordon auth login --remote %s\n", remoteName) | |||
| 739 | + | return nil | |||
| 740 | + | } | |||
| 741 | + | ||||
| 742 | + | return fmt.Errorf("failed to verify auth: %w", err) | |||
| 743 | + | } | |||
| 744 | + | ||||
| 745 | + | // displayAuthStatus displays authentication status information. | |||
| 746 | + | func displayAuthStatus(remoteName, url string, status *dto.AuthVerifyResponse) { | |||
| 747 | + | fmt.Println(styles.Theme.Bold.Render("Remote: " + url)) | |||
| 748 | + | fmt.Println() | |||
| 749 | + | ||||
| 750 | + | if status.Valid { | |||
| 751 | + | // Valid authentication | |||
| 752 | + | fmt.Println(styles.Theme.Success.Render(styles.IconSuccess + " Authenticated")) | |||
| 753 | + | fmt.Println() | |||
| 754 | + | ||||
| 755 | + | if status.Subject != "" { | |||
| 756 | + | fmt.Printf("%s %s\n", styles.Theme.Bold.Render("Subject:"), status.Subject) | |||
| 757 | + | } | |||
| 758 | + | ||||
| 759 | + | if status.IssuedAt > 0 { | |||
| 760 | + | issuedAt := time.Unix(status.IssuedAt, 0) | |||
| 761 | + | fmt.Printf("%s %s\n", styles.Theme.Bold.Render("Issued:"), issuedAt.Format("2006-01-02 15:04:05")) | |||
| 762 | + | } | |||
| 763 | + | ||||
| 764 | + | if status.ExpiresAt > 0 { | |||
| 765 | + | expiresAt := time.Unix(status.ExpiresAt, 0) | |||
| 766 | + | remaining := time.Until(expiresAt) | |||
| 767 | + | fmt.Printf("%s %s (%s)\n", styles.Theme.Bold.Render("Expires:"), | |||
| 768 | + | expiresAt.Format("2006-01-02 15:04:05"), | |||
| 769 | + | styles.Theme.Muted.Render(remaining.Round(time.Second).String())) | |||
| 770 | + | } | |||
| 771 | + | ||||
| 772 | + | if len(status.Scopes) > 0 { | |||
| 773 | + | fmt.Printf("%s %s\n", styles.Theme.Bold.Render("Scopes:"), strings.Join(status.Scopes, ", ")) | |||
| 774 | + | } | |||
| 775 | + | } else { | |||
| 776 | + | // Invalid or no authentication | |||
| 777 | + | fmt.Println(styles.Theme.Error.Render(styles.IconError + " Not Authenticated")) | |||
| 778 | + | fmt.Println() | |||
| 779 | + | fmt.Println(styles.Theme.Muted.Render("Action: Re-authenticate with:")) | |||
| 780 | + | fmt.Printf(" gordon auth login --remote %s\n", remoteName) | |||
| 781 | + | } | |||
| 782 | + | } | |||
M internal/adapters/in/cli/remote/client.go +15 -0
| @@ -632,3 +632,18 @@ func (c *Client) streamLogs(ctx context.Context, path string) (<-chan string, er | |||
| 632 | 632 | ||
| 633 | 633 | return ch, nil | |
| 634 | 634 | } | |
| 635 | + | ||
| 636 | + | // VerifyAuth checks if authentication session is valid. | |
| 637 | + | func (c *Client) VerifyAuth(ctx context.Context) (*dto.AuthVerifyResponse, error) { | |
| 638 | + | resp, err := c.request(ctx, http.MethodGet, "/auth/verify", nil) | |
| 639 | + | if err != nil { | |
| 640 | + | return nil, err | |
| 641 | + | } | |
| 642 | + | ||
| 643 | + | var result dto.AuthVerifyResponse | |
| 644 | + | if err := parseResponse(resp, &result); err != nil { | |
| 645 | + | return nil, err | |
| 646 | + | } | |
| 647 | + | ||
| 648 | + | return &result, nil | |
| 649 | + | } | |
| @@ -632,3 +632,18 @@ func (c *Client) streamLogs(ctx context.Context, path string) (<-chan string, er | |||||
| 632 | 632 | ||||
| 633 | return ch, nil | 633 | return ch, nil | ||
| 634 | } | 634 | } | ||
| 635 | + | ||||
| 636 | + | // VerifyAuth checks if authentication session is valid. | |||
| 637 | + | func (c *Client) VerifyAuth(ctx context.Context) (*dto.AuthVerifyResponse, error) { | |||
| 638 | + | resp, err := c.request(ctx, http.MethodGet, "/auth/verify", nil) | |||
| 639 | + | if err != nil { | |||
| 640 | + | return nil, err | |||
| 641 | + | } | |||
| 642 | + | ||||
| 643 | + | var result dto.AuthVerifyResponse | |||
| 644 | + | if err := parseResponse(resp, &result); err != nil { | |||
| 645 | + | return nil, err | |||
| 646 | + | } | |||
| 647 | + | ||||
| 648 | + | return &result, nil | |||
| 649 | + | } | |||
M internal/adapters/in/http/admin/handler.go +30 -5
| @@ -140,11 +140,12 @@ type routeHandler func(w http.ResponseWriter, r *http.Request, path string) | |||
| 140 | 140 | func (h *Handler) matchRoute(path string) (routeHandler, bool) { | |
| 141 | 141 | // Exact match routes | |
| 142 | 142 | exactRoutes := map[string]routeHandler{ | |
| 143 | - | "/networks": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleNetworks(w, r) }, | |
| 144 | - | "/status": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleStatus(w, r) }, | |
| 145 | - | "/health": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleHealth(w, r) }, | |
| 146 | - | "/reload": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleReload(w, r) }, | |
| 147 | - | "/config": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleConfig(w, r) }, | |
| 143 | + | "/networks": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleNetworks(w, r) }, | |
| 144 | + | "/status": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleStatus(w, r) }, | |
| 145 | + | "/health": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleHealth(w, r) }, | |
| 146 | + | "/reload": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleReload(w, r) }, | |
| 147 | + | "/config": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleConfig(w, r) }, | |
| 148 | + | "/auth/verify": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleAuthVerify(w, r) }, | |
| 148 | 149 | } | |
| 149 | 150 | if handler, ok := exactRoutes[path]; ok { | |
| 150 | 151 | return handler, true | |
| @@ -1078,3 +1079,27 @@ func (h *Handler) handleAttachmentsConfigDelete(w http.ResponseWriter, r *http.R | |||
| 1078 | 1079 | log.Info().Str("target", domainOrGroup).Str("image", image).Msg("attachment removed") | |
| 1079 | 1080 | h.sendJSON(w, http.StatusOK, dto.AttachmentStatusResponse{Status: "removed"}) | |
| 1080 | 1081 | } | |
| 1082 | + | ||
| 1083 | + | // handleAuthVerify handles /admin/auth/verify endpoint. | |
| 1084 | + | // Validates authentication session and returns token status. | |
| 1085 | + | func (h *Handler) handleAuthVerify(w http.ResponseWriter, r *http.Request) { | |
| 1086 | + | ctx := r.Context() | |
| 1087 | + | ||
| 1088 | + | // Call use case to get auth status | |
| 1089 | + | status, err := h.authSvc.GetAuthStatus(ctx) | |
| 1090 | + | if err != nil { | |
| 1091 | + | h.sendError(w, http.StatusInternalServerError, "failed to get auth status") | |
| 1092 | + | return | |
| 1093 | + | } | |
| 1094 | + | ||
| 1095 | + | // Convert domain.AuthStatus to DTO | |
| 1096 | + | response := dto.AuthVerifyResponse{ | |
| 1097 | + | Valid: status.Valid, | |
| 1098 | + | Subject: status.Subject, | |
| 1099 | + | Scopes: status.Scopes, | |
| 1100 | + | ExpiresAt: status.ExpiresAt, | |
| 1101 | + | IssuedAt: status.IssuedAt, | |
| 1102 | + | } | |
| 1103 | + | ||
| 1104 | + | h.sendJSON(w, http.StatusOK, response) | |
| 1105 | + | } | |
| @@ -140,11 +140,12 @@ type routeHandler func(w http.ResponseWriter, r *http.Request, path string) | |||||
| 140 | func (h *Handler) matchRoute(path string) (routeHandler, bool) { | 140 | func (h *Handler) matchRoute(path string) (routeHandler, bool) { | ||
| 141 | // Exact match routes | 141 | // Exact match routes | ||
| 142 | exactRoutes := map[string]routeHandler{ | 142 | exactRoutes := map[string]routeHandler{ | ||
| 143 | - | "/networks": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleNetworks(w, r) }, | 143 | + | "/networks": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleNetworks(w, r) }, |
| 144 | - | "/status": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleStatus(w, r) }, | 144 | + | "/status": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleStatus(w, r) }, |
| 145 | - | "/health": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleHealth(w, r) }, | 145 | + | "/health": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleHealth(w, r) }, |
| 146 | - | "/reload": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleReload(w, r) }, | 146 | + | "/reload": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleReload(w, r) }, |
| 147 | - | "/config": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleConfig(w, r) }, | 147 | + | "/config": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleConfig(w, r) }, |
| 148 | + | "/auth/verify": func(w http.ResponseWriter, r *http.Request, _ string) { h.handleAuthVerify(w, r) }, | |||
| 148 | } | 149 | } | ||
| 149 | if handler, ok := exactRoutes[path]; ok { | 150 | if handler, ok := exactRoutes[path]; ok { | ||
| 150 | return handler, true | 151 | return handler, true | ||
| @@ -1078,3 +1079,27 @@ func (h *Handler) handleAttachmentsConfigDelete(w http.ResponseWriter, r *http.R | |||||
| 1078 | log.Info().Str("target", domainOrGroup).Str("image", image).Msg("attachment removed") | 1079 | log.Info().Str("target", domainOrGroup).Str("image", image).Msg("attachment removed") | ||
| 1079 | h.sendJSON(w, http.StatusOK, dto.AttachmentStatusResponse{Status: "removed"}) | 1080 | h.sendJSON(w, http.StatusOK, dto.AttachmentStatusResponse{Status: "removed"}) | ||
| 1080 | } | 1081 | } | ||
| 1082 | + | ||||
| 1083 | + | // handleAuthVerify handles /admin/auth/verify endpoint. | |||
| 1084 | + | // Validates authentication session and returns token status. | |||
| 1085 | + | func (h *Handler) handleAuthVerify(w http.ResponseWriter, r *http.Request) { | |||
| 1086 | + | ctx := r.Context() | |||
| 1087 | + | ||||
| 1088 | + | // Call use case to get auth status | |||
| 1089 | + | status, err := h.authSvc.GetAuthStatus(ctx) | |||
| 1090 | + | if err != nil { | |||
| 1091 | + | h.sendError(w, http.StatusInternalServerError, "failed to get auth status") | |||
| 1092 | + | return | |||
| 1093 | + | } | |||
| 1094 | + | ||||
| 1095 | + | // Convert domain.AuthStatus to DTO | |||
| 1096 | + | response := dto.AuthVerifyResponse{ | |||
| 1097 | + | Valid: status.Valid, | |||
| 1098 | + | Subject: status.Subject, | |||
| 1099 | + | Scopes: status.Scopes, | |||
| 1100 | + | ExpiresAt: status.ExpiresAt, | |||
| 1101 | + | IssuedAt: status.IssuedAt, | |||
| 1102 | + | } | |||
| 1103 | + | ||||
| 1104 | + | h.sendJSON(w, http.StatusOK, response) | |||
| 1105 | + | } | |||
M internal/boundaries/in/auth.go +4 -0
| @@ -42,4 +42,8 @@ type AuthService interface { | |||
| 42 | 42 | ||
| 43 | 43 | // GeneratePasswordHash generates a bcrypt hash for a password. | |
| 44 | 44 | GeneratePasswordHash(password string) (string, error) | |
| 45 | + | ||
| 46 | + | // GetAuthStatus returns authentication status from context. | |
| 47 | + | // Extracts and validates token claims that were set by auth middleware. | |
| 48 | + | GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error) | |
| 45 | 49 | } | |
| @@ -42,4 +42,8 @@ type AuthService interface { | |||||
| 42 | 42 | ||||
| 43 | // GeneratePasswordHash generates a bcrypt hash for a password. | 43 | // GeneratePasswordHash generates a bcrypt hash for a password. | ||
| 44 | GeneratePasswordHash(password string) (string, error) | 44 | GeneratePasswordHash(password string) (string, error) | ||
| 45 | + | ||||
| 46 | + | // GetAuthStatus returns authentication status from context. | |||
| 47 | + | // Extracts and validates token claims that were set by auth middleware. | |||
| 48 | + | GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error) | |||
| 45 | } | 49 | } | ||
M internal/boundaries/in/mocks/mock_auth_service.go +63 -1
| @@ -6,9 +6,9 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | "time" | |
| 11 | 10 | ||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |
| 12 | 12 | mock "github.com/stretchr/testify/mock" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| @@ -255,6 +255,68 @@ func (_c *MockAuthService_GenerateToken_Call) RunAndReturn(run func(ctx context. | |||
| 255 | 255 | return _c | |
| 256 | 256 | } | |
| 257 | 257 | ||
| 258 | + | // GetAuthStatus provides a mock function for the type MockAuthService | |
| 259 | + | func (_mock *MockAuthService) GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error) { | |
| 260 | + | ret := _mock.Called(ctx) | |
| 261 | + | ||
| 262 | + | if len(ret) == 0 { | |
| 263 | + | panic("no return value specified for GetAuthStatus") | |
| 264 | + | } | |
| 265 | + | ||
| 266 | + | var r0 *domain.AuthStatus | |
| 267 | + | var r1 error | |
| 268 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) (*domain.AuthStatus, error)); ok { | |
| 269 | + | return returnFunc(ctx) | |
| 270 | + | } | |
| 271 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) *domain.AuthStatus); ok { | |
| 272 | + | r0 = returnFunc(ctx) | |
| 273 | + | } else { | |
| 274 | + | if ret.Get(0) != nil { | |
| 275 | + | r0 = ret.Get(0).(*domain.AuthStatus) | |
| 276 | + | } | |
| 277 | + | } | |
| 278 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |
| 279 | + | r1 = returnFunc(ctx) | |
| 280 | + | } else { | |
| 281 | + | r1 = ret.Error(1) | |
| 282 | + | } | |
| 283 | + | return r0, r1 | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | // MockAuthService_GetAuthStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAuthStatus' | |
| 287 | + | type MockAuthService_GetAuthStatus_Call struct { | |
| 288 | + | *mock.Call | |
| 289 | + | } | |
| 290 | + | ||
| 291 | + | // GetAuthStatus is a helper method to define mock.On call | |
| 292 | + | // - ctx context.Context | |
| 293 | + | func (_e *MockAuthService_Expecter) GetAuthStatus(ctx interface{}) *MockAuthService_GetAuthStatus_Call { | |
| 294 | + | return &MockAuthService_GetAuthStatus_Call{Call: _e.mock.On("GetAuthStatus", ctx)} | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | func (_c *MockAuthService_GetAuthStatus_Call) Run(run func(ctx context.Context)) *MockAuthService_GetAuthStatus_Call { | |
| 298 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 299 | + | var arg0 context.Context | |
| 300 | + | if args[0] != nil { | |
| 301 | + | arg0 = args[0].(context.Context) | |
| 302 | + | } | |
| 303 | + | run( | |
| 304 | + | arg0, | |
| 305 | + | ) | |
| 306 | + | }) | |
| 307 | + | return _c | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | func (_c *MockAuthService_GetAuthStatus_Call) Return(authStatus *domain.AuthStatus, err error) *MockAuthService_GetAuthStatus_Call { | |
| 311 | + | _c.Call.Return(authStatus, err) | |
| 312 | + | return _c | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | func (_c *MockAuthService_GetAuthStatus_Call) RunAndReturn(run func(ctx context.Context) (*domain.AuthStatus, error)) *MockAuthService_GetAuthStatus_Call { | |
| 316 | + | _c.Call.Return(run) | |
| 317 | + | return _c | |
| 318 | + | } | |
| 319 | + | ||
| 258 | 320 | // GetAuthType provides a mock function for the type MockAuthService | |
| 259 | 321 | func (_mock *MockAuthService) GetAuthType() domain.AuthType { | |
| 260 | 322 | ret := _mock.Called() | |
| @@ -6,9 +6,9 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | "time" | 9 | "time" | ||
| 11 | 10 | ||||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |||
| 12 | mock "github.com/stretchr/testify/mock" | 12 | mock "github.com/stretchr/testify/mock" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
| @@ -255,6 +255,68 @@ func (_c *MockAuthService_GenerateToken_Call) RunAndReturn(run func(ctx context. | |||||
| 255 | return _c | 255 | return _c | ||
| 256 | } | 256 | } | ||
| 257 | 257 | ||||
| 258 | + | // GetAuthStatus provides a mock function for the type MockAuthService | |||
| 259 | + | func (_mock *MockAuthService) GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error) { | |||
| 260 | + | ret := _mock.Called(ctx) | |||
| 261 | + | ||||
| 262 | + | if len(ret) == 0 { | |||
| 263 | + | panic("no return value specified for GetAuthStatus") | |||
| 264 | + | } | |||
| 265 | + | ||||
| 266 | + | var r0 *domain.AuthStatus | |||
| 267 | + | var r1 error | |||
| 268 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) (*domain.AuthStatus, error)); ok { | |||
| 269 | + | return returnFunc(ctx) | |||
| 270 | + | } | |||
| 271 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) *domain.AuthStatus); ok { | |||
| 272 | + | r0 = returnFunc(ctx) | |||
| 273 | + | } else { | |||
| 274 | + | if ret.Get(0) != nil { | |||
| 275 | + | r0 = ret.Get(0).(*domain.AuthStatus) | |||
| 276 | + | } | |||
| 277 | + | } | |||
| 278 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |||
| 279 | + | r1 = returnFunc(ctx) | |||
| 280 | + | } else { | |||
| 281 | + | r1 = ret.Error(1) | |||
| 282 | + | } | |||
| 283 | + | return r0, r1 | |||
| 284 | + | } | |||
| 285 | + | ||||
| 286 | + | // MockAuthService_GetAuthStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAuthStatus' | |||
| 287 | + | type MockAuthService_GetAuthStatus_Call struct { | |||
| 288 | + | *mock.Call | |||
| 289 | + | } | |||
| 290 | + | ||||
| 291 | + | // GetAuthStatus is a helper method to define mock.On call | |||
| 292 | + | // - ctx context.Context | |||
| 293 | + | func (_e *MockAuthService_Expecter) GetAuthStatus(ctx interface{}) *MockAuthService_GetAuthStatus_Call { | |||
| 294 | + | return &MockAuthService_GetAuthStatus_Call{Call: _e.mock.On("GetAuthStatus", ctx)} | |||
| 295 | + | } | |||
| 296 | + | ||||
| 297 | + | func (_c *MockAuthService_GetAuthStatus_Call) Run(run func(ctx context.Context)) *MockAuthService_GetAuthStatus_Call { | |||
| 298 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 299 | + | var arg0 context.Context | |||
| 300 | + | if args[0] != nil { | |||
| 301 | + | arg0 = args[0].(context.Context) | |||
| 302 | + | } | |||
| 303 | + | run( | |||
| 304 | + | arg0, | |||
| 305 | + | ) | |||
| 306 | + | }) | |||
| 307 | + | return _c | |||
| 308 | + | } | |||
| 309 | + | ||||
| 310 | + | func (_c *MockAuthService_GetAuthStatus_Call) Return(authStatus *domain.AuthStatus, err error) *MockAuthService_GetAuthStatus_Call { | |||
| 311 | + | _c.Call.Return(authStatus, err) | |||
| 312 | + | return _c | |||
| 313 | + | } | |||
| 314 | + | ||||
| 315 | + | func (_c *MockAuthService_GetAuthStatus_Call) RunAndReturn(run func(ctx context.Context) (*domain.AuthStatus, error)) *MockAuthService_GetAuthStatus_Call { | |||
| 316 | + | _c.Call.Return(run) | |||
| 317 | + | return _c | |||
| 318 | + | } | |||
| 319 | + | ||||
| 258 | // GetAuthType provides a mock function for the type MockAuthService | 320 | // GetAuthType provides a mock function for the type MockAuthService | ||
| 259 | func (_mock *MockAuthService) GetAuthType() domain.AuthType { | 321 | func (_mock *MockAuthService) GetAuthType() domain.AuthType { | ||
| 260 | ret := _mock.Called() | 322 | ret := _mock.Called() | ||
M internal/boundaries/in/mocks/mock_config_service.go +1 -1
| @@ -6,8 +6,8 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | ||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |
| 11 | 11 | mock "github.com/stretchr/testify/mock" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| @@ -6,8 +6,8 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | 9 | ||||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |||
| 11 | mock "github.com/stretchr/testify/mock" | 11 | mock "github.com/stretchr/testify/mock" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
M internal/boundaries/in/mocks/mock_container_service.go +1 -1
| @@ -6,8 +6,8 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | ||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |
| 11 | 11 | mock "github.com/stretchr/testify/mock" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| @@ -6,8 +6,8 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | 9 | ||||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |||
| 11 | mock "github.com/stretchr/testify/mock" | 11 | mock "github.com/stretchr/testify/mock" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
M internal/boundaries/in/mocks/mock_health_service.go +1 -1
| @@ -6,8 +6,8 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | ||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |
| 11 | 11 | mock "github.com/stretchr/testify/mock" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| @@ -6,8 +6,8 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | 9 | ||||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |||
| 11 | mock "github.com/stretchr/testify/mock" | 11 | mock "github.com/stretchr/testify/mock" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
M internal/boundaries/in/mocks/mock_proxy_service.go +1 -1
| @@ -6,9 +6,9 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | "net/http" | |
| 11 | 10 | ||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |
| 12 | 12 | mock "github.com/stretchr/testify/mock" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| @@ -6,9 +6,9 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | "net/http" | 9 | "net/http" | ||
| 11 | 10 | ||||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |||
| 12 | mock "github.com/stretchr/testify/mock" | 12 | mock "github.com/stretchr/testify/mock" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
M internal/boundaries/in/mocks/mock_registry_service.go +1 -1
| @@ -6,9 +6,9 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | "io" | |
| 11 | 10 | ||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |
| 12 | 12 | mock "github.com/stretchr/testify/mock" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| @@ -6,9 +6,9 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | "io" | 9 | "io" | ||
| 11 | 10 | ||||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |||
| 12 | mock "github.com/stretchr/testify/mock" | 12 | mock "github.com/stretchr/testify/mock" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
M internal/boundaries/in/mocks/mock_secret_service.go +1 -1
| @@ -6,8 +6,8 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/boundaries/out" | |
| 10 | 9 | ||
| 10 | + | "github.com/bnema/gordon/internal/boundaries/out" | |
| 11 | 11 | mock "github.com/stretchr/testify/mock" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| @@ -6,8 +6,8 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/boundaries/out" | |||
| 10 | 9 | ||||
| 10 | + | "github.com/bnema/gordon/internal/boundaries/out" | |||
| 11 | mock "github.com/stretchr/testify/mock" | 11 | mock "github.com/stretchr/testify/mock" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
M internal/boundaries/out/mocks/mock_container_runtime.go +1 -1
| @@ -6,9 +6,9 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | "io" | |
| 11 | 10 | ||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |
| 12 | 12 | mock "github.com/stretchr/testify/mock" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| @@ -6,9 +6,9 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | "io" | 9 | "io" | ||
| 11 | 10 | ||||
| 11 | + | "github.com/bnema/gordon/internal/domain" | |||
| 12 | mock "github.com/stretchr/testify/mock" | 12 | mock "github.com/stretchr/testify/mock" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
M internal/boundaries/out/mocks/mock_domain_secret_store.go +0 -1
| @@ -6,7 +6,6 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "github.com/bnema/gordon/internal/boundaries/out" | |
| 9 | - | ||
| 10 | 9 | mock "github.com/stretchr/testify/mock" | |
| 11 | 10 | ) | |
| 12 | 11 | ||
| @@ -6,7 +6,6 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "github.com/bnema/gordon/internal/boundaries/out" | 8 | "github.com/bnema/gordon/internal/boundaries/out" | ||
| 9 | - | ||||
| 10 | mock "github.com/stretchr/testify/mock" | 9 | mock "github.com/stretchr/testify/mock" | ||
| 11 | ) | 10 | ) | ||
| 12 | 11 | ||||
M internal/boundaries/out/mocks/mock_event_bus.go +0 -1
| @@ -7,7 +7,6 @@ package mocks | |||
| 7 | 7 | import ( | |
| 8 | 8 | "github.com/bnema/gordon/internal/boundaries/out" | |
| 9 | 9 | "github.com/bnema/gordon/internal/domain" | |
| 10 | - | ||
| 11 | 10 | mock "github.com/stretchr/testify/mock" | |
| 12 | 11 | ) | |
| 13 | 12 | ||
| @@ -7,7 +7,6 @@ package mocks | |||||
| 7 | import ( | 7 | import ( | ||
| 8 | "github.com/bnema/gordon/internal/boundaries/out" | 8 | "github.com/bnema/gordon/internal/boundaries/out" | ||
| 9 | "github.com/bnema/gordon/internal/domain" | 9 | "github.com/bnema/gordon/internal/domain" | ||
| 10 | - | ||||
| 11 | mock "github.com/stretchr/testify/mock" | 10 | mock "github.com/stretchr/testify/mock" | ||
| 12 | ) | 11 | ) | ||
| 13 | 12 | ||||
M internal/boundaries/out/mocks/mock_event_handler.go +0 -1
| @@ -6,7 +6,6 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "github.com/bnema/gordon/internal/domain" | |
| 9 | - | ||
| 10 | 9 | mock "github.com/stretchr/testify/mock" | |
| 11 | 10 | ) | |
| 12 | 11 | ||
| @@ -6,7 +6,6 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "github.com/bnema/gordon/internal/domain" | 8 | "github.com/bnema/gordon/internal/domain" | ||
| 9 | - | ||||
| 10 | mock "github.com/stretchr/testify/mock" | 9 | mock "github.com/stretchr/testify/mock" | ||
| 11 | ) | 10 | ) | ||
| 12 | 11 | ||||
M internal/boundaries/out/mocks/mock_event_publisher.go +0 -1
| @@ -6,7 +6,6 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "github.com/bnema/gordon/internal/domain" | |
| 9 | - | ||
| 10 | 9 | mock "github.com/stretchr/testify/mock" | |
| 11 | 10 | ) | |
| 12 | 11 | ||
| @@ -6,7 +6,6 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "github.com/bnema/gordon/internal/domain" | 8 | "github.com/bnema/gordon/internal/domain" | ||
| 9 | - | ||||
| 10 | mock "github.com/stretchr/testify/mock" | 9 | mock "github.com/stretchr/testify/mock" | ||
| 11 | ) | 10 | ) | ||
| 12 | 11 | ||||
M internal/boundaries/out/mocks/mock_event_subscriber.go +0 -1
| @@ -6,7 +6,6 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "github.com/bnema/gordon/internal/boundaries/out" | |
| 9 | - | ||
| 10 | 9 | mock "github.com/stretchr/testify/mock" | |
| 11 | 10 | ) | |
| 12 | 11 | ||
| @@ -6,7 +6,6 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "github.com/bnema/gordon/internal/boundaries/out" | 8 | "github.com/bnema/gordon/internal/boundaries/out" | ||
| 9 | - | ||||
| 10 | mock "github.com/stretchr/testify/mock" | 9 | mock "github.com/stretchr/testify/mock" | ||
| 11 | ) | 10 | ) | ||
| 12 | 11 | ||||
M internal/boundaries/out/mocks/mock_token_store.go +1 -1
| @@ -6,8 +6,8 @@ package mocks | |||
| 6 | 6 | ||
| 7 | 7 | import ( | |
| 8 | 8 | "context" | |
| 9 | - | "github.com/bnema/gordon/internal/domain" | |
| 10 | 9 | ||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |
| 11 | 11 | mock "github.com/stretchr/testify/mock" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| @@ -6,8 +6,8 @@ package mocks | |||||
| 6 | 6 | ||||
| 7 | import ( | 7 | import ( | ||
| 8 | "context" | 8 | "context" | ||
| 9 | - | "github.com/bnema/gordon/internal/domain" | |||
| 10 | 9 | ||||
| 10 | + | "github.com/bnema/gordon/internal/domain" | |||
| 11 | mock "github.com/stretchr/testify/mock" | 11 | mock "github.com/stretchr/testify/mock" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
M internal/domain/auth.go +9 -0
| @@ -264,3 +264,12 @@ func AdminScopeConfig(actions ...string) string { | |||
| 264 | 264 | func AdminScopeStatus(actions ...string) string { | |
| 265 | 265 | return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ",")) | |
| 266 | 266 | } | |
| 267 | + | ||
| 268 | + | // AuthStatus represents status of an authentication session. | |
| 269 | + | type AuthStatus struct { | |
| 270 | + | Valid bool | |
| 271 | + | Subject string | |
| 272 | + | Scopes []string | |
| 273 | + | ExpiresAt int64 // Unix timestamp, 0 if never expires | |
| 274 | + | IssuedAt int64 // Unix timestamp | |
| 275 | + | } | |
| @@ -264,3 +264,12 @@ func AdminScopeConfig(actions ...string) string { | |||||
| 264 | func AdminScopeStatus(actions ...string) string { | 264 | func AdminScopeStatus(actions ...string) string { | ||
| 265 | return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ",")) | 265 | return fmt.Sprintf("%s:%s:%s", ScopeTypeAdmin, AdminResourceStatus, strings.Join(actions, ",")) | ||
| 266 | } | 266 | } | ||
| 267 | + | ||||
| 268 | + | // AuthStatus represents status of an authentication session. | |||
| 269 | + | type AuthStatus struct { | |||
| 270 | + | Valid bool | |||
| 271 | + | Subject string | |||
| 272 | + | Scopes []string | |||
| 273 | + | ExpiresAt int64 // Unix timestamp, 0 if never expires | |||
| 274 | + | IssuedAt int64 // Unix timestamp | |||
| 275 | + | } | |||
M internal/usecase/auth/service.go +29 -0
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "github.com/google/uuid" | |
| 14 | 14 | "golang.org/x/crypto/bcrypt" | |
| 15 | 15 | ||
| 16 | + | "github.com/bnema/gordon/internal/adapters/in/http/middleware" | |
| 16 | 17 | "github.com/bnema/gordon/internal/boundaries/out" | |
| 17 | 18 | "github.com/bnema/gordon/internal/domain" | |
| 18 | 19 | ) | |
| @@ -449,3 +450,31 @@ func getStringClaim(claims jwt.MapClaims, key string) string { | |||
| 449 | 450 | } | |
| 450 | 451 | return "" | |
| 451 | 452 | } | |
| 453 | + | ||
| 454 | + | // GetAuthStatus returns authentication status from context. | |
| 455 | + | // Claims are already validated by AdminAuth middleware and stored in context. | |
| 456 | + | func (s *Service) GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error) { | |
| 457 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |
| 458 | + | zerowrap.FieldLayer: "usecase", | |
| 459 | + | zerowrap.FieldUseCase: "GetAuthStatus", | |
| 460 | + | }) | |
| 461 | + | ||
| 462 | + | // If auth is disabled, return valid status with no claims | |
| 463 | + | if !s.IsEnabled() { | |
| 464 | + | return &domain.AuthStatus{Valid: true}, nil | |
| 465 | + | } | |
| 466 | + | ||
| 467 | + | // Extract claims from context (set by AdminAuth middleware) | |
| 468 | + | claims, ok := ctx.Value(middleware.TokenClaimsKey).(*domain.TokenClaims) | |
| 469 | + | if !ok || claims == nil { | |
| 470 | + | return &domain.AuthStatus{Valid: false}, nil | |
| 471 | + | } | |
| 472 | + | ||
| 473 | + | return &domain.AuthStatus{ | |
| 474 | + | Valid: true, | |
| 475 | + | Subject: claims.Subject, | |
| 476 | + | Scopes: claims.Scopes, | |
| 477 | + | ExpiresAt: claims.ExpiresAt, | |
| 478 | + | IssuedAt: claims.IssuedAt, | |
| 479 | + | }, nil | |
| 480 | + | } | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "github.com/google/uuid" | 13 | "github.com/google/uuid" | ||
| 14 | "golang.org/x/crypto/bcrypt" | 14 | "golang.org/x/crypto/bcrypt" | ||
| 15 | 15 | ||||
| 16 | + | "github.com/bnema/gordon/internal/adapters/in/http/middleware" | |||
| 16 | "github.com/bnema/gordon/internal/boundaries/out" | 17 | "github.com/bnema/gordon/internal/boundaries/out" | ||
| 17 | "github.com/bnema/gordon/internal/domain" | 18 | "github.com/bnema/gordon/internal/domain" | ||
| 18 | ) | 19 | ) | ||
| @@ -449,3 +450,31 @@ func getStringClaim(claims jwt.MapClaims, key string) string { | |||||
| 449 | } | 450 | } | ||
| 450 | return "" | 451 | return "" | ||
| 451 | } | 452 | } | ||
| 453 | + | ||||
| 454 | + | // GetAuthStatus returns authentication status from context. | |||
| 455 | + | // Claims are already validated by AdminAuth middleware and stored in context. | |||
| 456 | + | func (s *Service) GetAuthStatus(ctx context.Context) (*domain.AuthStatus, error) { | |||
| 457 | + | ctx = zerowrap.CtxWithFields(ctx, map[string]any{ | |||
| 458 | + | zerowrap.FieldLayer: "usecase", | |||
| 459 | + | zerowrap.FieldUseCase: "GetAuthStatus", | |||
| 460 | + | }) | |||
| 461 | + | ||||
| 462 | + | // If auth is disabled, return valid status with no claims | |||
| 463 | + | if !s.IsEnabled() { | |||
| 464 | + | return &domain.AuthStatus{Valid: true}, nil | |||
| 465 | + | } | |||
| 466 | + | ||||
| 467 | + | // Extract claims from context (set by AdminAuth middleware) | |||
| 468 | + | claims, ok := ctx.Value(middleware.TokenClaimsKey).(*domain.TokenClaims) | |||
| 469 | + | if !ok || claims == nil { | |||
| 470 | + | return &domain.AuthStatus{Valid: false}, nil | |||
| 471 | + | } | |||
| 472 | + | ||||
| 473 | + | return &domain.AuthStatus{ | |||
| 474 | + | Valid: true, | |||
| 475 | + | Subject: claims.Subject, | |||
| 476 | + | Scopes: claims.Scopes, | |||
| 477 | + | ExpiresAt: claims.ExpiresAt, | |||
| 478 | + | IssuedAt: claims.IssuedAt, | |||
| 479 | + | }, nil | |||
| 480 | + | } | |||