mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-04-18 11:46:09 +00:00
Compare commits
16 Commits
f65df872f0
...
v5.0.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc1d4f2082 | ||
|
|
08e6b84615 | ||
|
|
cec0a7327a | ||
|
|
d7d6a476bf | ||
|
|
c3636784b6 | ||
|
|
da247f8552 | ||
|
|
ce581a76f1 | ||
|
|
7139ad1cc6 | ||
|
|
7bfee6efc2 | ||
|
|
bf4e567a4e | ||
|
|
1926ad9085 | ||
|
|
66ceadf974 | ||
|
|
45276d6f4c | ||
|
|
90766694f0 | ||
|
|
68ddd22aed | ||
|
|
5811218dbf |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "^1.24.0"
|
||||
go-version: "^1.26.0"
|
||||
|
||||
- name: Initialize submodules
|
||||
run: |
|
||||
|
||||
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Install go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "^1.24.0"
|
||||
go-version: "^1.26.0"
|
||||
|
||||
- name: Initialize submodules
|
||||
run: |
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
- name: Install go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "^1.24.0"
|
||||
go-version: "^1.26.0"
|
||||
|
||||
- name: Initialize submodules
|
||||
run: |
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Install go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "^1.24.0"
|
||||
go-version: "^1.26.0"
|
||||
|
||||
- name: Initialize submodules
|
||||
run: |
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
- name: Install go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "^1.24.0"
|
||||
go-version: "^1.26.0"
|
||||
|
||||
- name: Initialize submodules
|
||||
run: |
|
||||
|
||||
@@ -20,7 +20,7 @@ COPY ./frontend/vite.config.ts ./
|
||||
RUN bun run build
|
||||
|
||||
# Builder
|
||||
FROM golang:1.25-alpine3.21 AS builder
|
||||
FROM golang:1.26-alpine3.23 AS builder
|
||||
|
||||
ARG VERSION
|
||||
ARG COMMIT_HASH
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.25-alpine3.21
|
||||
FROM golang:1.26-alpine3.23
|
||||
|
||||
WORKDIR /tinyauth
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ COPY ./frontend/vite.config.ts ./
|
||||
RUN bun run build
|
||||
|
||||
# Builder
|
||||
FROM golang:1.25-alpine3.21 AS builder
|
||||
FROM golang:1.26-alpine3.23 AS builder
|
||||
|
||||
ARG VERSION
|
||||
ARG COMMIT_HASH
|
||||
|
||||
@@ -58,7 +58,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
|
||||
|
||||
A big thank you to the following people for providing me with more coffee:
|
||||
|
||||
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/algorist-ahmad"><img src="https://github.com/algorist-ahmad.png" width="64px" alt="User avatar: algorist-ahmad" /></a> <!-- sponsors -->
|
||||
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/ax-mad"><img src="https://github.com/ax-mad.png" width="64px" alt="User avatar: ax-mad" /></a> <!-- sponsors -->
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"charm.land/huh/v2"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/traefik/paerser/cli"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -61,9 +61,8 @@ func createUserCmd() *cli.Command {
|
||||
),
|
||||
)
|
||||
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
|
||||
err := form.WithTheme(baseTheme).Run()
|
||||
theme := new(themeBase)
|
||||
err := form.WithTheme(theme).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"charm.land/huh/v2"
|
||||
"github.com/mdp/qrterminal/v3"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/traefik/paerser/cli"
|
||||
@@ -54,9 +54,8 @@ func generateTotpCmd() *cli.Command {
|
||||
),
|
||||
)
|
||||
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
|
||||
err := form.WithTheme(baseTheme).Run()
|
||||
theme := new(themeBase)
|
||||
err := form.WithTheme(theme).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
type healthzResponse struct {
|
||||
Status string `json:"status"`
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/loaders"
|
||||
@@ -123,3 +124,9 @@ func runCmd(cfg config.Config) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type themeBase struct{}
|
||||
|
||||
func (t *themeBase) Theme(isDark bool) *huh.Styles {
|
||||
return huh.ThemeBase(isDark)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"charm.land/huh/v2"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/traefik/paerser/cli"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -71,9 +71,8 @@ func verifyUserCmd() *cli.Command {
|
||||
),
|
||||
)
|
||||
|
||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||
|
||||
err := form.WithTheme(baseTheme).Run()
|
||||
theme := new(themeBase)
|
||||
err := form.WithTheme(theme).Run()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run interactive prompt: %w", err)
|
||||
|
||||
@@ -12,21 +12,21 @@
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@tanstack/react-query": "^5.95.2",
|
||||
"axios": "^1.13.6",
|
||||
"@tanstack/react-query": "^5.96.1",
|
||||
"axios": "^1.14.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^25.10.5",
|
||||
"i18next": "^26.0.3",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.577.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.72.0",
|
||||
"react-i18next": "^16.6.2",
|
||||
"react-i18next": "^17.0.2",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.13.2",
|
||||
"sonner": "^2.0.7",
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.95.2",
|
||||
"@tanstack/eslint-plugin-query": "^5.96.1",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
@@ -48,8 +48,8 @@
|
||||
"prettier": "3.8.1",
|
||||
"rollup-plugin-visualizer": "^7.0.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^8.0.3",
|
||||
},
|
||||
},
|
||||
@@ -369,11 +369,11 @@
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.2", "", { "dependencies": { "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "tailwindcss": "4.2.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w=="],
|
||||
|
||||
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.95.2", "", { "dependencies": { "@typescript-eslint/utils": "^8.48.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "^5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-EYUFRaqjBep4EHMPpZR12sXP7Kr5qv9iDIlq93NfbhHwhITaW6Txu3ROO6dLFz5r84T8p+oZXBG77pa2Wuok7A=="],
|
||||
"@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.96.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.48.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": "^5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-BDJU+Q+zESjarSSFmbzpCBh+1wDxwW+DyQlvwIukF24MHYOoRPH4ouJRTlDdbp3BnIkeylZaHHSgIvxY9lgI/g=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.95.2", "", {}, "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ=="],
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.96.1", "", {}, "sha512-u1yBgtavSy+N8wgtW3PiER6UpxcplMje65yXnnVgiHTqiMwLlxiw4WvQDrXyn+UD6lnn8kHaxmerJUzQcV/MMg=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.95.2", "", { "dependencies": { "@tanstack/query-core": "5.95.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA=="],
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.96.1", "", { "dependencies": { "@tanstack/query-core": "5.96.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-2X7KYK5KKWUKGeWCVcqxXAkYefJtrKB7tSKWgeG++b0H6BRHxQaLSSi8AxcgjmUnnosHuh9WsFZqvE16P1WCzA=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
@@ -401,25 +401,25 @@
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/type-utils": "8.57.2", "@typescript-eslint/utils": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w=="],
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA=="],
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.2", "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw=="],
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="],
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw=="],
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg=="],
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="],
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.2", "@typescript-eslint/tsconfig-utils": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA=="],
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="],
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw=="],
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
@@ -439,7 +439,7 @@
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="],
|
||||
"axios": ["axios@1.14.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ=="],
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
@@ -611,7 +611,7 @@
|
||||
|
||||
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
|
||||
|
||||
"i18next": ["i18next@25.10.5", "", { "dependencies": { "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-jRnF7eRNsdcnh7AASSgaU3lj/8lJZuHkfsouetnLEDH0xxE1vVi7qhiJ9RhdSPUyzg4ltb7P7aXsFlTk9sxL2w=="],
|
||||
"i18next": ["i18next@26.0.3", "", { "dependencies": { "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg=="],
|
||||
|
||||
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="],
|
||||
|
||||
@@ -697,7 +697,7 @@
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="],
|
||||
"lucide-react": ["lucide-react@1.7.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
@@ -805,7 +805,7 @@
|
||||
|
||||
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
@@ -817,7 +817,7 @@
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.72.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-V4v6jubaf6JAurEaVnT9aUPKFbNtDgohj5CIgVGyPHvT9wRx5OZHVjz31GsxnPNI278XMu+ruFz+wGOscHaLKw=="],
|
||||
|
||||
"react-i18next": ["react-i18next@16.6.2", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-/S/GPzElTqEi5o2kzd0/O2627hPDmE6OGhJCCwCfUaQ3syyu+kaYH8/PYFtZeWc25NzfxTN/2fD1QjvrTgrFfA=="],
|
||||
"react-i18next": ["react-i18next@17.0.2", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.0.1", "react": ">= 16.8.0", "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-shBftH2vaTWK2Bsp7FiL+cevx3xFJlvFxmsDFQSrJc+6twHkP0tv/bGa01VVWzpreUVVwU+3Hev5iFqRg65RwA=="],
|
||||
|
||||
"react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
|
||||
|
||||
@@ -881,7 +881,7 @@
|
||||
|
||||
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
|
||||
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
@@ -889,9 +889,9 @@
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.57.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.2", "@typescript-eslint/parser": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2", "@typescript-eslint/utils": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A=="],
|
||||
"typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
@@ -1015,31 +1015,31 @@
|
||||
|
||||
"@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="],
|
||||
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
|
||||
|
||||
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
|
||||
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
|
||||
|
||||
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
|
||||
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
|
||||
|
||||
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="],
|
||||
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="],
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
|
||||
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="],
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.2", "@typescript-eslint/tsconfig-utils": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
|
||||
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
|
||||
|
||||
"eslint-plugin-react-hooks/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
|
||||
|
||||
@@ -1061,7 +1061,7 @@
|
||||
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="],
|
||||
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="],
|
||||
|
||||
"vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
@@ -1079,23 +1079,25 @@
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
|
||||
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="],
|
||||
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
|
||||
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="],
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.2", "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw=="],
|
||||
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="],
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw=="],
|
||||
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="],
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw=="],
|
||||
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.2", "", { "dependencies": { "@typescript-eslint/types": "8.57.2", "@typescript-eslint/visitor-keys": "8.57.2" } }, "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw=="],
|
||||
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.57.2", "", {}, "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA=="],
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
|
||||
|
||||
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
|
||||
|
||||
"@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
|
||||
@@ -18,21 +18,21 @@
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@tanstack/react-query": "^5.95.2",
|
||||
"axios": "^1.13.6",
|
||||
"@tanstack/react-query": "^5.96.1",
|
||||
"axios": "^1.14.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"i18next": "^25.10.5",
|
||||
"i18next": "^26.0.3",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.577.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.72.0",
|
||||
"react-i18next": "^16.6.2",
|
||||
"react-i18next": "^17.0.2",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.13.2",
|
||||
"sonner": "^2.0.7",
|
||||
@@ -42,7 +42,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.95.2",
|
||||
"@tanstack/eslint-plugin-query": "^5.96.1",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
@@ -54,8 +54,8 @@
|
||||
"prettier": "3.8.1",
|
||||
"rollup-plugin-visualizer": "^7.0.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^8.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@
|
||||
"fieldRequired": "Це поле обов'язкове для заповнення",
|
||||
"invalidInput": "Невірне введення",
|
||||
"domainWarningTitle": "Невірний домен",
|
||||
"domainWarningSubtitle": "Даний ресурс налаштований для доступу з <code>{{appUrl}}</code>, але використовується <code>{{currentUrl}}</code>. Якщо ви продовжите, можуть виникнути проблеми з автентифікацією.",
|
||||
"domainWarningCurrent": "Current:",
|
||||
"domainWarningExpected": "Expected:",
|
||||
"domainWarningSubtitle": "Ви отримуєте доступ до даного екземпляра з неправильного домену. Якщо ви продовжите, у вас можуть виникнути проблеми з автентифікацією.",
|
||||
"domainWarningCurrent": "Поточний:",
|
||||
"domainWarningExpected": "Очікувалося:",
|
||||
"ignoreTitle": "Ігнорувати",
|
||||
"goToCorrectDomainTitle": "Перейти за коректним доменом",
|
||||
"authorizeTitle": "Авторизуватись",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Resolve paths
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
},
|
||||
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
@@ -26,7 +25,7 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
"noUncheckedSideEffectImports": true,
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src"],
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
"@/*": ["./src/*"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
41
go.mod
41
go.mod
@@ -1,32 +1,36 @@
|
||||
module github.com/steveiliop56/tinyauth
|
||||
|
||||
go 1.25.0
|
||||
go 1.26.0
|
||||
|
||||
replace github.com/traefik/paerser v0.2.2 => ./paerser
|
||||
|
||||
require (
|
||||
charm.land/huh/v2 v2.0.3
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/charmbracelet/huh v0.8.0
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/go-jose/go-jose/v4 v4.1.3
|
||||
github.com/go-jose/go-jose/v4 v4.1.4
|
||||
github.com/go-ldap/ldap/v3 v3.4.13
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||
github.com/google/go-querystring v1.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mdp/qrterminal/v3 v3.2.1
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/rs/zerolog v1.35.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/traefik/paerser v0.2.2
|
||||
github.com/weppos/publicsuffix-go v0.50.3
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
gotest.tools/v3 v3.5.2
|
||||
modernc.org/sqlite v1.47.0
|
||||
modernc.org/sqlite v1.48.0
|
||||
)
|
||||
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.0.0 // indirect
|
||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
||||
charm.land/lipgloss/v2 v2.0.1 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
@@ -34,29 +38,30 @@ require (
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.2 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.3.6 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
@@ -74,11 +79,10 @@ require (
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
@@ -88,14 +92,13 @@ require (
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
|
||||
96
go.sum
96
go.sum
@@ -1,3 +1,11 @@
|
||||
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
||||
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
||||
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
||||
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
||||
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
|
||||
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
||||
charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys=
|
||||
charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||
@@ -19,10 +27,8 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
|
||||
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
@@ -38,34 +44,34 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
|
||||
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
|
||||
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
|
||||
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
|
||||
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
||||
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
||||
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
|
||||
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
||||
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/charmbracelet/x/xpty v0.1.3 h1:eGSitii4suhzrISYH50ZfufV3v085BXQwIytcOdFSsw=
|
||||
github.com/charmbracelet/x/xpty v0.1.3/go.mod h1:poPYpWuLDBFCKmKLDnhBp51ATa0ooD8FhypRwEFtH3Y=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
@@ -74,7 +80,6 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -91,8 +96,6 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
@@ -105,8 +108,8 @@ github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -126,7 +129,6 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
@@ -175,19 +177,14 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||
@@ -215,12 +212,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -242,14 +235,12 @@ github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SA
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
||||
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
@@ -331,13 +322,10 @@ golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -401,8 +389,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
|
||||
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/sqlite v1.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4=
|
||||
modernc.org/sqlite v1.48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -2,152 +2,131 @@ package controller_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gotest.tools/v3/assert"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var contextControllerCfg = controller.ContextControllerConfig{
|
||||
Providers: []controller.Provider{
|
||||
func TestContextController(t *testing.T) {
|
||||
controllerConfig := controller.ContextControllerConfig{
|
||||
Providers: []controller.Provider{
|
||||
{
|
||||
Name: "Local",
|
||||
ID: "local",
|
||||
OAuth: false,
|
||||
},
|
||||
},
|
||||
Title: "Tinyauth",
|
||||
AppURL: "https://tinyauth.example.com",
|
||||
CookieDomain: "example.com",
|
||||
ForgotPasswordMessage: "foo",
|
||||
BackgroundImage: "/background.jpg",
|
||||
OAuthAutoRedirect: "none",
|
||||
WarningsEnabled: true,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
middlewares []gin.HandlerFunc
|
||||
expected string
|
||||
path string
|
||||
}{
|
||||
{
|
||||
Name: "Local",
|
||||
ID: "local",
|
||||
OAuth: false,
|
||||
description: "Ensure context controller returns app context",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
path: "/api/context/app",
|
||||
expected: func() string {
|
||||
expectedAppContextResponse := controller.AppContextResponse{
|
||||
Status: 200,
|
||||
Message: "Success",
|
||||
Providers: controllerConfig.Providers,
|
||||
Title: controllerConfig.Title,
|
||||
AppURL: controllerConfig.AppURL,
|
||||
CookieDomain: controllerConfig.CookieDomain,
|
||||
ForgotPasswordMessage: controllerConfig.ForgotPasswordMessage,
|
||||
BackgroundImage: controllerConfig.BackgroundImage,
|
||||
OAuthAutoRedirect: controllerConfig.OAuthAutoRedirect,
|
||||
WarningsEnabled: controllerConfig.WarningsEnabled,
|
||||
}
|
||||
bytes, err := json.Marshal(expectedAppContextResponse)
|
||||
assert.NoError(t, err)
|
||||
return string(bytes)
|
||||
}(),
|
||||
},
|
||||
{
|
||||
Name: "Google",
|
||||
ID: "google",
|
||||
OAuth: true,
|
||||
description: "Ensure user context returns 401 when unauthorized",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
path: "/api/context/user",
|
||||
expected: func() string {
|
||||
expectedUserContextResponse := controller.UserContextResponse{
|
||||
Status: 401,
|
||||
Message: "Unauthorized",
|
||||
}
|
||||
bytes, err := json.Marshal(expectedUserContextResponse)
|
||||
assert.NoError(t, err)
|
||||
return string(bytes)
|
||||
}(),
|
||||
},
|
||||
},
|
||||
Title: "Test App",
|
||||
AppURL: "http://localhost:8080",
|
||||
CookieDomain: "localhost",
|
||||
ForgotPasswordMessage: "Contact admin to reset your password.",
|
||||
BackgroundImage: "/assets/bg.jpg",
|
||||
OAuthAutoRedirect: "google",
|
||||
WarningsEnabled: true,
|
||||
}
|
||||
|
||||
var contextCtrlTestContext = config.UserContext{
|
||||
Username: "testuser",
|
||||
Name: "testuser",
|
||||
Email: "test@example.com",
|
||||
IsLoggedIn: true,
|
||||
IsBasicAuth: false,
|
||||
OAuth: false,
|
||||
Provider: "local",
|
||||
TotpPending: false,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: false,
|
||||
OAuthSub: "",
|
||||
}
|
||||
|
||||
func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
// Setup
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.Default()
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
if middlewares != nil {
|
||||
for _, m := range *middlewares {
|
||||
router.Use(m)
|
||||
}
|
||||
}
|
||||
|
||||
group := router.Group("/api")
|
||||
|
||||
ctrl := controller.NewContextController(contextControllerCfg, group)
|
||||
ctrl.SetupRoutes()
|
||||
|
||||
return router, recorder
|
||||
}
|
||||
|
||||
func TestAppContextHandler(t *testing.T) {
|
||||
expectedRes := controller.AppContextResponse{
|
||||
Status: 200,
|
||||
Message: "Success",
|
||||
Providers: contextControllerCfg.Providers,
|
||||
Title: contextControllerCfg.Title,
|
||||
AppURL: contextControllerCfg.AppURL,
|
||||
CookieDomain: contextControllerCfg.CookieDomain,
|
||||
ForgotPasswordMessage: contextControllerCfg.ForgotPasswordMessage,
|
||||
BackgroundImage: contextControllerCfg.BackgroundImage,
|
||||
OAuthAutoRedirect: contextControllerCfg.OAuthAutoRedirect,
|
||||
WarningsEnabled: contextControllerCfg.WarningsEnabled,
|
||||
}
|
||||
|
||||
router, recorder := setupContextController(nil)
|
||||
req := httptest.NewRequest("GET", "/api/context/app", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var ctrlRes controller.AppContextResponse
|
||||
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, expectedRes, ctrlRes)
|
||||
}
|
||||
|
||||
func TestUserContextHandler(t *testing.T) {
|
||||
expectedRes := controller.UserContextResponse{
|
||||
Status: 200,
|
||||
Message: "Success",
|
||||
IsLoggedIn: contextCtrlTestContext.IsLoggedIn,
|
||||
Username: contextCtrlTestContext.Username,
|
||||
Name: contextCtrlTestContext.Name,
|
||||
Email: contextCtrlTestContext.Email,
|
||||
Provider: contextCtrlTestContext.Provider,
|
||||
OAuth: contextCtrlTestContext.OAuth,
|
||||
TotpPending: contextCtrlTestContext.TotpPending,
|
||||
OAuthName: contextCtrlTestContext.OAuthName,
|
||||
}
|
||||
|
||||
// Test with context
|
||||
router, recorder := setupContextController(&[]gin.HandlerFunc{
|
||||
func(c *gin.Context) {
|
||||
c.Set("context", &contextCtrlTestContext)
|
||||
c.Next()
|
||||
{
|
||||
description: "Ensure user context returns when authorized",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
func(c *gin.Context) {
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: "johndoe",
|
||||
Name: "John Doe",
|
||||
Email: utils.CompileUserEmail("johndoe", controllerConfig.CookieDomain),
|
||||
Provider: "local",
|
||||
IsLoggedIn: true,
|
||||
})
|
||||
},
|
||||
},
|
||||
path: "/api/context/user",
|
||||
expected: func() string {
|
||||
expectedUserContextResponse := controller.UserContextResponse{
|
||||
Status: 200,
|
||||
Message: "Success",
|
||||
IsLoggedIn: true,
|
||||
Username: "johndoe",
|
||||
Name: "John Doe",
|
||||
Email: utils.CompileUserEmail("johndoe", controllerConfig.CookieDomain),
|
||||
Provider: "local",
|
||||
}
|
||||
bytes, err := json.Marshal(expectedUserContextResponse)
|
||||
assert.NoError(t, err)
|
||||
return string(bytes)
|
||||
}(),
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/context/user", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var ctrlRes controller.UserContextResponse
|
||||
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, expectedRes, ctrlRes)
|
||||
|
||||
// Test no context
|
||||
expectedRes = controller.UserContextResponse{
|
||||
Status: 401,
|
||||
Message: "Unauthorized",
|
||||
IsLoggedIn: false,
|
||||
}
|
||||
|
||||
router, recorder = setupContextController(nil)
|
||||
req = httptest.NewRequest("GET", "/api/context/user", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
router := gin.Default()
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
for _, middleware := range test.middlewares {
|
||||
router.Use(middleware)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
|
||||
group := router.Group("/api")
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, expectedRes, ctrlRes)
|
||||
contextController := controller.NewContextController(controllerConfig, group)
|
||||
contextController.SetupRoutes()
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
request, err := http.NewRequest("GET", test.path, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
assert.Equal(t, test.expected, recorder.Body.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func (controller *HealthController) SetupRoutes() {
|
||||
|
||||
func (controller *HealthController) healthHandler(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"status": 200,
|
||||
"message": "Healthy",
|
||||
})
|
||||
}
|
||||
|
||||
71
internal/controller/health_controller_test.go
Normal file
71
internal/controller/health_controller_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHealthController(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
path string
|
||||
method string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
description: "Ensure health endpoint returns 200 OK",
|
||||
path: "/api/healthz",
|
||||
method: "GET",
|
||||
expected: func() string {
|
||||
expectedHealthResponse := map[string]any{
|
||||
"status": 200,
|
||||
"message": "Healthy",
|
||||
}
|
||||
bytes, err := json.Marshal(expectedHealthResponse)
|
||||
assert.NoError(t, err)
|
||||
return string(bytes)
|
||||
}(),
|
||||
},
|
||||
{
|
||||
description: "Ensure health endpoint returns 200 OK for HEAD request",
|
||||
path: "/api/healthz",
|
||||
method: "HEAD",
|
||||
expected: func() string {
|
||||
expectedHealthResponse := map[string]any{
|
||||
"status": 200,
|
||||
"message": "Healthy",
|
||||
}
|
||||
bytes, err := json.Marshal(expectedHealthResponse)
|
||||
assert.NoError(t, err)
|
||||
return string(bytes)
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
router := gin.Default()
|
||||
group := router.Group("/api")
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
healthController := controller.NewHealthController(group)
|
||||
healthController.SetupRoutes()
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
request, err := http.NewRequest(test.method, test.path, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
assert.Equal(t, test.expected, recorder.Body.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -206,11 +206,17 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if service.ID() != req.Provider {
|
||||
tlog.App.Error().Msgf("OAuth service ID mismatch: expected %s, got %s", service.ID(), req.Provider)
|
||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||
return
|
||||
}
|
||||
|
||||
sessionCookie := repository.Session{
|
||||
Username: username,
|
||||
Name: name,
|
||||
Email: user.Email,
|
||||
Provider: req.Provider,
|
||||
Provider: service.ID(),
|
||||
OAuthGroups: utils.CoalesceToString(user.Groups),
|
||||
OAuthName: service.Name(),
|
||||
OAuthSub: user.Sub,
|
||||
|
||||
@@ -235,7 +235,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
|
||||
if !ok {
|
||||
tlog.App.Error().Msg("Missing authorization header")
|
||||
c.Header("www-authenticate", "basic")
|
||||
c.Header("www-authenticate", `Basic realm="Tinyauth OIDC Token Endpoint"`)
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_client",
|
||||
})
|
||||
|
||||
@@ -2,10 +2,9 @@ package controller_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -16,266 +15,456 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"gotest.tools/v3/assert"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var oidcServiceConfig = service.OIDCServiceConfig{
|
||||
Clients: map[string]config.OIDCClientConfig{
|
||||
"client1": {
|
||||
ClientID: "some-client-id",
|
||||
ClientSecret: "some-client-secret",
|
||||
ClientSecretFile: "",
|
||||
TrustedRedirectURIs: []string{
|
||||
"https://example.com/oauth/callback",
|
||||
},
|
||||
Name: "Client 1",
|
||||
},
|
||||
},
|
||||
PrivateKeyPath: "/tmp/tinyauth_oidc_key",
|
||||
PublicKeyPath: "/tmp/tinyauth_oidc_key.pub",
|
||||
Issuer: "https://example.com",
|
||||
SessionExpiry: 3600,
|
||||
}
|
||||
|
||||
var oidcCtrlTestContext = config.UserContext{
|
||||
Username: "test",
|
||||
Name: "Test",
|
||||
Email: "test@example.com",
|
||||
IsLoggedIn: true,
|
||||
IsBasicAuth: false,
|
||||
OAuth: false,
|
||||
Provider: "ldap", // ldap in order to test the groups
|
||||
TotpPending: false,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: false,
|
||||
OAuthName: "",
|
||||
OAuthSub: "",
|
||||
LdapGroups: "test1,test2",
|
||||
}
|
||||
|
||||
// Test is not amazing, but it will confirm the OIDC server works
|
||||
func TestOIDCController(t *testing.T) {
|
||||
tlog.NewSimpleLogger().Init()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create an app instance
|
||||
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||
|
||||
// Get db
|
||||
db, err := app.SetupDatabase("/tmp/tinyauth.db")
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Create queries
|
||||
queries := repository.New(db)
|
||||
|
||||
// Create a new OIDC Servicee
|
||||
oidcService := service.NewOIDCService(oidcServiceConfig, queries)
|
||||
err = oidcService.Init()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Create test router
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.Default()
|
||||
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Set("context", &oidcCtrlTestContext)
|
||||
c.Next()
|
||||
})
|
||||
|
||||
group := router.Group("/api")
|
||||
|
||||
// Register oidc controller
|
||||
oidcController := controller.NewOIDCController(controller.OIDCControllerConfig{}, oidcService, group)
|
||||
oidcController.SetupRoutes()
|
||||
|
||||
// Get redirect URL test
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
marshalled, err := json.Marshal(service.AuthorizeRequest{
|
||||
Scope: "openid profile email groups",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://example.com/oauth/callback",
|
||||
State: "some-state",
|
||||
})
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
req, err := http.NewRequest("POST", "/api/oidc/authorize", strings.NewReader(string(marshalled)))
|
||||
assert.NilError(t, err)
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
resJson := map[string]any{}
|
||||
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &resJson)
|
||||
assert.NilError(t, err)
|
||||
|
||||
redirect_uri, ok := resJson["redirect_uri"].(string)
|
||||
assert.Assert(t, ok)
|
||||
|
||||
u, err := url.Parse(redirect_uri)
|
||||
assert.NilError(t, err)
|
||||
|
||||
m, err := url.ParseQuery(u.RawQuery)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, m["state"][0], "some-state")
|
||||
|
||||
code := m["code"][0]
|
||||
|
||||
// Exchange code for token
|
||||
recorder = httptest.NewRecorder()
|
||||
|
||||
params, err := query.Values(controller.TokenRequest{
|
||||
GrantType: "authorization_code",
|
||||
Code: code,
|
||||
RedirectURI: "https://example.com/oauth/callback",
|
||||
})
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
req, err = http.NewRequest("POST", "/api/oidc/token", strings.NewReader(params.Encode()))
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("content-type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
resJson = map[string]any{}
|
||||
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &resJson)
|
||||
assert.NilError(t, err)
|
||||
|
||||
accessToken, ok := resJson["access_token"].(string)
|
||||
assert.Assert(t, ok)
|
||||
|
||||
_, ok = resJson["id_token"].(string)
|
||||
assert.Assert(t, ok)
|
||||
|
||||
refreshToken, ok := resJson["refresh_token"].(string)
|
||||
assert.Assert(t, ok)
|
||||
|
||||
expires_in, ok := resJson["expires_in"].(float64)
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, expires_in, float64(oidcServiceConfig.SessionExpiry))
|
||||
|
||||
// Ensure code is expired
|
||||
recorder = httptest.NewRecorder()
|
||||
|
||||
params, err = query.Values(controller.TokenRequest{
|
||||
GrantType: "authorization_code",
|
||||
Code: code,
|
||||
RedirectURI: "https://example.com/oauth/callback",
|
||||
})
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
req, err = http.NewRequest("POST", "/api/oidc/token", strings.NewReader(params.Encode()))
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("content-type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusBadRequest, recorder.Code)
|
||||
|
||||
// Test userinfo
|
||||
recorder = httptest.NewRecorder()
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
resJson = map[string]any{}
|
||||
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &resJson)
|
||||
assert.NilError(t, err)
|
||||
|
||||
_, ok = resJson["sub"].(string)
|
||||
assert.Assert(t, ok)
|
||||
|
||||
name, ok := resJson["name"].(string)
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, name, oidcCtrlTestContext.Name)
|
||||
|
||||
email, ok := resJson["email"].(string)
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, email, oidcCtrlTestContext.Email)
|
||||
|
||||
preferred_username, ok := resJson["preferred_username"].(string)
|
||||
assert.Assert(t, ok)
|
||||
assert.Equal(t, preferred_username, oidcCtrlTestContext.Username)
|
||||
|
||||
// Not sure why this is failing, will look into it later
|
||||
igroups, ok := resJson["groups"].([]any)
|
||||
assert.Assert(t, ok)
|
||||
|
||||
groups := make([]string, len(igroups))
|
||||
for i, group := range igroups {
|
||||
groups[i], ok = group.(string)
|
||||
assert.Assert(t, ok)
|
||||
oidcServiceCfg := service.OIDCServiceConfig{
|
||||
Clients: map[string]config.OIDCClientConfig{
|
||||
"test": {
|
||||
ClientID: "some-client-id",
|
||||
ClientSecret: "some-client-secret",
|
||||
TrustedRedirectURIs: []string{"https://test.example.com/callback"},
|
||||
Name: "Test Client",
|
||||
},
|
||||
},
|
||||
PrivateKeyPath: path.Join(tempDir, "key.pem"),
|
||||
PublicKeyPath: path.Join(tempDir, "key.pub"),
|
||||
Issuer: "https://tinyauth.example.com",
|
||||
SessionExpiry: 500,
|
||||
}
|
||||
|
||||
assert.DeepEqual(t, strings.Split(oidcCtrlTestContext.LdapGroups, ","), groups)
|
||||
controllerCfg := controller.OIDCControllerConfig{}
|
||||
|
||||
// Test refresh token
|
||||
recorder = httptest.NewRecorder()
|
||||
simpleCtx := func(c *gin.Context) {
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: "test",
|
||||
Name: "Test User",
|
||||
Email: "test@example.com",
|
||||
IsLoggedIn: true,
|
||||
Provider: "local",
|
||||
})
|
||||
c.Next()
|
||||
}
|
||||
|
||||
params, err = query.Values(controller.TokenRequest{
|
||||
GrantType: "refresh_token",
|
||||
RefreshToken: refreshToken,
|
||||
type testCase struct {
|
||||
description string
|
||||
middlewares []gin.HandlerFunc
|
||||
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
|
||||
}
|
||||
|
||||
var tests []testCase
|
||||
|
||||
getTestByDescription := func(description string) (func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder), bool) {
|
||||
for _, test := range tests {
|
||||
if test.description == description {
|
||||
return test.run, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
tests = []testCase{
|
||||
{
|
||||
description: "Ensure we can fetch the client",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/clients/some-client-id", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure API fails on non-existent client ID",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/clients/non-existent-client-id", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 404, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure authorize fails with empty context",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("POST", "/api/oidc/authorize", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, res["redirect_uri"], "https://tinyauth.example.com/error?error=User+is+not+logged+in+or+the+session+is+invalid")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure authorize fails with an invalid param",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
reqBody := service.AuthorizeRequest{
|
||||
Scope: "openid",
|
||||
ResponseType: "some_unsupported_response_type",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
State: "some-state",
|
||||
Nonce: "some-nonce",
|
||||
}
|
||||
reqBodyBytes, err := json.Marshal(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/authorize", strings.NewReader(string(reqBodyBytes)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
var res map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, res["redirect_uri"], "https://test.example.com/callback?error=unsupported_response_type&error_description=Invalid+request+parameters&state=some-state")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure authorize succeeds with valid params",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
reqBody := service.AuthorizeRequest{
|
||||
Scope: "openid",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
State: "some-state",
|
||||
Nonce: "some-nonce",
|
||||
}
|
||||
reqBodyBytes, err := json.Marshal(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/authorize", strings.NewReader(string(reqBodyBytes)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
redirectURI := res["redirect_uri"].(string)
|
||||
url, err := url.Parse(redirectURI)
|
||||
assert.NoError(t, err)
|
||||
|
||||
queryParams := url.Query()
|
||||
assert.Equal(t, queryParams.Get("state"), "some-state")
|
||||
|
||||
code := queryParams.Get("code")
|
||||
assert.NotEmpty(t, code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure token request fails with invalid grant",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
reqBody := controller.TokenRequest{
|
||||
GrantType: "invalid_grant",
|
||||
Code: "",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
}
|
||||
reqBodyEncoded, err := query.Values(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
var res map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, res["error"], "unsupported_grant_type")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure token endpoint accepts basic auth",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
reqBody := controller.TokenRequest{
|
||||
GrantType: "authorization_code",
|
||||
Code: "some-code",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
}
|
||||
reqBodyEncoded, err := query.Values(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Empty(t, recorder.Header().Get("www-authenticate"))
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure token endpoint accepts form auth",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
form := url.Values{}
|
||||
form.Set("grant_type", "authorization_code")
|
||||
form.Set("code", "some-code")
|
||||
form.Set("redirect_uri", "https://test.example.com/callback")
|
||||
form.Set("client_id", "some-client-id")
|
||||
form.Set("client_secret", "some-client-secret")
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Empty(t, recorder.Header().Get("www-authenticate"))
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure token endpoint sets authenticate header when no auth is available",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
reqBody := controller.TokenRequest{
|
||||
GrantType: "authorization_code",
|
||||
Code: "some-code",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
}
|
||||
reqBodyEncoded, err := query.Values(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
authHeader := recorder.Header().Get("www-authenticate")
|
||||
assert.Contains(t, authHeader, "Basic")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure we can get a token with a valid request",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
authorizeCodeTest, found := getTestByDescription("Ensure authorize succeeds with valid params")
|
||||
assert.True(t, found, "Authorize test not found")
|
||||
authorizeTestRecorder := httptest.NewRecorder()
|
||||
authorizeCodeTest(t, router, authorizeTestRecorder)
|
||||
|
||||
var authorizeRes map[string]any
|
||||
err := json.Unmarshal(authorizeTestRecorder.Body.Bytes(), &authorizeRes)
|
||||
assert.NoError(t, err)
|
||||
|
||||
redirectURI := authorizeRes["redirect_uri"].(string)
|
||||
url, err := url.Parse(redirectURI)
|
||||
assert.NoError(t, err)
|
||||
|
||||
queryParams := url.Query()
|
||||
code := queryParams.Get("code")
|
||||
assert.NotEmpty(t, code)
|
||||
|
||||
reqBody := controller.TokenRequest{
|
||||
GrantType: "authorization_code",
|
||||
Code: code,
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
}
|
||||
reqBodyEncoded, err := query.Values(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure we can renew the access token with the refresh token",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
tokenTest, found := getTestByDescription("Ensure we can get a token with a valid request")
|
||||
assert.True(t, found, "Token test not found")
|
||||
tokenRecorder := httptest.NewRecorder()
|
||||
tokenTest(t, router, tokenRecorder)
|
||||
|
||||
var tokenRes map[string]any
|
||||
err := json.Unmarshal(tokenRecorder.Body.Bytes(), &tokenRes)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, ok := tokenRes["refresh_token"]
|
||||
assert.True(t, ok, "Expected refresh token in response")
|
||||
refreshToken := tokenRes["refresh_token"].(string)
|
||||
assert.NotEmpty(t, refreshToken)
|
||||
|
||||
reqBody := controller.TokenRequest{
|
||||
GrantType: "refresh_token",
|
||||
RefreshToken: refreshToken,
|
||||
ClientID: "some-client-id",
|
||||
ClientSecret: "some-client-secret",
|
||||
}
|
||||
reqBodyEncoded, err := query.Values(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.NotEmpty(t, recorder.Header().Get("cache-control"))
|
||||
assert.NotEmpty(t, recorder.Header().Get("pragma"))
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
var refreshRes map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &refreshRes)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, ok = refreshRes["access_token"]
|
||||
assert.True(t, ok, "Expected access token in refresh response")
|
||||
assert.NotEqual(t, tokenRes["refresh_token"].(string), refreshRes["access_token"].(string))
|
||||
assert.NotEqual(t, tokenRes["access_token"].(string), refreshRes["access_token"].(string))
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure token endpoint deletes code after use",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
authorizeCodeTest, found := getTestByDescription("Ensure authorize succeeds with valid params")
|
||||
assert.True(t, found, "Authorize test not found")
|
||||
authorizeTestRecorder := httptest.NewRecorder()
|
||||
authorizeCodeTest(t, router, authorizeTestRecorder)
|
||||
|
||||
var authorizeRes map[string]any
|
||||
err := json.Unmarshal(authorizeTestRecorder.Body.Bytes(), &authorizeRes)
|
||||
assert.NoError(t, err)
|
||||
|
||||
redirectURI := authorizeRes["redirect_uri"].(string)
|
||||
url, err := url.Parse(redirectURI)
|
||||
assert.NoError(t, err)
|
||||
|
||||
queryParams := url.Query()
|
||||
code := queryParams.Get("code")
|
||||
assert.NotEmpty(t, code)
|
||||
|
||||
reqBody := controller.TokenRequest{
|
||||
GrantType: "authorization_code",
|
||||
Code: code,
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
}
|
||||
reqBodyEncoded, err := query.Values(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
// Try to use the same code again
|
||||
secondReq := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
|
||||
secondReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
secondReq.SetBasicAuth("some-client-id", "some-client-secret")
|
||||
secondRecorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(secondRecorder, secondReq)
|
||||
|
||||
assert.Equal(t, 400, secondRecorder.Code)
|
||||
|
||||
var secondRes map[string]any
|
||||
err = json.Unmarshal(secondRecorder.Body.Bytes(), &secondRes)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, secondRes["error"], "invalid_grant")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo forbids access with invalid access token",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||
req.Header.Set("Authorization", "Bearer invalid-access-token")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure access token can be used to access protected resources",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
tokenTest, found := getTestByDescription("Ensure we can get a token with a valid request")
|
||||
assert.True(t, found, "Token test not found")
|
||||
tokenRecorder := httptest.NewRecorder()
|
||||
tokenTest(t, router, tokenRecorder)
|
||||
|
||||
var tokenRes map[string]any
|
||||
err := json.Unmarshal(tokenRecorder.Body.Bytes(), &tokenRes)
|
||||
assert.NoError(t, err)
|
||||
|
||||
accessToken := tokenRes["access_token"].(string)
|
||||
assert.NotEmpty(t, accessToken)
|
||||
|
||||
protectedReq := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||
protectedReq.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
router.ServeHTTP(recorder, protectedReq)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var userInfoRes map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &userInfoRes)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, ok := userInfoRes["sub"]
|
||||
assert.True(t, ok, "Expected sub claim in userinfo response")
|
||||
|
||||
// We should not have an email claim since we didn't request it in the scope
|
||||
_, ok = userInfoRes["email"]
|
||||
assert.False(t, ok, "Did not expect email claim in userinfo response")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||
|
||||
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
|
||||
require.NoError(t, err)
|
||||
|
||||
queries := repository.New(db)
|
||||
oidcService := service.NewOIDCService(oidcServiceCfg, queries)
|
||||
err = oidcService.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
router := gin.Default()
|
||||
|
||||
for _, middleware := range test.middlewares {
|
||||
router.Use(middleware)
|
||||
}
|
||||
|
||||
group := router.Group("/api")
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
oidcController := controller.NewOIDCController(controllerCfg, oidcService, group)
|
||||
oidcController.SetupRoutes()
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
test.run(t, router, recorder)
|
||||
})
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
err = db.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
req, err = http.NewRequest("POST", "/api/oidc/token", strings.NewReader(params.Encode()))
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
resJson = map[string]any{}
|
||||
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &resJson)
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
newToken, ok := resJson["access_token"].(string)
|
||||
assert.Assert(t, ok)
|
||||
assert.Assert(t, newToken != accessToken)
|
||||
|
||||
// Ensure old token is invalid
|
||||
recorder = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("authorization", fmt.Sprintf("Bearer %s", accessToken))
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
|
||||
|
||||
// Test new token
|
||||
recorder = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("authorization", fmt.Sprintf("Bearer %s", newToken))
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
@@ -1,302 +1,373 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gotest.tools/v3/assert"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var loggedInCtx = config.UserContext{
|
||||
Username: "test",
|
||||
Name: "Test",
|
||||
Email: "test@example.com",
|
||||
IsLoggedIn: true,
|
||||
Provider: "local",
|
||||
}
|
||||
func TestProxyController(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
func setupProxyController(t *testing.T, middlewares []gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
||||
// Setup
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.Default()
|
||||
|
||||
if len(middlewares) > 0 {
|
||||
for _, m := range middlewares {
|
||||
router.Use(m)
|
||||
}
|
||||
}
|
||||
|
||||
group := router.Group("/api")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
// Mock app
|
||||
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||
|
||||
// Database
|
||||
db, err := app.SetupDatabase(":memory:")
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Queries
|
||||
queries := repository.New(db)
|
||||
|
||||
// Docker
|
||||
dockerService := service.NewDockerService()
|
||||
|
||||
assert.NilError(t, dockerService.Init())
|
||||
|
||||
// Access controls
|
||||
accessControlsService := service.NewAccessControlsService(dockerService, map[string]config.App{
|
||||
"whoami": {
|
||||
Path: config.AppPath{
|
||||
Allow: "/allow",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NilError(t, accessControlsService.Init())
|
||||
|
||||
// Auth service
|
||||
authService := service.NewAuthService(service.AuthServiceConfig{
|
||||
authServiceCfg := service.AuthServiceConfig{
|
||||
Users: []config.User{
|
||||
{
|
||||
Username: "testuser",
|
||||
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
|
||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||
},
|
||||
{
|
||||
Username: "totpuser",
|
||||
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.",
|
||||
TotpSecret: "foo",
|
||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||
TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
||||
},
|
||||
},
|
||||
OauthWhitelist: []string{},
|
||||
SessionExpiry: 3600,
|
||||
SessionMaxLifetime: 0,
|
||||
SecureCookie: false,
|
||||
CookieDomain: "localhost",
|
||||
LoginTimeout: 300,
|
||||
LoginMaxRetries: 3,
|
||||
SessionCookieName: "tinyauth-session",
|
||||
}, dockerService, nil, queries, &service.OAuthBrokerService{})
|
||||
SessionExpiry: 10, // 10 seconds, useful for testing
|
||||
CookieDomain: "example.com",
|
||||
LoginTimeout: 10, // 10 seconds, useful for testing
|
||||
LoginMaxRetries: 3,
|
||||
SessionCookieName: "tinyauth-session",
|
||||
}
|
||||
|
||||
// Controller
|
||||
ctrl := controller.NewProxyController(controller.ProxyControllerConfig{
|
||||
AppURL: "http://tinyauth.example.com",
|
||||
}, group, accessControlsService, authService)
|
||||
ctrl.SetupRoutes()
|
||||
controllerCfg := controller.ProxyControllerConfig{
|
||||
AppURL: "https://tinyauth.example.com",
|
||||
}
|
||||
|
||||
return router, recorder
|
||||
}
|
||||
|
||||
// TODO: Needs tests for context middleware
|
||||
|
||||
func TestProxyHandler(t *testing.T) {
|
||||
// Test logged out user traefik/caddy (forward_auth)
|
||||
router, recorder := setupProxyController(t, nil)
|
||||
|
||||
req, err := http.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("x-forwarded-host", "whoami.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "http")
|
||||
req.Header.Set("x-forwarded-uri", "/")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusUnauthorized)
|
||||
|
||||
// Test logged out user nginx (auth_request)
|
||||
router, recorder = setupProxyController(t, nil)
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("x-original-url", "http://whoami.example.com/")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusUnauthorized)
|
||||
|
||||
// Test logged out user envoy (ext_authz)
|
||||
router, recorder = setupProxyController(t, nil)
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/envoy?path=/", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Host = "whoami.example.com"
|
||||
req.Header.Set("x-forwarded-proto", "http")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusUnauthorized)
|
||||
|
||||
// Test logged in user traefik/caddy (forward_auth)
|
||||
router, recorder = setupProxyController(t, []gin.HandlerFunc{
|
||||
func(c *gin.Context) {
|
||||
c.Set("context", &loggedInCtx)
|
||||
c.Next()
|
||||
},
|
||||
})
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("x-forwarded-host", "whoami.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "http")
|
||||
req.Header.Set("x-forwarded-uri", "/")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||
|
||||
// Test logged in user nginx (auth_request)
|
||||
router, recorder = setupProxyController(t, []gin.HandlerFunc{
|
||||
func(c *gin.Context) {
|
||||
c.Set("context", &loggedInCtx)
|
||||
c.Next()
|
||||
},
|
||||
})
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("x-original-url", "http://whoami.example.com/")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||
|
||||
// Test logged in user envoy (ext_authz)
|
||||
router, recorder = setupProxyController(t, []gin.HandlerFunc{
|
||||
func(c *gin.Context) {
|
||||
c.Set("context", &loggedInCtx)
|
||||
c.Next()
|
||||
},
|
||||
})
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/envoy?path=/", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Host = "whoami.example.com"
|
||||
req.Header.Set("x-forwarded-proto", "http")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||
|
||||
// Test ACL allow caddy/traefik (forward_auth)
|
||||
router, recorder = setupProxyController(t, nil)
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("x-forwarded-host", "whoami.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "http")
|
||||
req.Header.Set("x-forwarded-uri", "/allow")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||
|
||||
// Test ACL allow nginx
|
||||
router, recorder = setupProxyController(t, nil)
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("x-original-url", "http://whoami.example.com/allow")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||
|
||||
// Test ACL allow envoy
|
||||
router, recorder = setupProxyController(t, nil)
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/envoy?path=/allow", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Host = "whoami.example.com"
|
||||
req.Header.Set("x-forwarded-proto", "http")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||
|
||||
// Test traefik/caddy (forward_auth) without required headers
|
||||
router, recorder = setupProxyController(t, nil)
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusBadRequest)
|
||||
|
||||
// Test nginx (forward_auth) without required headers
|
||||
router, recorder = setupProxyController(t, nil)
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusBadRequest)
|
||||
|
||||
// Test envoy (forward_auth) without required headers
|
||||
router, recorder = setupProxyController(t, nil)
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/envoy", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusBadRequest)
|
||||
|
||||
// Test nginx (auth_request) with forward_auth fallback with ACLs
|
||||
router, recorder = setupProxyController(t, nil)
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("x-forwarded-host", "whoami.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "http")
|
||||
req.Header.Set("x-forwarded-uri", "/allow")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||
|
||||
// Test envoy (ext_authz) with forward_auth fallback with ACLs
|
||||
router, recorder = setupProxyController(t, nil)
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/envoy", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("x-forwarded-host", "whoami.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "http")
|
||||
req.Header.Set("x-forwarded-uri", "/allow")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||
|
||||
// Test envoy (ext_authz) with empty path
|
||||
router, recorder = setupProxyController(t, nil)
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/envoy", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Host = "whoami.example.com"
|
||||
req.Header.Set("x-forwarded-proto", "http")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusUnauthorized)
|
||||
|
||||
// Ensure forward_auth fallback works with path (should ignore)
|
||||
router, recorder = setupProxyController(t, nil)
|
||||
|
||||
req, err = http.NewRequest("GET", "/api/auth/traefik?path=/allow", nil)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req.Header.Set("x-forwarded-proto", "http")
|
||||
req.Header.Set("x-forwarded-host", "whoami.example.com")
|
||||
req.Header.Set("x-forwarded-uri", "/allow")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, recorder.Code, http.StatusOK)
|
||||
acls := map[string]config.App{
|
||||
"app_path_allow": {
|
||||
Config: config.AppConfig{
|
||||
Domain: "path-allow.example.com",
|
||||
},
|
||||
Path: config.AppPath{
|
||||
Allow: "/allowed",
|
||||
},
|
||||
},
|
||||
"app_user_allow": {
|
||||
Config: config.AppConfig{
|
||||
Domain: "user-allow.example.com",
|
||||
},
|
||||
Users: config.AppUsers{
|
||||
Allow: "testuser",
|
||||
},
|
||||
},
|
||||
"ip_bypass": {
|
||||
Config: config.AppConfig{
|
||||
Domain: "ip-bypass.example.com",
|
||||
},
|
||||
IP: config.AppIP{
|
||||
Bypass: []string{"10.10.10.10"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const browserUserAgent = `
|
||||
Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36`
|
||||
|
||||
simpleCtx := func(c *gin.Context) {
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: "testuser",
|
||||
Name: "Testuser",
|
||||
Email: "testuser@example.com",
|
||||
IsLoggedIn: true,
|
||||
Provider: "local",
|
||||
})
|
||||
c.Next()
|
||||
}
|
||||
|
||||
simpleCtxTotp := func(c *gin.Context) {
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: "totpuser",
|
||||
Name: "Totpuser",
|
||||
Email: "totpuser@example.com",
|
||||
IsLoggedIn: true,
|
||||
Provider: "local",
|
||||
TotpEnabled: true,
|
||||
})
|
||||
c.Next()
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
description string
|
||||
middlewares []gin.HandlerFunc
|
||||
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
description: "Default forward auth should be detected and used",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
req.Header.Set("x-forwarded-host", "test.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/")
|
||||
req.Header.Set("user-agent", browserUserAgent)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 307, recorder.Code)
|
||||
location := recorder.Header().Get("Location")
|
||||
assert.Contains(t, location, "https://tinyauth.example.com/login?redirect_uri=")
|
||||
assert.Contains(t, location, "https%3A%2F%2Ftest.example.com%2F")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Auth request (nginx) should be detected and used",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
req.Header.Set("x-original-url", "https://test.example.com/")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ext authz (envoy) should be detected and used",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil) // test a different method for envoy
|
||||
req.Host = "test.example.com"
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure forward auth fallback for nginx",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
req.Header.Set("x-forwarded-host", "test.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure forward auth fallback for envoy",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
|
||||
req.Header.Set("x-forwarded-host", "test.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/hello")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure normal authentication flow for forward auth",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
req.Header.Set("x-forwarded-host", "test.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
|
||||
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
|
||||
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure normal authentication flow for nginx auth request",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
req.Header.Set("x-original-url", "https://test.example.com/")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
|
||||
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
|
||||
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure normal authentication flow for envoy ext authz",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
|
||||
req.Host = "test.example.com"
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
|
||||
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
|
||||
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure path allow ACL works on forward auth",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
req.Header.Set("x-forwarded-host", "path-allow.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/allowed")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure path allow ACL works on nginx auth request",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
req.Header.Set("x-original-url", "https://path-allow.example.com/allowed")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure path allow ACL works on envoy ext authz",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/allowed", nil)
|
||||
req.Host = "path-allow.example.com"
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure ip bypass ACL works on forward auth",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
req.Header.Set("x-forwarded-host", "ip-bypass.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/")
|
||||
req.Header.Set("x-forwarded-for", "10.10.10.10")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure ip bypass ACL works on nginx auth request",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
||||
req.Header.Set("x-original-url", "https://ip-bypass.example.com/")
|
||||
req.Header.Set("x-forwarded-for", "10.10.10.10")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure ip bypass ACL works on envoy ext authz",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
|
||||
req.Host = "ip-bypass.example.com"
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-for", "10.10.10.10")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure user allow ACL allows correct user (should allow testuser)",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
req.Header.Set("x-forwarded-host", "user-allow.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure user allow ACL blocks incorrect user (should block totpuser)",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtxTotp,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||
req.Header.Set("x-forwarded-host", "user-allow.example.com")
|
||||
req.Header.Set("x-forwarded-proto", "https")
|
||||
req.Header.Set("x-forwarded-uri", "/")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 403, recorder.Code)
|
||||
assert.Equal(t, "", recorder.Header().Get("remote-user"))
|
||||
assert.Equal(t, "", recorder.Header().Get("remote-name"))
|
||||
assert.Equal(t, "", recorder.Header().Get("remote-email"))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
|
||||
|
||||
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||
|
||||
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
|
||||
require.NoError(t, err)
|
||||
|
||||
queries := repository.New(db)
|
||||
|
||||
docker := service.NewDockerService()
|
||||
err = docker.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
ldap := service.NewLdapService(service.LdapServiceConfig{})
|
||||
err = ldap.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
broker := service.NewOAuthBrokerService(oauthBrokerCfgs)
|
||||
err = broker.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)
|
||||
err = authService.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
aclsService := service.NewAccessControlsService(docker, acls)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
router := gin.Default()
|
||||
|
||||
for _, m := range test.middlewares {
|
||||
router.Use(m)
|
||||
}
|
||||
|
||||
group := router.Group("/api")
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
proxyController := controller.NewProxyController(controllerCfg, group, aclsService, authService)
|
||||
proxyController.SetupRoutes()
|
||||
|
||||
test.run(t, router, recorder)
|
||||
})
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
err = db.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,57 +3,81 @@ package controller_test
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gotest.tools/v3/assert"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResourcesHandler(t *testing.T) {
|
||||
// Setup
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
group := router.Group("/")
|
||||
func TestResourcesController(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
ctrl := controller.NewResourcesController(controller.ResourcesControllerConfig{
|
||||
Path: "/tmp/tinyauth",
|
||||
resourcesControllerCfg := controller.ResourcesControllerConfig{
|
||||
Path: path.Join(tempDir, "resources"),
|
||||
Enabled: true,
|
||||
}, group)
|
||||
ctrl.SetupRoutes()
|
||||
}
|
||||
|
||||
// Create test data
|
||||
err := os.Mkdir("/tmp/tinyauth", 0755)
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll("/tmp/tinyauth")
|
||||
err := os.Mkdir(resourcesControllerCfg.Path, 0777)
|
||||
require.NoError(t, err)
|
||||
|
||||
file, err := os.Create("/tmp/tinyauth/test.txt")
|
||||
assert.NilError(t, err)
|
||||
type testCase struct {
|
||||
description string
|
||||
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
|
||||
}
|
||||
|
||||
_, err = file.WriteString("This is a test file.")
|
||||
assert.NilError(t, err)
|
||||
file.Close()
|
||||
tests := []testCase{
|
||||
{
|
||||
description: "Ensure resources endpoint returns 200 OK for existing file",
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/resources/testfile.txt", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
// Test existing file
|
||||
req := httptest.NewRequest("GET", "/resources/test.txt", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Equal(t, "This is a test file.", recorder.Body.String())
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure resources endpoint returns 404 Not Found for non-existing file",
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/resources/nonexistent.txt", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Equal(t, "This is a test file.", recorder.Body.String())
|
||||
assert.Equal(t, 404, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure resources controller denies path traversal",
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/resources/../somefile.txt", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
// Test non-existing file
|
||||
req = httptest.NewRequest("GET", "/resources/nonexistent.txt", nil)
|
||||
recorder = httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 404, recorder.Code)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, 404, recorder.Code)
|
||||
testFilePath := resourcesControllerCfg.Path + "/testfile.txt"
|
||||
err = os.WriteFile(testFilePath, []byte("This is a test file."), 0777)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test directory traversal attack
|
||||
req = httptest.NewRequest("GET", "/resources/../etc/passwd", nil)
|
||||
recorder = httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, req)
|
||||
testFilePathParent := tempDir + "/somefile.txt"
|
||||
err = os.WriteFile(testFilePathParent, []byte("This file should not be accessible."), 0777)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 404, recorder.Code)
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
router := gin.Default()
|
||||
group := router.Group("/")
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
resourcesController := controller.NewResourcesController(resourcesControllerCfg, group)
|
||||
resourcesController.SetupRoutes()
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
test.run(t, router, recorder)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,305 +2,355 @@ package controller_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"gotest.tools/v3/assert"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var cookieValue string
|
||||
var totpSecret = "6WFZXPEZRK5MZHHYAFW4DAOUYQMCASBJ"
|
||||
func TestUserController(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
// Setup
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.Default()
|
||||
|
||||
if middlewares != nil {
|
||||
for _, m := range *middlewares {
|
||||
router.Use(m)
|
||||
}
|
||||
}
|
||||
|
||||
group := router.Group("/api")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
// Mock app
|
||||
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||
|
||||
// Database
|
||||
db, err := app.SetupDatabase(":memory:")
|
||||
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Queries
|
||||
queries := repository.New(db)
|
||||
|
||||
// Auth service
|
||||
authService := service.NewAuthService(service.AuthServiceConfig{
|
||||
authServiceCfg := service.AuthServiceConfig{
|
||||
Users: []config.User{
|
||||
{
|
||||
Username: "testuser",
|
||||
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
|
||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||
},
|
||||
{
|
||||
Username: "totpuser",
|
||||
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
|
||||
TotpSecret: totpSecret,
|
||||
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
|
||||
TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
|
||||
},
|
||||
},
|
||||
OauthWhitelist: []string{},
|
||||
SessionExpiry: 3600,
|
||||
SessionMaxLifetime: 0,
|
||||
SecureCookie: false,
|
||||
CookieDomain: "localhost",
|
||||
LoginTimeout: 300,
|
||||
LoginMaxRetries: 3,
|
||||
SessionCookieName: "tinyauth-session",
|
||||
}, nil, nil, queries, &service.OAuthBrokerService{})
|
||||
|
||||
// Controller
|
||||
ctrl := controller.NewUserController(controller.UserControllerConfig{
|
||||
CookieDomain: "localhost",
|
||||
}, group, authService)
|
||||
ctrl.SetupRoutes()
|
||||
|
||||
return router, recorder
|
||||
}
|
||||
|
||||
func TestLoginHandler(t *testing.T) {
|
||||
// Setup
|
||||
router, recorder := setupUserController(t, nil)
|
||||
|
||||
loginReq := controller.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "test",
|
||||
SessionExpiry: 10, // 10 seconds, useful for testing
|
||||
CookieDomain: "example.com",
|
||||
LoginTimeout: 10, // 10 seconds, useful for testing
|
||||
LoginMaxRetries: 3,
|
||||
SessionCookieName: "tinyauth-session",
|
||||
}
|
||||
|
||||
loginReqJson, err := json.Marshal(loginReq)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Test
|
||||
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson)))
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
cookie := recorder.Result().Cookies()[0]
|
||||
|
||||
assert.Equal(t, "tinyauth-session", cookie.Name)
|
||||
assert.Assert(t, cookie.Value != "")
|
||||
|
||||
cookieValue = cookie.Value
|
||||
|
||||
// Test invalid credentials
|
||||
loginReq = controller.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "invalid",
|
||||
userControllerCfg := controller.UserControllerConfig{
|
||||
CookieDomain: "example.com",
|
||||
}
|
||||
|
||||
loginReqJson, err = json.Marshal(loginReq)
|
||||
assert.NilError(t, err)
|
||||
|
||||
recorder = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson)))
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
|
||||
// Test totp required
|
||||
loginReq = controller.LoginRequest{
|
||||
Username: "totpuser",
|
||||
Password: "test",
|
||||
type testCase struct {
|
||||
description string
|
||||
middlewares []gin.HandlerFunc
|
||||
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
|
||||
}
|
||||
|
||||
loginReqJson, err = json.Marshal(loginReq)
|
||||
assert.NilError(t, err)
|
||||
tests := []testCase{
|
||||
{
|
||||
description: "Should be able to login with valid credentials",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
loginReq := controller.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "password",
|
||||
}
|
||||
loginReqBody, err := json.Marshal(loginReq)
|
||||
assert.NoError(t, err)
|
||||
|
||||
recorder = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson)))
|
||||
router.ServeHTTP(recorder, req)
|
||||
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
loginResJson, err := json.Marshal(map[string]any{
|
||||
"message": "TOTP required",
|
||||
"status": 200,
|
||||
"totpPending": true,
|
||||
})
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Len(t, recorder.Result().Cookies(), 1)
|
||||
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(loginResJson), recorder.Body.String())
|
||||
|
||||
// Test invalid json
|
||||
recorder = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader("{invalid json}"))
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 400, recorder.Code)
|
||||
|
||||
// Test rate limiting
|
||||
loginReq = controller.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "invalid",
|
||||
}
|
||||
|
||||
loginReqJson, err = json.Marshal(loginReq)
|
||||
assert.NilError(t, err)
|
||||
|
||||
for range 5 {
|
||||
recorder = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqJson)))
|
||||
router.ServeHTTP(recorder, req)
|
||||
}
|
||||
|
||||
assert.Equal(t, 429, recorder.Code)
|
||||
}
|
||||
|
||||
func TestLogoutHandler(t *testing.T) {
|
||||
// Setup
|
||||
router, recorder := setupUserController(t, nil)
|
||||
|
||||
// Test
|
||||
req := httptest.NewRequest("POST", "/api/user/logout", nil)
|
||||
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: "tinyauth-session",
|
||||
Value: cookieValue,
|
||||
})
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
cookie := recorder.Result().Cookies()[0]
|
||||
|
||||
assert.Equal(t, "tinyauth-session", cookie.Name)
|
||||
assert.Equal(t, "", cookie.Value)
|
||||
assert.Equal(t, -1, cookie.MaxAge)
|
||||
}
|
||||
|
||||
func TestTotpHandler(t *testing.T) {
|
||||
// Setup
|
||||
router, recorder := setupUserController(t, &[]gin.HandlerFunc{
|
||||
func(c *gin.Context) {
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: "totpuser",
|
||||
Name: "totpuser",
|
||||
Email: "totpuser@example.com",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "local",
|
||||
TotpPending: true,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: true,
|
||||
})
|
||||
c.Next()
|
||||
cookie := recorder.Result().Cookies()[0]
|
||||
assert.Equal(t, "tinyauth-session", cookie.Name)
|
||||
assert.True(t, cookie.HttpOnly)
|
||||
assert.Equal(t, "example.com", cookie.Domain)
|
||||
assert.Equal(t, 10, cookie.MaxAge)
|
||||
},
|
||||
},
|
||||
})
|
||||
{
|
||||
description: "Should reject login with invalid credentials",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
loginReq := controller.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "wrongpassword",
|
||||
}
|
||||
loginReqBody, err := json.Marshal(loginReq)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test
|
||||
code, err := totp.GenerateCode(totpSecret, time.Now())
|
||||
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
assert.NilError(t, err)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
totpReq := controller.TotpRequest{
|
||||
Code: code,
|
||||
}
|
||||
|
||||
totpReqJson, err := json.Marshal(totpReq)
|
||||
assert.NilError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson)))
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
cookie := recorder.Result().Cookies()[0]
|
||||
|
||||
assert.Equal(t, "tinyauth-session", cookie.Name)
|
||||
assert.Assert(t, cookie.Value != "")
|
||||
|
||||
// Test invalid json
|
||||
recorder = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader("{invalid json}"))
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 400, recorder.Code)
|
||||
|
||||
// Test rate limiting
|
||||
totpReq = controller.TotpRequest{
|
||||
Code: "000000",
|
||||
}
|
||||
|
||||
totpReqJson, err = json.Marshal(totpReq)
|
||||
assert.NilError(t, err)
|
||||
|
||||
for range 5 {
|
||||
recorder = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson)))
|
||||
router.ServeHTTP(recorder, req)
|
||||
}
|
||||
|
||||
assert.Equal(t, 429, recorder.Code)
|
||||
|
||||
// Test invalid code
|
||||
router, recorder = setupUserController(t, &[]gin.HandlerFunc{
|
||||
func(c *gin.Context) {
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: "totpuser",
|
||||
Name: "totpuser",
|
||||
Email: "totpuser@example.com",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "local",
|
||||
TotpPending: true,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: true,
|
||||
})
|
||||
c.Next()
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
assert.Len(t, recorder.Result().Cookies(), 0)
|
||||
assert.Contains(t, recorder.Body.String(), "Unauthorized")
|
||||
},
|
||||
},
|
||||
})
|
||||
{
|
||||
description: "Should rate limit on 3 invalid attempts",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
loginReq := controller.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "wrongpassword",
|
||||
}
|
||||
loginReqBody, err := json.Marshal(loginReq)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson)))
|
||||
router.ServeHTTP(recorder, req)
|
||||
for range 3 {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Test no totp pending
|
||||
router, recorder = setupUserController(t, &[]gin.HandlerFunc{
|
||||
func(c *gin.Context) {
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: "totpuser",
|
||||
Name: "totpuser",
|
||||
Email: "totpuser@example.com",
|
||||
IsLoggedIn: false,
|
||||
OAuth: false,
|
||||
Provider: "local",
|
||||
TotpPending: false,
|
||||
OAuthGroups: "",
|
||||
TotpEnabled: false,
|
||||
})
|
||||
c.Next()
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
assert.Len(t, recorder.Result().Cookies(), 0)
|
||||
assert.Contains(t, recorder.Body.String(), "Unauthorized")
|
||||
}
|
||||
|
||||
// 4th attempt should be rate limited
|
||||
recorder = httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 429, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "Too many failed login attempts.")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Should not allow full login with totp",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
loginReq := controller.LoginRequest{
|
||||
Username: "totpuser",
|
||||
Password: "password",
|
||||
}
|
||||
loginReqBody, err := json.Marshal(loginReq)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
decodedBody := make(map[string]any)
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &decodedBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, decodedBody["totpPending"], true)
|
||||
|
||||
// should set the session cookie
|
||||
assert.Len(t, recorder.Result().Cookies(), 1)
|
||||
cookie := recorder.Result().Cookies()[0]
|
||||
assert.Equal(t, "tinyauth-session", cookie.Name)
|
||||
assert.True(t, cookie.HttpOnly)
|
||||
assert.Equal(t, "example.com", cookie.Domain)
|
||||
assert.Equal(t, 3600, cookie.MaxAge) // 1 hour, default for totp pending sessions
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Should be able to logout",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
// First login to get a session cookie
|
||||
loginReq := controller.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "password",
|
||||
}
|
||||
loginReqBody, err := json.Marshal(loginReq)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Len(t, recorder.Result().Cookies(), 1)
|
||||
|
||||
cookie := recorder.Result().Cookies()[0]
|
||||
assert.Equal(t, "tinyauth-session", cookie.Name)
|
||||
|
||||
// Now logout using the session cookie
|
||||
recorder = httptest.NewRecorder()
|
||||
req = httptest.NewRequest("POST", "/api/user/logout", nil)
|
||||
req.AddCookie(cookie)
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Len(t, recorder.Result().Cookies(), 1)
|
||||
|
||||
logoutCookie := recorder.Result().Cookies()[0]
|
||||
assert.Equal(t, "tinyauth-session", logoutCookie.Name)
|
||||
assert.Equal(t, "", logoutCookie.Value)
|
||||
assert.Equal(t, -1, logoutCookie.MaxAge) // MaxAge -1 means delete cookie
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Should be able to login with totp",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
|
||||
assert.NoError(t, err)
|
||||
|
||||
totpReq := controller.TotpRequest{
|
||||
Code: code,
|
||||
}
|
||||
|
||||
totpReqBody, err := json.Marshal(totpReq)
|
||||
assert.NoError(t, err)
|
||||
|
||||
recorder = httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
assert.Len(t, recorder.Result().Cookies(), 1)
|
||||
|
||||
// should set a new session cookie with totp pending removed
|
||||
totpCookie := recorder.Result().Cookies()[0]
|
||||
assert.Equal(t, "tinyauth-session", totpCookie.Name)
|
||||
assert.True(t, totpCookie.HttpOnly)
|
||||
assert.Equal(t, "example.com", totpCookie.Domain)
|
||||
assert.Equal(t, 10, totpCookie.MaxAge) // should use the regular session expiry time
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Totp should rate limit on multiple invalid attempts",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
for range 3 {
|
||||
totpReq := controller.TotpRequest{
|
||||
Code: "000000", // invalid code
|
||||
}
|
||||
|
||||
totpReqBody, err := json.Marshal(totpReq)
|
||||
assert.NoError(t, err)
|
||||
|
||||
recorder = httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "Unauthorized")
|
||||
}
|
||||
|
||||
// 4th attempt should be rate limited
|
||||
recorder = httptest.NewRecorder()
|
||||
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(`{"code":"000000"}`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 429, recorder.Code)
|
||||
assert.Contains(t, recorder.Body.String(), "Too many failed TOTP attempts.")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
|
||||
|
||||
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||
|
||||
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
|
||||
require.NoError(t, err)
|
||||
|
||||
queries := repository.New(db)
|
||||
|
||||
docker := service.NewDockerService()
|
||||
err = docker.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
ldap := service.NewLdapService(service.LdapServiceConfig{})
|
||||
err = ldap.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
broker := service.NewOAuthBrokerService(oauthBrokerCfgs)
|
||||
err = broker.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)
|
||||
err = authService.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
beforeEach := func() {
|
||||
// Clear failed login attempts before each test
|
||||
authService.ClearRateLimitsTestingOnly()
|
||||
}
|
||||
|
||||
setTotpMiddlewareOverrides := []string{
|
||||
"Should be able to login with totp",
|
||||
"Totp should rate limit on multiple invalid attempts",
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
beforeEach()
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
router := gin.Default()
|
||||
|
||||
for _, middleware := range test.middlewares {
|
||||
router.Use(middleware)
|
||||
}
|
||||
|
||||
// Gin is stupid and doesn't allow setting a middleware after the groups
|
||||
// so we need to do some stupid overrides here
|
||||
if slices.Contains(setTotpMiddlewareOverrides, test.description) {
|
||||
// Assuming the cookie is set, it should be picked up by the
|
||||
// context middleware
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: "totpuser",
|
||||
Name: "Totpuser",
|
||||
Email: "totpuser@example.com",
|
||||
Provider: "local",
|
||||
TotpPending: true,
|
||||
TotpEnabled: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
group := router.Group("/api")
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
userController := controller.NewUserController(userControllerCfg, group, authService)
|
||||
userController.SetupRoutes()
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
test.run(t, router, recorder)
|
||||
})
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
err = db.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson)))
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
}
|
||||
|
||||
129
internal/controller/well_known_controller_test.go
Normal file
129
internal/controller/well_known_controller_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/steveiliop56/tinyauth/internal/bootstrap"
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWellKnownController(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
oidcServiceCfg := service.OIDCServiceConfig{
|
||||
Clients: map[string]config.OIDCClientConfig{
|
||||
"test": {
|
||||
ClientID: "some-client-id",
|
||||
ClientSecret: "some-client-secret",
|
||||
TrustedRedirectURIs: []string{"https://test.example.com/callback"},
|
||||
Name: "Test Client",
|
||||
},
|
||||
},
|
||||
PrivateKeyPath: path.Join(tempDir, "key.pem"),
|
||||
PublicKeyPath: path.Join(tempDir, "key.pub"),
|
||||
Issuer: "https://tinyauth.example.com",
|
||||
SessionExpiry: 500,
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
description string
|
||||
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{
|
||||
description: "Ensure well-known endpoint returns correct OIDC configuration",
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/.well-known/openid-configuration", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
res := controller.OpenIDConnectConfiguration{}
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := controller.OpenIDConnectConfiguration{
|
||||
Issuer: oidcServiceCfg.Issuer,
|
||||
AuthorizationEndpoint: fmt.Sprintf("%s/authorize", oidcServiceCfg.Issuer),
|
||||
TokenEndpoint: fmt.Sprintf("%s/api/oidc/token", oidcServiceCfg.Issuer),
|
||||
UserinfoEndpoint: fmt.Sprintf("%s/api/oidc/userinfo", oidcServiceCfg.Issuer),
|
||||
JwksUri: fmt.Sprintf("%s/.well-known/jwks.json", oidcServiceCfg.Issuer),
|
||||
ScopesSupported: service.SupportedScopes,
|
||||
ResponseTypesSupported: service.SupportedResponseTypes,
|
||||
GrantTypesSupported: service.SupportedGrantTypes,
|
||||
SubjectTypesSupported: []string{"pairwise"},
|
||||
IDTokenSigningAlgValuesSupported: []string{"RS256"},
|
||||
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"},
|
||||
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups"},
|
||||
ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, res)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure well-known endpoint returns correct JWKS",
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/.well-known/jwks.json", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
decodedBody := make(map[string]any)
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &decodedBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
keys, ok := decodedBody["keys"].([]any)
|
||||
assert.True(t, ok)
|
||||
assert.Len(t, keys, 1)
|
||||
|
||||
keyData, ok := keys[0].(map[string]any)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "RSA", keyData["kty"])
|
||||
assert.Equal(t, "sig", keyData["use"])
|
||||
assert.Equal(t, "RS256", keyData["alg"])
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||
|
||||
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
|
||||
require.NoError(t, err)
|
||||
|
||||
queries := repository.New(db)
|
||||
|
||||
oidcService := service.NewOIDCService(oidcServiceCfg, queries)
|
||||
err = oidcService.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
router := gin.Default()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
wellKnownController := controller.NewWellKnownController(controller.WellKnownControllerConfig{}, oidcService, router)
|
||||
wellKnownController.SetupRoutes()
|
||||
|
||||
test.run(t, router, recorder)
|
||||
})
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
err = db.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -13,7 +12,24 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var OIDCIgnorePaths = []string{"/api/oidc/token", "/api/oidc/userinfo"}
|
||||
// Gin won't let us set a middleware on a specific route (at least it doesn't work,
|
||||
// see https://github.com/gin-gonic/gin/issues/531) so we have to do some hackery
|
||||
var (
|
||||
contextSkipPathsPrefix = []string{
|
||||
"GET /api/context/app",
|
||||
"GET /api/healthz",
|
||||
"HEAD /api/healthz",
|
||||
"GET /api/oauth/url",
|
||||
"GET /api/oauth/callback",
|
||||
"GET /api/oidc/clients",
|
||||
"POST /api/oidc/token",
|
||||
"GET /api/oidc/userinfo",
|
||||
"GET /resources",
|
||||
"POST /api/user/login",
|
||||
"GET /.well-known/openid-configuration",
|
||||
"GET /.well-known/jwks.json",
|
||||
}
|
||||
)
|
||||
|
||||
type ContextMiddlewareConfig struct {
|
||||
CookieDomain string
|
||||
@@ -39,9 +55,7 @@ func (m *ContextMiddleware) Init() error {
|
||||
|
||||
func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// There is no point in trying to get credentials if it's an OIDC endpoint
|
||||
path := c.Request.URL.Path
|
||||
if slices.Contains(OIDCIgnorePaths, strings.TrimSuffix(path, "/")) {
|
||||
if m.isIgnorePath(c.Request.Method + " " + c.Request.URL.Path) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
@@ -224,3 +238,12 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ContextMiddleware) isIgnorePath(path string) bool {
|
||||
for _, prefix := range contextSkipPathsPrefix {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
)
|
||||
|
||||
// See context middleware for explanation of why we have to do this
|
||||
var (
|
||||
loggerSkipPathsPrefix = []string{
|
||||
"GET /api/health",
|
||||
"HEAD /api/health",
|
||||
"GET /api/healthz",
|
||||
"HEAD /api/healthz",
|
||||
"GET /favicon.ico",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -78,6 +79,8 @@ type AuthService struct {
|
||||
queries *repository.Queries
|
||||
oauthBroker *OAuthBrokerService
|
||||
lockdown *Lockdown
|
||||
lockdownCtx context.Context
|
||||
lockdownCancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService {
|
||||
@@ -770,6 +773,11 @@ func (auth *AuthService) ensureOAuthSessionLimit() {
|
||||
}
|
||||
|
||||
func (auth *AuthService) lockdownMode() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
auth.lockdownCtx = ctx
|
||||
auth.lockdownCancelFunc = cancel
|
||||
|
||||
auth.loginMutex.Lock()
|
||||
|
||||
tlog.App.Warn().Msg("Multiple login attempts detected, possibly DDOS attack. Activating temporary lockdown.")
|
||||
@@ -788,7 +796,12 @@ func (auth *AuthService) lockdownMode() {
|
||||
|
||||
auth.loginMutex.Unlock()
|
||||
|
||||
<-timer.C
|
||||
select {
|
||||
case <-timer.C:
|
||||
// Timer expired, end lockdown
|
||||
case <-ctx.Done():
|
||||
// Context cancelled, end lockdown
|
||||
}
|
||||
|
||||
auth.loginMutex.Lock()
|
||||
|
||||
@@ -796,3 +809,13 @@ func (auth *AuthService) lockdownMode() {
|
||||
auth.lockdown = nil
|
||||
auth.loginMutex.Unlock()
|
||||
}
|
||||
|
||||
// Function only used for testing - do not use in prod!
|
||||
func (auth *AuthService) ClearRateLimitsTestingOnly() {
|
||||
auth.loginMutex.Lock()
|
||||
auth.loginAttempts = make(map[string]*LoginAttempt)
|
||||
if auth.lockdown != nil {
|
||||
auth.lockdownCancelFunc()
|
||||
}
|
||||
auth.loginMutex.Unlock()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
type OAuthServiceImpl interface {
|
||||
Name() string
|
||||
ID() string
|
||||
NewRandom() string
|
||||
GetAuthURL(state string, verifier string) string
|
||||
GetToken(code string, verifier string) (*oauth2.Token, error)
|
||||
@@ -39,7 +40,7 @@ func (broker *OAuthBrokerService) Init() error {
|
||||
broker.services[name] = presetFunc(cfg)
|
||||
tlog.App.Debug().Str("service", name).Msg("Loaded OAuth service from preset")
|
||||
} else {
|
||||
broker.services[name] = NewOAuthService(cfg)
|
||||
broker.services[name] = NewOAuthService(cfg, name)
|
||||
tlog.App.Debug().Str("service", name).Msg("Loaded OAuth service from config")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ func newGoogleOAuthService(config config.OAuthServiceConfig) *OAuthService {
|
||||
config.AuthURL = endpoints.Google.AuthURL
|
||||
config.TokenURL = endpoints.Google.TokenURL
|
||||
config.UserinfoURL = "https://openidconnect.googleapis.com/v1/userinfo"
|
||||
return NewOAuthService(config)
|
||||
return NewOAuthService(config, "google")
|
||||
}
|
||||
|
||||
func newGitHubOAuthService(config config.OAuthServiceConfig) *OAuthService {
|
||||
@@ -19,5 +19,5 @@ func newGitHubOAuthService(config config.OAuthServiceConfig) *OAuthService {
|
||||
config.Scopes = scopes
|
||||
config.AuthURL = endpoints.GitHub.AuthURL
|
||||
config.TokenURL = endpoints.GitHub.TokenURL
|
||||
return NewOAuthService(config).WithUserinfoExtractor(githubExtractor)
|
||||
return NewOAuthService(config, "github").WithUserinfoExtractor(githubExtractor)
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@ type OAuthService struct {
|
||||
config *oauth2.Config
|
||||
ctx context.Context
|
||||
userinfoExtractor UserinfoExtractor
|
||||
id string
|
||||
}
|
||||
|
||||
func NewOAuthService(config config.OAuthServiceConfig) *OAuthService {
|
||||
func NewOAuthService(config config.OAuthServiceConfig, id string) *OAuthService {
|
||||
httpClient := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
@@ -45,6 +46,7 @@ func NewOAuthService(config config.OAuthServiceConfig) *OAuthService {
|
||||
},
|
||||
ctx: ctx,
|
||||
userinfoExtractor: defaultExtractor,
|
||||
id: id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +59,10 @@ func (s *OAuthService) Name() string {
|
||||
return s.serviceCfg.Name
|
||||
}
|
||||
|
||||
func (s *OAuthService) ID() string {
|
||||
return s.id
|
||||
}
|
||||
|
||||
func (s *OAuthService) NewRandom() string {
|
||||
// The generate verifier function just creates a random string,
|
||||
// so we can use it to generate a random state as well
|
||||
|
||||
@@ -25,12 +25,6 @@ func TestGetRootDomain(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
// Domain with no subdomain
|
||||
domain = "http://tinyauth.app"
|
||||
expected = "tinyauth.app"
|
||||
_, err = utils.GetCookieDomain(domain)
|
||||
assert.Error(t, err, "invalid app url, must be at least second level domain")
|
||||
|
||||
// Invalid domain (only TLD)
|
||||
domain = "com"
|
||||
_, err = utils.GetCookieDomain(domain)
|
||||
|
||||
Reference in New Issue
Block a user