mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 06:05:43 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			2d78e6b598
			...
			revert-337
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | c9817e7feb | 
| @@ -112,7 +112,6 @@ func init() { | |||||||
| 		{"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."}, | 		{"ldap-search-filter", "(uid=%s)", "LDAP search filter for user lookup."}, | ||||||
| 		{"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."}, | 		{"resources-dir", "/data/resources", "Path to a directory containing custom resources (e.g. background image)."}, | ||||||
| 		{"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."}, | 		{"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."}, | ||||||
| 		{"trusted-proxies", "", "Comma separated list of trusted proxies (IP addresses) for correct client IP detection and for header ACLs."}, |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, opt := range configOptions { | 	for _, opt := range configOptions { | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ | |||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueUntrustedRedirectTitle": "Untrusted redirect", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{rootDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ | |||||||
|     "continueInsecureRedirectTitle": "Insecure redirect", |     "continueInsecureRedirectTitle": "Insecure redirect", | ||||||
|     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", |     "continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?", | ||||||
|     "continueUntrustedRedirectTitle": "Untrusted redirect", |     "continueUntrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?", |     "continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{rootDomain}}</code>). Are you sure you want to continue?", | ||||||
|     "logoutFailTitle": "Failed to log out", |     "logoutFailTitle": "Failed to log out", | ||||||
|     "logoutFailSubtitle": "Please try again", |     "logoutFailSubtitle": "Please try again", | ||||||
|     "logoutSuccessTitle": "Logged out", |     "logoutSuccessTitle": "Logged out", | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ import { Navigate, useLocation, useNavigate } from "react-router"; | |||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
|  |  | ||||||
| export const ContinuePage = () => { | export const ContinuePage = () => { | ||||||
|   const { cookieDomain } = useAppContext(); |   const { rootDomain } = useAppContext(); | ||||||
|   const { isLoggedIn } = useUserContext(); |   const { isLoggedIn } = useUserContext(); | ||||||
|   const { search } = useLocation(); |   const { search } = useLocation(); | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
| @@ -33,8 +33,8 @@ export const ContinuePage = () => { | |||||||
|     : null; |     : null; | ||||||
|   const isTrustedRedirectUri = |   const isTrustedRedirectUri = | ||||||
|     redirectUriObj !== null |     redirectUriObj !== null | ||||||
|       ? redirectUriObj.hostname === cookieDomain || |       ? redirectUriObj.hostname === rootDomain || | ||||||
|         redirectUriObj.hostname.endsWith(`.${cookieDomain}`) |         redirectUriObj.hostname.endsWith(`.${rootDomain}`) | ||||||
|       : false; |       : false; | ||||||
|   const isAllowedRedirectProto = |   const isAllowedRedirectProto = | ||||||
|     redirectUriObj !== null |     redirectUriObj !== null | ||||||
| @@ -105,7 +105,7 @@ export const ContinuePage = () => { | |||||||
|               components={{ |               components={{ | ||||||
|                 code: <code />, |                 code: <code />, | ||||||
|               }} |               }} | ||||||
|               values={{ cookieDomain }} |               values={{ rootDomain }} | ||||||
|             /> |             /> | ||||||
|           </CardDescription> |           </CardDescription> | ||||||
|         </CardHeader> |         </CardHeader> | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ export const appContextSchema = z.object({ | |||||||
|   title: z.string(), |   title: z.string(), | ||||||
|   genericName: z.string(), |   genericName: z.string(), | ||||||
|   appUrl: z.string(), |   appUrl: z.string(), | ||||||
|   cookieDomain: z.string(), |   rootDomain: z.string(), | ||||||
|   forgotPasswordMessage: z.string(), |   forgotPasswordMessage: z.string(), | ||||||
|   oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]), |   oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]), | ||||||
|   backgroundImage: z.string(), |   backgroundImage: z.string(), | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,13 +1,10 @@ | |||||||
| module tinyauth | module tinyauth | ||||||
|  |  | ||||||
| go 1.24.0 | go 1.23.2 | ||||||
|  |  | ||||||
| toolchain go1.24.3 |  | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/cenkalti/backoff/v5 v5.0.3 | 	github.com/cenkalti/backoff/v5 v5.0.3 | ||||||
| 	github.com/gin-gonic/gin v1.10.1 | 	github.com/gin-gonic/gin v1.10.1 | ||||||
| 	github.com/glebarez/sqlite v1.11.0 |  | ||||||
| 	github.com/go-playground/validator/v10 v10.27.0 | 	github.com/go-playground/validator/v10 v10.27.0 | ||||||
| 	github.com/golang-migrate/migrate/v4 v4.18.3 | 	github.com/golang-migrate/migrate/v4 v4.18.3 | ||||||
| 	github.com/google/go-querystring v1.1.0 | 	github.com/google/go-querystring v1.1.0 | ||||||
| @@ -17,11 +14,10 @@ require ( | |||||||
| 	github.com/spf13/cobra v1.9.1 | 	github.com/spf13/cobra v1.9.1 | ||||||
| 	github.com/spf13/viper v1.20.1 | 	github.com/spf13/viper v1.20.1 | ||||||
| 	github.com/traefik/paerser v0.2.2 | 	github.com/traefik/paerser v0.2.2 | ||||||
| 	github.com/weppos/publicsuffix-go v0.50.0 | 	golang.org/x/crypto v0.41.0 | ||||||
| 	golang.org/x/crypto v0.42.0 | 	gorm.io/driver/sqlite v1.6.0 | ||||||
| 	golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b |  | ||||||
| 	gorm.io/gorm v1.30.1 | 	gorm.io/gorm v1.30.1 | ||||||
| 	gotest.tools/v3 v3.5.2 | 	modernc.org/sqlite v1.38.2 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| @@ -32,9 +28,9 @@ require ( | |||||||
| 	github.com/containerd/errdefs/pkg v0.3.0 // indirect | 	github.com/containerd/errdefs/pkg v0.3.0 // indirect | ||||||
| 	github.com/containerd/log v0.1.0 // indirect | 	github.com/containerd/log v0.1.0 // indirect | ||||||
| 	github.com/glebarez/go-sqlite v1.21.2 // indirect | 	github.com/glebarez/go-sqlite v1.21.2 // indirect | ||||||
|  | 	github.com/glebarez/sqlite v1.11.0 // indirect | ||||||
| 	github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect | 	github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect | ||||||
| 	github.com/go-viper/mapstructure/v2 v2.4.0 // indirect | 	github.com/go-viper/mapstructure/v2 v2.4.0 // indirect | ||||||
| 	github.com/google/go-cmp v0.6.0 // indirect |  | ||||||
| 	github.com/hashicorp/errwrap v1.1.0 // indirect | 	github.com/hashicorp/errwrap v1.1.0 // indirect | ||||||
| 	github.com/hashicorp/go-multierror v1.1.1 // indirect | 	github.com/hashicorp/go-multierror v1.1.1 // indirect | ||||||
| 	github.com/jinzhu/inflection v1.0.0 // indirect | 	github.com/jinzhu/inflection v1.0.0 // indirect | ||||||
| @@ -48,11 +44,12 @@ require ( | |||||||
| 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect | 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect | ||||||
| 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect | 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect | ||||||
| 	go.opentelemetry.io/otel/sdk v1.34.0 // indirect | 	go.opentelemetry.io/otel/sdk v1.34.0 // indirect | ||||||
| 	golang.org/x/term v0.35.0 // indirect | 	golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect | ||||||
|  | 	golang.org/x/term v0.34.0 // indirect | ||||||
|  | 	gotest.tools/v3 v3.5.2 // indirect | ||||||
| 	modernc.org/libc v1.66.3 // indirect | 	modernc.org/libc v1.66.3 // indirect | ||||||
| 	modernc.org/mathutil v1.7.1 // indirect | 	modernc.org/mathutil v1.7.1 // indirect | ||||||
| 	modernc.org/memory v1.11.0 // indirect | 	modernc.org/memory v1.11.0 // indirect | ||||||
| 	modernc.org/sqlite v1.38.2 // indirect |  | ||||||
| 	rsc.io/qr v0.2.0 // indirect | 	rsc.io/qr v0.2.0 // indirect | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -89,6 +86,8 @@ require ( | |||||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||||
| 	github.com/goccy/go-json v0.10.4 // indirect | 	github.com/goccy/go-json v0.10.4 // indirect | ||||||
| 	github.com/gogo/protobuf v1.3.2 // indirect | 	github.com/gogo/protobuf v1.3.2 // indirect | ||||||
|  | 	github.com/gorilla/securecookie v1.1.2 // indirect | ||||||
|  | 	github.com/gorilla/sessions v1.4.0 | ||||||
| 	github.com/inconshreveable/mousetrap v1.1.0 // indirect | 	github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||||||
| 	github.com/json-iterator/go v1.1.12 // indirect | 	github.com/json-iterator/go v1.1.12 // indirect | ||||||
| 	github.com/klauspost/cpuid/v2 v2.2.9 // indirect | 	github.com/klauspost/cpuid/v2 v2.2.9 // indirect | ||||||
| @@ -126,11 +125,11 @@ require ( | |||||||
| 	go.uber.org/atomic v1.9.0 // indirect | 	go.uber.org/atomic v1.9.0 // indirect | ||||||
| 	go.uber.org/multierr v1.9.0 // indirect | 	go.uber.org/multierr v1.9.0 // indirect | ||||||
| 	golang.org/x/arch v0.13.0 // indirect | 	golang.org/x/arch v0.13.0 // indirect | ||||||
| 	golang.org/x/net v0.44.0 // indirect | 	golang.org/x/net v0.42.0 // indirect | ||||||
| 	golang.org/x/oauth2 v0.30.0 | 	golang.org/x/oauth2 v0.30.0 | ||||||
| 	golang.org/x/sync v0.17.0 // indirect | 	golang.org/x/sync v0.16.0 // indirect | ||||||
| 	golang.org/x/sys v0.36.0 // indirect | 	golang.org/x/sys v0.35.0 // indirect | ||||||
| 	golang.org/x/text v0.29.0 // indirect | 	golang.org/x/text v0.28.0 // indirect | ||||||
| 	google.golang.org/protobuf v1.36.3 // indirect | 	google.golang.org/protobuf v1.36.3 // indirect | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										42
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								go.sum
									
									
									
									
									
								
							| @@ -132,10 +132,16 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN | |||||||
| github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= | ||||||
| github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= | ||||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||||
|  | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= | ||||||
|  | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||||
| github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= | github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= | ||||||
| github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= | github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= | ||||||
| github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||||||
| github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
|  | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= | ||||||
|  | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= | ||||||
|  | github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= | ||||||
|  | github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= | ||||||
| github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= | github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= | ||||||
| github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= | github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= | ||||||
| github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||||
| @@ -280,8 +286,6 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS | |||||||
| github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= | ||||||
| github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= | ||||||
| github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= | ||||||
| github.com/weppos/publicsuffix-go v0.50.0 h1:M178k6l8cnh9T1c1cStkhytVxdk5zPd6gGZf8ySIuVo= |  | ||||||
| github.com/weppos/publicsuffix-go v0.50.0/go.mod h1:VXhClBYMlDrUsome4pOTpe68Ui0p6iQRAbyHQD1yKoU= |  | ||||||
| github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= | ||||||
| github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= | ||||||
| github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||||
| @@ -313,27 +317,27 @@ golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= | |||||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
| golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= | golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= | ||||||
| golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= | golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= | ||||||
| golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= | golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= | ||||||
| golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= | golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= | ||||||
| golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||||
| golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||||
| golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= | golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= | ||||||
| golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= | golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= | ||||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
| golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
| golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= | ||||||
| golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= | golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= | ||||||
| golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= | golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= | ||||||
| golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= | ||||||
| golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= | ||||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= | golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= | ||||||
| golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= | golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= | ||||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| @@ -341,22 +345,22 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc | |||||||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.6.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= | golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= | ||||||
| golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||||||
| golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= | golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= | ||||||
| golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= | golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= | ||||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
| golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= | golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= | ||||||
| golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= | golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= | ||||||
| golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= | golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= | ||||||
| golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= | golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= | ||||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||||
| golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= | ||||||
| golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= | ||||||
| golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= | golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= | ||||||
| golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= | golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= | ||||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| @@ -376,6 +380,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV | |||||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
|  | gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= | ||||||
|  | gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= | ||||||
| gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= | gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= | ||||||
| gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= | gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= | ||||||
| gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= | gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= | ||||||
|   | |||||||
| @@ -45,8 +45,8 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Get cookie domain | 	// Get root domain | ||||||
| 	cookieDomain, err := utils.GetCookieDomain(app.Config.AppURL) | 	rootDomain, err := utils.GetRootDomain(app.Config.AppURL) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @@ -65,7 +65,7 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 		OauthWhitelist:    app.Config.OAuthWhitelist, | 		OauthWhitelist:    app.Config.OAuthWhitelist, | ||||||
| 		SessionExpiry:     app.Config.SessionExpiry, | 		SessionExpiry:     app.Config.SessionExpiry, | ||||||
| 		SecureCookie:      app.Config.SecureCookie, | 		SecureCookie:      app.Config.SecureCookie, | ||||||
| 		CookieDomain:      cookieDomain, | 		RootDomain:        rootDomain, | ||||||
| 		LoginTimeout:      app.Config.LoginTimeout, | 		LoginTimeout:      app.Config.LoginTimeout, | ||||||
| 		LoginMaxRetries:   app.Config.LoginMaxRetries, | 		LoginMaxRetries:   app.Config.LoginMaxRetries, | ||||||
| 		SessionCookieName: sessionCookieName, | 		SessionCookieName: sessionCookieName, | ||||||
| @@ -146,7 +146,6 @@ func (app *BootstrapApp) Setup() error { | |||||||
|  |  | ||||||
| 	// Create engine | 	// Create engine | ||||||
| 	engine := gin.New() | 	engine := gin.New() | ||||||
| 	engine.SetTrustedProxies(strings.Split(app.Config.TrustedProxies, ",")) |  | ||||||
|  |  | ||||||
| 	if config.Version != "development" { | 	if config.Version != "development" { | ||||||
| 		gin.SetMode(gin.ReleaseMode) | 		gin.SetMode(gin.ReleaseMode) | ||||||
| @@ -156,7 +155,7 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 	var middlewares []Middleware | 	var middlewares []Middleware | ||||||
|  |  | ||||||
| 	contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{ | 	contextMiddleware := middleware.NewContextMiddleware(middleware.ContextMiddlewareConfig{ | ||||||
| 		CookieDomain: cookieDomain, | 		RootDomain: rootDomain, | ||||||
| 	}, authService, oauthBrokerService) | 	}, authService, oauthBrokerService) | ||||||
|  |  | ||||||
| 	uiMiddleware := middleware.NewUIMiddleware() | 	uiMiddleware := middleware.NewUIMiddleware() | ||||||
| @@ -183,7 +182,7 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 		Title:                 app.Config.Title, | 		Title:                 app.Config.Title, | ||||||
| 		GenericName:           app.Config.GenericName, | 		GenericName:           app.Config.GenericName, | ||||||
| 		AppURL:                app.Config.AppURL, | 		AppURL:                app.Config.AppURL, | ||||||
| 		CookieDomain:          cookieDomain, | 		RootDomain:            rootDomain, | ||||||
| 		ForgotPasswordMessage: app.Config.ForgotPasswordMessage, | 		ForgotPasswordMessage: app.Config.ForgotPasswordMessage, | ||||||
| 		BackgroundImage:       app.Config.BackgroundImage, | 		BackgroundImage:       app.Config.BackgroundImage, | ||||||
| 		OAuthAutoRedirect:     app.Config.OAuthAutoRedirect, | 		OAuthAutoRedirect:     app.Config.OAuthAutoRedirect, | ||||||
| @@ -194,7 +193,7 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 		SecureCookie:       app.Config.SecureCookie, | 		SecureCookie:       app.Config.SecureCookie, | ||||||
| 		CSRFCookieName:     csrfCookieName, | 		CSRFCookieName:     csrfCookieName, | ||||||
| 		RedirectCookieName: redirectCookieName, | 		RedirectCookieName: redirectCookieName, | ||||||
| 		CookieDomain:       cookieDomain, | 		RootDomain:         rootDomain, | ||||||
| 	}, apiRouter, authService, oauthBrokerService) | 	}, apiRouter, authService, oauthBrokerService) | ||||||
|  |  | ||||||
| 	proxyController := controller.NewProxyController(controller.ProxyControllerConfig{ | 	proxyController := controller.NewProxyController(controller.ProxyControllerConfig{ | ||||||
| @@ -202,7 +201,7 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 	}, apiRouter, dockerService, authService) | 	}, apiRouter, dockerService, authService) | ||||||
|  |  | ||||||
| 	userController := controller.NewUserController(controller.UserControllerConfig{ | 	userController := controller.NewUserController(controller.UserControllerConfig{ | ||||||
| 		CookieDomain: cookieDomain, | 		RootDomain: rootDomain, | ||||||
| 	}, apiRouter, authService) | 	}, apiRouter, authService) | ||||||
|  |  | ||||||
| 	resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{ | 	resourcesController := controller.NewResourcesController(controller.ResourcesControllerConfig{ | ||||||
|   | |||||||
| @@ -53,7 +53,6 @@ type Config struct { | |||||||
| 	LdapSearchFilter        string `mapstructure:"ldap-search-filter"` | 	LdapSearchFilter        string `mapstructure:"ldap-search-filter"` | ||||||
| 	ResourcesDir            string `mapstructure:"resources-dir"` | 	ResourcesDir            string `mapstructure:"resources-dir"` | ||||||
| 	DatabasePath            string `mapstructure:"database-path" validate:"required"` | 	DatabasePath            string `mapstructure:"database-path" validate:"required"` | ||||||
| 	TrustedProxies          string `mapstructure:"trusted-proxies"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // OAuth/OIDC config | // OAuth/OIDC config | ||||||
| @@ -126,51 +125,51 @@ type RedirectQuery struct { | |||||||
|  |  | ||||||
| // Labels | // Labels | ||||||
|  |  | ||||||
| type Apps struct { | type Labels struct { | ||||||
| 	Apps map[string]App | 	Apps map[string]AppLabels | ||||||
| } | } | ||||||
|  |  | ||||||
| type App struct { | type AppLabels struct { | ||||||
| 	Config   AppConfig | 	Config   ConfigLabels | ||||||
| 	Users    AppUsers | 	Users    UsersLabels | ||||||
| 	OAuth    AppOAuth | 	OAuth    OAuthLabels | ||||||
| 	IP       AppIP | 	IP       IPLabels | ||||||
| 	Response AppResponse | 	Response ResponseLabels | ||||||
| 	Path     AppPath | 	Path     PathLabels | ||||||
| } | } | ||||||
|  |  | ||||||
| type AppConfig struct { | type ConfigLabels struct { | ||||||
| 	Domain string | 	Domain string | ||||||
| } | } | ||||||
|  |  | ||||||
| type AppUsers struct { | type UsersLabels struct { | ||||||
| 	Allow string | 	Allow string | ||||||
| 	Block string | 	Block string | ||||||
| } | } | ||||||
|  |  | ||||||
| type AppOAuth struct { | type OAuthLabels struct { | ||||||
| 	Whitelist string | 	Whitelist string | ||||||
| 	Groups    string | 	Groups    string | ||||||
| } | } | ||||||
|  |  | ||||||
| type AppIP struct { | type IPLabels struct { | ||||||
| 	Allow  []string | 	Allow  []string | ||||||
| 	Block  []string | 	Block  []string | ||||||
| 	Bypass []string | 	Bypass []string | ||||||
| } | } | ||||||
|  |  | ||||||
| type AppResponse struct { | type ResponseLabels struct { | ||||||
| 	Headers   []string | 	Headers   []string | ||||||
| 	BasicAuth AppBasicAuth | 	BasicAuth BasicAuthLabels | ||||||
| } | } | ||||||
|  |  | ||||||
| type AppBasicAuth struct { | type BasicAuthLabels struct { | ||||||
| 	Username     string | 	Username     string | ||||||
| 	Password     string | 	Password     string | ||||||
| 	PasswordFile string | 	PasswordFile string | ||||||
| } | } | ||||||
|  |  | ||||||
| type AppPath struct { | type PathLabels struct { | ||||||
| 	Allow string | 	Allow string | ||||||
| 	Block string | 	Block string | ||||||
| } | } | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ type AppContextResponse struct { | |||||||
| 	Title                 string   `json:"title"` | 	Title                 string   `json:"title"` | ||||||
| 	GenericName           string   `json:"genericName"` | 	GenericName           string   `json:"genericName"` | ||||||
| 	AppURL                string   `json:"appUrl"` | 	AppURL                string   `json:"appUrl"` | ||||||
| 	CookieDomain          string   `json:"cookieDomain"` | 	RootDomain            string   `json:"rootDomain"` | ||||||
| 	ForgotPasswordMessage string   `json:"forgotPasswordMessage"` | 	ForgotPasswordMessage string   `json:"forgotPasswordMessage"` | ||||||
| 	BackgroundImage       string   `json:"backgroundImage"` | 	BackgroundImage       string   `json:"backgroundImage"` | ||||||
| 	OAuthAutoRedirect     string   `json:"oauthAutoRedirect"` | 	OAuthAutoRedirect     string   `json:"oauthAutoRedirect"` | ||||||
| @@ -39,7 +39,7 @@ type ContextControllerConfig struct { | |||||||
| 	Title                 string | 	Title                 string | ||||||
| 	GenericName           string | 	GenericName           string | ||||||
| 	AppURL                string | 	AppURL                string | ||||||
| 	CookieDomain          string | 	RootDomain            string | ||||||
| 	ForgotPasswordMessage string | 	ForgotPasswordMessage string | ||||||
| 	BackgroundImage       string | 	BackgroundImage       string | ||||||
| 	OAuthAutoRedirect     string | 	OAuthAutoRedirect     string | ||||||
| @@ -100,7 +100,7 @@ func (controller *ContextController) appContextHandler(c *gin.Context) { | |||||||
| 		Title:                 controller.config.Title, | 		Title:                 controller.config.Title, | ||||||
| 		GenericName:           controller.config.GenericName, | 		GenericName:           controller.config.GenericName, | ||||||
| 		AppURL:                fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host), | 		AppURL:                fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host), | ||||||
| 		CookieDomain:          controller.config.CookieDomain, | 		RootDomain:            controller.config.RootDomain, | ||||||
| 		ForgotPasswordMessage: controller.config.ForgotPasswordMessage, | 		ForgotPasswordMessage: controller.config.ForgotPasswordMessage, | ||||||
| 		BackgroundImage:       controller.config.BackgroundImage, | 		BackgroundImage:       controller.config.BackgroundImage, | ||||||
| 		OAuthAutoRedirect:     controller.config.OAuthAutoRedirect, | 		OAuthAutoRedirect:     controller.config.OAuthAutoRedirect, | ||||||
|   | |||||||
| @@ -1,135 +0,0 @@ | |||||||
| package controller_test |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"net/http/httptest" |  | ||||||
| 	"testing" |  | ||||||
| 	"tinyauth/internal/config" |  | ||||||
| 	"tinyauth/internal/controller" |  | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"gotest.tools/v3/assert" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var controllerCfg = controller.ContextControllerConfig{ |  | ||||||
| 	ConfiguredProviders:   []string{"github", "google", "generic"}, |  | ||||||
| 	Title:                 "Test App", |  | ||||||
| 	GenericName:           "Generic", |  | ||||||
| 	AppURL:                "http://localhost:8080", |  | ||||||
| 	CookieDomain:          "localhost", |  | ||||||
| 	ForgotPasswordMessage: "Contact admin to reset your password.", |  | ||||||
| 	BackgroundImage:       "/assets/bg.jpg", |  | ||||||
| 	OAuthAutoRedirect:     "google", |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var userContext = config.UserContext{ |  | ||||||
| 	Username:    "testuser", |  | ||||||
| 	Name:        "testuser", |  | ||||||
| 	Email:       "test@example.com", |  | ||||||
| 	IsLoggedIn:  true, |  | ||||||
| 	OAuth:       false, |  | ||||||
| 	Provider:    "username", |  | ||||||
| 	TotpPending: false, |  | ||||||
| 	OAuthGroups: "", |  | ||||||
| 	TotpEnabled: false, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) { |  | ||||||
| 	// 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(controllerCfg, group) |  | ||||||
| 	ctrl.SetupRoutes() |  | ||||||
|  |  | ||||||
| 	return router, recorder |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestAppContextHandler(t *testing.T) { |  | ||||||
| 	expectedRes := controller.AppContextResponse{ |  | ||||||
| 		Status:                200, |  | ||||||
| 		Message:               "Success", |  | ||||||
| 		ConfiguredProviders:   controllerCfg.ConfiguredProviders, |  | ||||||
| 		Title:                 controllerCfg.Title, |  | ||||||
| 		GenericName:           controllerCfg.GenericName, |  | ||||||
| 		AppURL:                controllerCfg.AppURL, |  | ||||||
| 		CookieDomain:          controllerCfg.CookieDomain, |  | ||||||
| 		ForgotPasswordMessage: controllerCfg.ForgotPasswordMessage, |  | ||||||
| 		BackgroundImage:       controllerCfg.BackgroundImage, |  | ||||||
| 		OAuthAutoRedirect:     controllerCfg.OAuthAutoRedirect, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	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:  userContext.IsLoggedIn, |  | ||||||
| 		Username:    userContext.Username, |  | ||||||
| 		Name:        userContext.Name, |  | ||||||
| 		Email:       userContext.Email, |  | ||||||
| 		Provider:    userContext.Provider, |  | ||||||
| 		OAuth:       userContext.OAuth, |  | ||||||
| 		TotpPending: userContext.TotpPending, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Test with context |  | ||||||
| 	router, recorder := setupContextController(&[]gin.HandlerFunc{ |  | ||||||
| 		func(c *gin.Context) { |  | ||||||
| 			c.Set("context", &userContext) |  | ||||||
| 			c.Next() |  | ||||||
| 		}, |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	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) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 200, recorder.Code) |  | ||||||
|  |  | ||||||
| 	err = json.Unmarshal(recorder.Body.Bytes(), &ctrlRes) |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	assert.DeepEqual(t, expectedRes, ctrlRes) |  | ||||||
| } |  | ||||||
| @@ -23,7 +23,7 @@ type OAuthControllerConfig struct { | |||||||
| 	RedirectCookieName string | 	RedirectCookieName string | ||||||
| 	SecureCookie       bool | 	SecureCookie       bool | ||||||
| 	AppURL             string | 	AppURL             string | ||||||
| 	CookieDomain       string | 	RootDomain         string | ||||||
| } | } | ||||||
|  |  | ||||||
| type OAuthController struct { | type OAuthController struct { | ||||||
| @@ -74,13 +74,13 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) { | |||||||
|  |  | ||||||
| 	state := service.GenerateState() | 	state := service.GenerateState() | ||||||
| 	authURL := service.GetAuthURL(state) | 	authURL := service.GetAuthURL(state) | ||||||
| 	c.SetCookie(controller.config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) | 	c.SetCookie(controller.config.CSRFCookieName, state, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) | ||||||
|  |  | ||||||
| 	redirectURI := c.Query("redirect_uri") | 	redirectURI := c.Query("redirect_uri") | ||||||
|  |  | ||||||
| 	if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) { | 	if redirectURI != "" && utils.IsRedirectSafe(redirectURI, controller.config.RootDomain) { | ||||||
| 		log.Debug().Msg("Setting redirect URI cookie") | 		log.Debug().Msg("Setting redirect URI cookie") | ||||||
| 		c.SetCookie(controller.config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) | 		c.SetCookie(controller.config.RedirectCookieName, redirectURI, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.JSON(200, gin.H{ | 	c.JSON(200, gin.H{ | ||||||
| @@ -108,12 +108,12 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { | |||||||
|  |  | ||||||
| 	if err != nil || state != csrfCookie { | 	if err != nil || state != csrfCookie { | ||||||
| 		log.Warn().Err(err).Msg("CSRF token mismatch or cookie missing") | 		log.Warn().Err(err).Msg("CSRF token mismatch or cookie missing") | ||||||
| 		c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) | 		c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) | ||||||
| 		c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) | 		c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL)) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) | 	c.SetCookie(controller.config.CSRFCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) | ||||||
|  |  | ||||||
| 	code := c.Query("code") | 	code := c.Query("code") | ||||||
| 	service, exists := controller.broker.GetService(req.Provider) | 	service, exists := controller.broker.GetService(req.Provider) | ||||||
| @@ -196,7 +196,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { | |||||||
|  |  | ||||||
| 	redirectURI, err := c.Cookie(controller.config.RedirectCookieName) | 	redirectURI, err := c.Cookie(controller.config.RedirectCookieName) | ||||||
|  |  | ||||||
| 	if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.CookieDomain) { | 	if err != nil || !utils.IsRedirectSafe(redirectURI, controller.config.RootDomain) { | ||||||
| 		log.Debug().Msg("No redirect URI cookie found, redirecting to app root") | 		log.Debug().Msg("No redirect URI cookie found, redirecting to app root") | ||||||
| 		c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL) | 		c.Redirect(http.StatusTemporaryRedirect, controller.config.AppURL) | ||||||
| 		return | 		return | ||||||
| @@ -212,6 +212,6 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.SetCookie(controller.config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true) | 	c.SetCookie(controller.config.RedirectCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.RootDomain), controller.config.SecureCookie, true) | ||||||
| 	c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.config.AppURL, queries.Encode())) | 	c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/continue?%s", controller.config.AppURL, queries.Encode())) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -55,15 +55,6 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if req.Proxy != "nginx" && req.Proxy != "traefik" && req.Proxy != "caddy" { |  | ||||||
| 		log.Warn().Str("proxy", req.Proxy).Msg("Invalid proxy") |  | ||||||
| 		c.JSON(400, gin.H{ |  | ||||||
| 			"status":  400, |  | ||||||
| 			"message": "Bad Request", |  | ||||||
| 		}) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html") | 	isBrowser := strings.Contains(c.Request.Header.Get("Accept"), "text/html") | ||||||
|  |  | ||||||
| 	if isBrowser { | 	if isBrowser { | ||||||
| @@ -260,7 +251,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { | |||||||
| 	c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode())) | 	c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/login?%s", controller.config.AppURL, queries.Encode())) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (controller *ProxyController) setHeaders(c *gin.Context, labels config.App) { | func (controller *ProxyController) setHeaders(c *gin.Context, labels config.AppLabels) { | ||||||
| 	c.Header("Authorization", c.Request.Header.Get("Authorization")) | 	c.Header("Authorization", c.Request.Header.Get("Authorization")) | ||||||
|  |  | ||||||
| 	headers := utils.ParseHeaders(labels.Response.Headers) | 	headers := utils.ParseHeaders(labels.Response.Headers) | ||||||
|   | |||||||
| @@ -1,164 +0,0 @@ | |||||||
| package controller_test |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"net/http/httptest" |  | ||||||
| 	"testing" |  | ||||||
| 	"tinyauth/internal/config" |  | ||||||
| 	"tinyauth/internal/controller" |  | ||||||
| 	"tinyauth/internal/service" |  | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"gotest.tools/v3/assert" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder, *service.AuthService) { |  | ||||||
| 	// 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() |  | ||||||
|  |  | ||||||
| 	// Database |  | ||||||
| 	databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{ |  | ||||||
| 		DatabasePath: "/tmp/tinyauth_test.db", |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, databaseService.Init()) |  | ||||||
|  |  | ||||||
| 	database := databaseService.GetDatabase() |  | ||||||
|  |  | ||||||
| 	// Docker |  | ||||||
| 	dockerService := service.NewDockerService() |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, dockerService.Init()) |  | ||||||
|  |  | ||||||
| 	// Auth service |  | ||||||
| 	authService := service.NewAuthService(service.AuthServiceConfig{ |  | ||||||
| 		Users: []config.User{ |  | ||||||
| 			{ |  | ||||||
| 				Username: "testuser", |  | ||||||
| 				Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		OauthWhitelist:    "", |  | ||||||
| 		SessionExpiry:     3600, |  | ||||||
| 		SecureCookie:      false, |  | ||||||
| 		CookieDomain:      "localhost", |  | ||||||
| 		LoginTimeout:      300, |  | ||||||
| 		LoginMaxRetries:   3, |  | ||||||
| 		SessionCookieName: "tinyauth-session", |  | ||||||
| 	}, dockerService, nil, database) |  | ||||||
|  |  | ||||||
| 	// Controller |  | ||||||
| 	ctrl := controller.NewProxyController(controller.ProxyControllerConfig{ |  | ||||||
| 		AppURL: "http://localhost:8080", |  | ||||||
| 	}, group, dockerService, authService) |  | ||||||
| 	ctrl.SetupRoutes() |  | ||||||
|  |  | ||||||
| 	return router, recorder, authService |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestProxyHandler(t *testing.T) { |  | ||||||
| 	// Setup |  | ||||||
| 	router, recorder, authService := setupProxyController(t, nil) |  | ||||||
|  |  | ||||||
| 	// Test invalid proxy |  | ||||||
| 	req := httptest.NewRequest("GET", "/api/auth/invalidproxy", nil) |  | ||||||
| 	router.ServeHTTP(recorder, req) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 400, recorder.Code) |  | ||||||
|  |  | ||||||
| 	// Test logged out user (traefik/caddy) |  | ||||||
| 	recorder = httptest.NewRecorder() |  | ||||||
| 	req = httptest.NewRequest("GET", "/api/auth/traefik", nil) |  | ||||||
| 	req.Header.Set("X-Forwarded-Proto", "https") |  | ||||||
| 	req.Header.Set("X-Forwarded-Host", "example.com") |  | ||||||
| 	req.Header.Set("X-Forwarded-Uri", "/somepath") |  | ||||||
| 	req.Header.Set("Accept", "text/html") |  | ||||||
| 	router.ServeHTTP(recorder, req) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 307, recorder.Code) |  | ||||||
| 	assert.Equal(t, "http://localhost:8080/login?redirect_uri=https%3A%2F%2Fexample.com%2Fsomepath", recorder.Header().Get("Location")) |  | ||||||
|  |  | ||||||
| 	// Test logged out user (nginx) |  | ||||||
| 	recorder = httptest.NewRecorder() |  | ||||||
| 	req = httptest.NewRequest("GET", "/api/auth/nginx", nil) |  | ||||||
| 	router.ServeHTTP(recorder, req) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 401, recorder.Code) |  | ||||||
|  |  | ||||||
| 	// Test logged in user |  | ||||||
| 	c := gin.CreateTestContextOnly(recorder, router) |  | ||||||
|  |  | ||||||
| 	err := authService.CreateSessionCookie(c, &config.SessionCookie{ |  | ||||||
| 		Username:    "testuser", |  | ||||||
| 		Name:        "testuser", |  | ||||||
| 		Email:       "testuser@example.com", |  | ||||||
| 		Provider:    "username", |  | ||||||
| 		TotpPending: false, |  | ||||||
| 		OAuthGroups: "", |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	cookie := c.Writer.Header().Get("Set-Cookie") |  | ||||||
|  |  | ||||||
| 	router, recorder, _ = setupProxyController(t, &[]gin.HandlerFunc{ |  | ||||||
| 		func(c *gin.Context) { |  | ||||||
| 			c.Set("context", &config.UserContext{ |  | ||||||
| 				Username:    "testuser", |  | ||||||
| 				Name:        "testuser", |  | ||||||
| 				Email:       "testuser@example.com", |  | ||||||
| 				IsLoggedIn:  true, |  | ||||||
| 				OAuth:       false, |  | ||||||
| 				Provider:    "username", |  | ||||||
| 				TotpPending: false, |  | ||||||
| 				OAuthGroups: "", |  | ||||||
| 				TotpEnabled: false, |  | ||||||
| 			}) |  | ||||||
| 			c.Next() |  | ||||||
| 		}, |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	req = httptest.NewRequest("GET", "/api/auth/traefik", nil) |  | ||||||
| 	req.Header.Set("Cookie", cookie) |  | ||||||
| 	req.Header.Set("Accept", "text/html") |  | ||||||
| 	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")) |  | ||||||
|  |  | ||||||
| 	// Ensure basic auth is disabled for TOTP enabled users |  | ||||||
| 	router, recorder, _ = setupProxyController(t, &[]gin.HandlerFunc{ |  | ||||||
| 		func(c *gin.Context) { |  | ||||||
| 			c.Set("context", &config.UserContext{ |  | ||||||
| 				Username:    "testuser", |  | ||||||
| 				Name:        "testuser", |  | ||||||
| 				Email:       "testuser@example.com", |  | ||||||
| 				IsLoggedIn:  true, |  | ||||||
| 				OAuth:       false, |  | ||||||
| 				Provider:    "basic", |  | ||||||
| 				TotpPending: false, |  | ||||||
| 				OAuthGroups: "", |  | ||||||
| 				TotpEnabled: true, |  | ||||||
| 			}) |  | ||||||
| 			c.Next() |  | ||||||
| 		}, |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	req = httptest.NewRequest("GET", "/api/auth/traefik", nil) |  | ||||||
| 	req.SetBasicAuth("testuser", "test") |  | ||||||
| 	router.ServeHTTP(recorder, req) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 401, recorder.Code) |  | ||||||
| } |  | ||||||
| @@ -1,57 +0,0 @@ | |||||||
| package controller_test |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"net/http/httptest" |  | ||||||
| 	"os" |  | ||||||
| 	"testing" |  | ||||||
| 	"tinyauth/internal/controller" |  | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"gotest.tools/v3/assert" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func TestResourcesHandler(t *testing.T) { |  | ||||||
| 	// Setup |  | ||||||
| 	gin.SetMode(gin.TestMode) |  | ||||||
| 	router := gin.New() |  | ||||||
| 	group := router.Group("/") |  | ||||||
|  |  | ||||||
| 	ctrl := controller.NewResourcesController(controller.ResourcesControllerConfig{ |  | ||||||
| 		ResourcesDir: "/tmp/tinyauth", |  | ||||||
| 	}, group) |  | ||||||
| 	ctrl.SetupRoutes() |  | ||||||
|  |  | ||||||
| 	// Create test data |  | ||||||
| 	err := os.Mkdir("/tmp/tinyauth", 0755) |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	defer os.RemoveAll("/tmp/tinyauth") |  | ||||||
|  |  | ||||||
| 	file, err := os.Create("/tmp/tinyauth/test.txt") |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	_, err = file.WriteString("This is a test file.") |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	file.Close() |  | ||||||
|  |  | ||||||
| 	// 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()) |  | ||||||
|  |  | ||||||
| 	// 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) |  | ||||||
|  |  | ||||||
| 	// Test directory traversal attack |  | ||||||
| 	req = httptest.NewRequest("GET", "/resources/../etc/passwd", nil) |  | ||||||
| 	recorder = httptest.NewRecorder() |  | ||||||
| 	router.ServeHTTP(recorder, req) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 404, recorder.Code) |  | ||||||
| } |  | ||||||
| @@ -22,7 +22,7 @@ type TotpRequest struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| type UserControllerConfig struct { | type UserControllerConfig struct { | ||||||
| 	CookieDomain string | 	RootDomain string | ||||||
| } | } | ||||||
|  |  | ||||||
| type UserController struct { | type UserController struct { | ||||||
| @@ -115,7 +115,7 @@ func (controller *UserController) loginHandler(c *gin.Context) { | |||||||
| 			err := controller.auth.CreateSessionCookie(c, &config.SessionCookie{ | 			err := controller.auth.CreateSessionCookie(c, &config.SessionCookie{ | ||||||
| 				Username:    user.Username, | 				Username:    user.Username, | ||||||
| 				Name:        utils.Capitalize(req.Username), | 				Name:        utils.Capitalize(req.Username), | ||||||
| 				Email:       fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain), | 				Email:       fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.RootDomain), | ||||||
| 				Provider:    "username", | 				Provider:    "username", | ||||||
| 				TotpPending: true, | 				TotpPending: true, | ||||||
| 			}) | 			}) | ||||||
| @@ -141,7 +141,7 @@ func (controller *UserController) loginHandler(c *gin.Context) { | |||||||
| 	err = controller.auth.CreateSessionCookie(c, &config.SessionCookie{ | 	err = controller.auth.CreateSessionCookie(c, &config.SessionCookie{ | ||||||
| 		Username: req.Username, | 		Username: req.Username, | ||||||
| 		Name:     utils.Capitalize(req.Username), | 		Name:     utils.Capitalize(req.Username), | ||||||
| 		Email:    fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain), | 		Email:    fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.RootDomain), | ||||||
| 		Provider: "username", | 		Provider: "username", | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| @@ -246,7 +246,7 @@ func (controller *UserController) totpHandler(c *gin.Context) { | |||||||
| 	err = controller.auth.CreateSessionCookie(c, &config.SessionCookie{ | 	err = controller.auth.CreateSessionCookie(c, &config.SessionCookie{ | ||||||
| 		Username: user.Username, | 		Username: user.Username, | ||||||
| 		Name:     utils.Capitalize(user.Username), | 		Name:     utils.Capitalize(user.Username), | ||||||
| 		Email:    fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.CookieDomain), | 		Email:    fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.RootDomain), | ||||||
| 		Provider: "username", | 		Provider: "username", | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,297 +0,0 @@ | |||||||
| package controller_test |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"net/http" |  | ||||||
| 	"net/http/httptest" |  | ||||||
| 	"strings" |  | ||||||
| 	"testing" |  | ||||||
| 	"time" |  | ||||||
| 	"tinyauth/internal/config" |  | ||||||
| 	"tinyauth/internal/controller" |  | ||||||
| 	"tinyauth/internal/service" |  | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"github.com/pquerna/otp/totp" |  | ||||||
| 	"gotest.tools/v3/assert" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var cookieValue string |  | ||||||
| var totpSecret = "6WFZXPEZRK5MZHHYAFW4DAOUYQMCASBJ" |  | ||||||
|  |  | ||||||
| func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) { |  | ||||||
| 	// 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() |  | ||||||
|  |  | ||||||
| 	// Database |  | ||||||
| 	databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{ |  | ||||||
| 		DatabasePath: "/tmp/tinyauth_test.db", |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, databaseService.Init()) |  | ||||||
|  |  | ||||||
| 	database := databaseService.GetDatabase() |  | ||||||
|  |  | ||||||
| 	// Auth service |  | ||||||
| 	authService := service.NewAuthService(service.AuthServiceConfig{ |  | ||||||
| 		Users: []config.User{ |  | ||||||
| 			{ |  | ||||||
| 				Username: "testuser", |  | ||||||
| 				Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test |  | ||||||
| 			}, |  | ||||||
| 			{ |  | ||||||
| 				Username:   "totpuser", |  | ||||||
| 				Password:   "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test |  | ||||||
| 				TotpSecret: totpSecret, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		OauthWhitelist:    "", |  | ||||||
| 		SessionExpiry:     3600, |  | ||||||
| 		SecureCookie:      false, |  | ||||||
| 		CookieDomain:      "localhost", |  | ||||||
| 		LoginTimeout:      300, |  | ||||||
| 		LoginMaxRetries:   3, |  | ||||||
| 		SessionCookieName: "tinyauth-session", |  | ||||||
| 	}, nil, nil, database) |  | ||||||
|  |  | ||||||
| 	// 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", |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	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", |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	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", |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	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, 200, recorder.Code) |  | ||||||
|  |  | ||||||
| 	loginResJson, err := json.Marshal(map[string]any{ |  | ||||||
| 		"message":     "TOTP required", |  | ||||||
| 		"status":      200, |  | ||||||
| 		"totpPending": true, |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	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:    "username", |  | ||||||
| 				TotpPending: true, |  | ||||||
| 				OAuthGroups: "", |  | ||||||
| 				TotpEnabled: true, |  | ||||||
| 			}) |  | ||||||
| 			c.Next() |  | ||||||
| 		}, |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	// Test |  | ||||||
| 	code, err := totp.GenerateCode(totpSecret, time.Now()) |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	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:    "username", |  | ||||||
| 				TotpPending: true, |  | ||||||
| 				OAuthGroups: "", |  | ||||||
| 				TotpEnabled: true, |  | ||||||
| 			}) |  | ||||||
| 			c.Next() |  | ||||||
| 		}, |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson))) |  | ||||||
| 	router.ServeHTTP(recorder, req) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 401, recorder.Code) |  | ||||||
|  |  | ||||||
| 	// 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:    "username", |  | ||||||
| 				TotpPending: false, |  | ||||||
| 				OAuthGroups: "", |  | ||||||
| 				TotpEnabled: false, |  | ||||||
| 			}) |  | ||||||
| 			c.Next() |  | ||||||
| 		}, |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	req = httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqJson))) |  | ||||||
| 	router.ServeHTTP(recorder, req) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 401, recorder.Code) |  | ||||||
| } |  | ||||||
| @@ -12,7 +12,7 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type ContextMiddlewareConfig struct { | type ContextMiddlewareConfig struct { | ||||||
| 	CookieDomain string | 	RootDomain string | ||||||
| } | } | ||||||
|  |  | ||||||
| type ContextMiddleware struct { | type ContextMiddleware struct { | ||||||
| @@ -134,7 +134,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { | |||||||
| 			c.Set("context", &config.UserContext{ | 			c.Set("context", &config.UserContext{ | ||||||
| 				Username:    user.Username, | 				Username:    user.Username, | ||||||
| 				Name:        utils.Capitalize(user.Username), | 				Name:        utils.Capitalize(user.Username), | ||||||
| 				Email:       fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.CookieDomain), | 				Email:       fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.RootDomain), | ||||||
| 				Provider:    "basic", | 				Provider:    "basic", | ||||||
| 				IsLoggedIn:  true, | 				IsLoggedIn:  true, | ||||||
| 				TotpEnabled: user.TotpSecret != "", | 				TotpEnabled: user.TotpSecret != "", | ||||||
| @@ -146,7 +146,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { | |||||||
| 			c.Set("context", &config.UserContext{ | 			c.Set("context", &config.UserContext{ | ||||||
| 				Username:   basic.Username, | 				Username:   basic.Username, | ||||||
| 				Name:       utils.Capitalize(basic.Username), | 				Name:       utils.Capitalize(basic.Username), | ||||||
| 				Email:      fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.CookieDomain), | 				Email:      fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.RootDomain), | ||||||
| 				Provider:   "basic", | 				Provider:   "basic", | ||||||
| 				IsLoggedIn: true, | 				IsLoggedIn: true, | ||||||
| 			}) | 			}) | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ type AuthServiceConfig struct { | |||||||
| 	OauthWhitelist    string | 	OauthWhitelist    string | ||||||
| 	SessionExpiry     int | 	SessionExpiry     int | ||||||
| 	SecureCookie      bool | 	SecureCookie      bool | ||||||
| 	CookieDomain      string | 	RootDomain        string | ||||||
| 	LoginTimeout      int | 	LoginTimeout      int | ||||||
| 	LoginMaxRetries   int | 	LoginMaxRetries   int | ||||||
| 	SessionCookieName string | 	SessionCookieName string | ||||||
| @@ -218,7 +218,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true) | 	c.SetCookie(auth.config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.config.RootDomain), auth.config.SecureCookie, true) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| @@ -236,7 +236,7 @@ func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error { | |||||||
| 		return res.Error | 		return res.Error | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	c.SetCookie(auth.config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true) | 	c.SetCookie(auth.config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.config.RootDomain), auth.config.SecureCookie, true) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| @@ -285,7 +285,7 @@ func (auth *AuthService) UserAuthConfigured() bool { | |||||||
| 	return len(auth.config.Users) > 0 || auth.ldap != nil | 	return len(auth.config.Users) > 0 || auth.ldap != nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.App) bool { | func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.AppLabels) bool { | ||||||
| 	if context.OAuth { | 	if context.OAuth { | ||||||
| 		log.Debug().Msg("Checking OAuth whitelist") | 		log.Debug().Msg("Checking OAuth whitelist") | ||||||
| 		return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) | 		return utils.CheckFilter(labels.OAuth.Whitelist, context.Email) | ||||||
| @@ -322,7 +322,7 @@ func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserConte | |||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *AuthService) IsAuthEnabled(uri string, path config.AppPath) (bool, error) { | func (auth *AuthService) IsAuthEnabled(uri string, path config.PathLabels) (bool, error) { | ||||||
| 	// Check for block list | 	// Check for block list | ||||||
| 	if path.Block != "" { | 	if path.Block != "" { | ||||||
| 		regex, err := regexp.Compile(path.Block) | 		regex, err := regexp.Compile(path.Block) | ||||||
| @@ -364,7 +364,7 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { | func (auth *AuthService) CheckIP(labels config.IPLabels, ip string) bool { | ||||||
| 	for _, blocked := range labels.Block { | 	for _, blocked := range labels.Block { | ||||||
| 		res, err := utils.FilterIP(blocked, ip) | 		res, err := utils.FilterIP(blocked, ip) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @@ -398,7 +398,7 @@ func (auth *AuthService) CheckIP(labels config.AppIP, ip string) bool { | |||||||
| 	return true | 	return true | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *AuthService) IsBypassedIP(labels config.AppIP, ip string) bool { | func (auth *AuthService) IsBypassedIP(labels config.IPLabels, ip string) bool { | ||||||
| 	for _, bypassed := range labels.Bypass { | 	for _, bypassed := range labels.Bypass { | ||||||
| 		res, err := utils.FilterIP(bypassed, ip) | 		res, err := utils.FilterIP(bypassed, ip) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"tinyauth/internal/config" | 	"tinyauth/internal/config" | ||||||
| 	"tinyauth/internal/utils/decoders" | 	"tinyauth/internal/utils" | ||||||
|  |  | ||||||
| 	container "github.com/docker/docker/api/types/container" | 	container "github.com/docker/docker/api/types/container" | ||||||
| 	"github.com/docker/docker/client" | 	"github.com/docker/docker/client" | ||||||
| @@ -55,17 +55,17 @@ func (docker *DockerService) DockerConnected() bool { | |||||||
| 	return err == nil | 	return err == nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { | func (docker *DockerService) GetLabels(appDomain string) (config.AppLabels, error) { | ||||||
| 	isConnected := docker.DockerConnected() | 	isConnected := docker.DockerConnected() | ||||||
|  |  | ||||||
| 	if !isConnected { | 	if !isConnected { | ||||||
| 		log.Debug().Msg("Docker not connected, returning empty labels") | 		log.Debug().Msg("Docker not connected, returning empty labels") | ||||||
| 		return config.App{}, nil | 		return config.AppLabels{}, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	containers, err := docker.GetContainers() | 	containers, err := docker.GetContainers() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return config.App{}, err | 		return config.AppLabels{}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, ctr := range containers { | 	for _, ctr := range containers { | ||||||
| @@ -75,7 +75,7 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { | |||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		labels, err := decoders.DecodeLabels(inspect.Config.Labels) | 		labels, err := utils.GetLabels(inspect.Config.Labels) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Warn().Str("id", ctr.ID).Err(err).Msg("Error getting container labels, skipping") | 			log.Warn().Str("id", ctr.ID).Err(err).Msg("Error getting container labels, skipping") | ||||||
| 			continue | 			continue | ||||||
| @@ -95,5 +95,5 @@ func (docker *DockerService) GetLabels(appDomain string) (config.App, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("No matching container found, returning empty labels") | 	log.Debug().Msg("No matching container found, returning empty labels") | ||||||
| 	return config.App{}, nil | 	return config.AppLabels{}, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,38 +8,30 @@ import ( | |||||||
| 	"tinyauth/internal/config" | 	"tinyauth/internal/config" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
|  |  | ||||||
| 	"github.com/rs/zerolog" | 	"github.com/rs/zerolog" | ||||||
| 	"github.com/weppos/publicsuffix-go/publicsuffix" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Get cookie domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) | // Get root domain parses a hostname and returns the upper domain (e.g. sub1.sub2.domain.com -> sub2.domain.com) | ||||||
| func GetCookieDomain(u string) (string, error) { | func GetRootDomain(appUrl string) (string, error) { | ||||||
| 	parsed, err := url.Parse(u) | 	appUrlParsed, err := url.Parse(appUrl) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	host := parsed.Hostname() | 	host := appUrlParsed.Hostname() | ||||||
|  |  | ||||||
| 	if netIP := net.ParseIP(host); netIP != nil { | 	if netIP := net.ParseIP(host); netIP != nil { | ||||||
| 		return "", errors.New("IP addresses not allowed") | 		return "", errors.New("IP addresses are not allowed") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	parts := strings.Split(host, ".") | 	urlParts := strings.Split(host, ".") | ||||||
|  |  | ||||||
| 	if len(parts) < 3 { | 	if len(urlParts) < 2 { | ||||||
| 		return "", errors.New("invalid app url, must be at least second level domain") | 		return "", errors.New("invalid domain, must be at least second level domain") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	domain := strings.Join(parts[1:], ".") | 	return strings.Join(urlParts[1:], "."), nil | ||||||
|  |  | ||||||
| 	_, err = publicsuffix.DomainFromListWithOptions(publicsuffix.DefaultList, domain, nil) |  | ||||||
|  |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", errors.New("domain in public suffix list, cannot set cookies") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return domain, nil |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func ParseFileToLine(content string) string { | func ParseFileToLine(content string) string { | ||||||
| @@ -57,7 +49,6 @@ func ParseFileToLine(content string) string { | |||||||
| } | } | ||||||
|  |  | ||||||
| func Filter[T any](slice []T, test func(T) bool) (res []T) { | func Filter[T any](slice []T, test func(T) bool) (res []T) { | ||||||
| 	res = make([]T, 0) |  | ||||||
| 	for _, value := range slice { | 	for _, value := range slice { | ||||||
| 		if test(value) { | 		if test(value) { | ||||||
| 			res = append(res, value) | 			res = append(res, value) | ||||||
| @@ -97,13 +88,13 @@ func IsRedirectSafe(redirectURL string, domain string) bool { | |||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	cookieDomain, err := GetCookieDomain(redirectURL) | 	upper, err := GetRootDomain(redirectURL) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if cookieDomain != domain { | 	if upper != domain { | ||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,202 +0,0 @@ | |||||||
| package utils_test |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"testing" |  | ||||||
| 	"tinyauth/internal/config" |  | ||||||
| 	"tinyauth/internal/utils" |  | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" |  | ||||||
| 	"gotest.tools/v3/assert" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func TestGetRootDomain(t *testing.T) { |  | ||||||
| 	// Normal case |  | ||||||
| 	domain := "http://sub.tinyauth.app" |  | ||||||
| 	expected := "tinyauth.app" |  | ||||||
| 	result, err := utils.GetCookieDomain(domain) |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	assert.Equal(t, expected, result) |  | ||||||
|  |  | ||||||
| 	// Domain with multiple subdomains |  | ||||||
| 	domain = "http://b.c.tinyauth.app" |  | ||||||
| 	expected = "c.tinyauth.app" |  | ||||||
| 	result, err = utils.GetCookieDomain(domain) |  | ||||||
| 	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) |  | ||||||
| 	assert.ErrorContains(t, err, "invalid app url, must be at least second level domain") |  | ||||||
|  |  | ||||||
| 	// IP address |  | ||||||
| 	domain = "http://10.10.10.10" |  | ||||||
| 	_, err = utils.GetCookieDomain(domain) |  | ||||||
| 	assert.ErrorContains(t, err, "IP addresses not allowed") |  | ||||||
|  |  | ||||||
| 	// Invalid URL |  | ||||||
| 	domain = "http://[::1]:namedport" |  | ||||||
| 	_, err = utils.GetCookieDomain(domain) |  | ||||||
| 	assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host") |  | ||||||
|  |  | ||||||
| 	// URL with scheme and path |  | ||||||
| 	domain = "https://sub.tinyauth.app/path" |  | ||||||
| 	expected = "tinyauth.app" |  | ||||||
| 	result, err = utils.GetCookieDomain(domain) |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	assert.Equal(t, expected, result) |  | ||||||
|  |  | ||||||
| 	// URL with port |  | ||||||
| 	domain = "http://sub.tinyauth.app:8080" |  | ||||||
| 	expected = "tinyauth.app" |  | ||||||
| 	result, err = utils.GetCookieDomain(domain) |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	assert.Equal(t, expected, result) |  | ||||||
|  |  | ||||||
| 	// Domain managed by ICANN |  | ||||||
| 	domain = "http://example.co.uk" |  | ||||||
| 	_, err = utils.GetCookieDomain(domain) |  | ||||||
| 	assert.Error(t, err, "domain in public suffix list, cannot set cookies") |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestParseFileToLine(t *testing.T) { |  | ||||||
| 	// Normal case |  | ||||||
| 	content := "user1\nuser2\nuser3" |  | ||||||
| 	expected := "user1,user2,user3" |  | ||||||
| 	result := utils.ParseFileToLine(content) |  | ||||||
| 	assert.Equal(t, expected, result) |  | ||||||
|  |  | ||||||
| 	// Case with empty lines and spaces |  | ||||||
| 	content = " user1 \n\n user2 \n user3 \n" |  | ||||||
| 	expected = "user1,user2,user3" |  | ||||||
| 	result = utils.ParseFileToLine(content) |  | ||||||
| 	assert.Equal(t, expected, result) |  | ||||||
|  |  | ||||||
| 	// Case with only empty lines |  | ||||||
| 	content = "\n\n\n" |  | ||||||
| 	expected = "" |  | ||||||
| 	result = utils.ParseFileToLine(content) |  | ||||||
| 	assert.Equal(t, expected, result) |  | ||||||
|  |  | ||||||
| 	// Case with single user |  | ||||||
| 	content = "singleuser" |  | ||||||
| 	expected = "singleuser" |  | ||||||
| 	result = utils.ParseFileToLine(content) |  | ||||||
| 	assert.Equal(t, expected, result) |  | ||||||
|  |  | ||||||
| 	// Case with trailing newline |  | ||||||
| 	content = "user1\nuser2\n" |  | ||||||
| 	expected = "user1,user2" |  | ||||||
| 	result = utils.ParseFileToLine(content) |  | ||||||
| 	assert.Equal(t, expected, result) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestFilter(t *testing.T) { |  | ||||||
| 	// Normal case |  | ||||||
| 	slice := []int{1, 2, 3, 4, 5} |  | ||||||
| 	testFunc := func(n int) bool { return n%2 == 0 } |  | ||||||
| 	expected := []int{2, 4} |  | ||||||
| 	result := utils.Filter(slice, testFunc) |  | ||||||
| 	assert.DeepEqual(t, expected, result) |  | ||||||
|  |  | ||||||
| 	// Case with no matches |  | ||||||
| 	slice = []int{1, 3, 5} |  | ||||||
| 	testFunc = func(n int) bool { return n%2 == 0 } |  | ||||||
| 	expected = []int{} |  | ||||||
| 	result = utils.Filter(slice, testFunc) |  | ||||||
| 	assert.DeepEqual(t, expected, result) |  | ||||||
|  |  | ||||||
| 	// Case with all matches |  | ||||||
| 	slice = []int{2, 4, 6} |  | ||||||
| 	testFunc = func(n int) bool { return n%2 == 0 } |  | ||||||
| 	expected = []int{2, 4, 6} |  | ||||||
| 	result = utils.Filter(slice, testFunc) |  | ||||||
| 	assert.DeepEqual(t, expected, result) |  | ||||||
|  |  | ||||||
| 	// Case with empty slice |  | ||||||
| 	slice = []int{} |  | ||||||
| 	testFunc = func(n int) bool { return n%2 == 0 } |  | ||||||
| 	expected = []int{} |  | ||||||
| 	result = utils.Filter(slice, testFunc) |  | ||||||
| 	assert.DeepEqual(t, expected, result) |  | ||||||
|  |  | ||||||
| 	// Case with different type (string) |  | ||||||
| 	sliceStr := []string{"apple", "banana", "cherry"} |  | ||||||
| 	testFuncStr := func(s string) bool { return len(s) > 5 } |  | ||||||
| 	expectedStr := []string{"banana", "cherry"} |  | ||||||
| 	resultStr := utils.Filter(sliceStr, testFuncStr) |  | ||||||
| 	assert.DeepEqual(t, expectedStr, resultStr) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestGetContext(t *testing.T) { |  | ||||||
| 	// Setup |  | ||||||
| 	gin.SetMode(gin.TestMode) |  | ||||||
| 	c, _ := gin.CreateTestContext(nil) |  | ||||||
|  |  | ||||||
| 	// Normal case |  | ||||||
| 	c.Set("context", &config.UserContext{Username: "testuser"}) |  | ||||||
| 	result, err := utils.GetContext(c) |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	assert.Equal(t, "testuser", result.Username) |  | ||||||
|  |  | ||||||
| 	// Case with no context |  | ||||||
| 	c.Set("context", nil) |  | ||||||
| 	_, err = utils.GetContext(c) |  | ||||||
| 	assert.Error(t, err, "invalid user context in request") |  | ||||||
|  |  | ||||||
| 	// Case with invalid context type |  | ||||||
| 	c.Set("context", "invalid type") |  | ||||||
| 	_, err = utils.GetContext(c) |  | ||||||
| 	assert.Error(t, err, "invalid user context in request") |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestIsRedirectSafe(t *testing.T) { |  | ||||||
| 	// Setup |  | ||||||
| 	domain := "example.com" |  | ||||||
|  |  | ||||||
| 	// Case with no subdomain |  | ||||||
| 	redirectURL := "http://example.com/welcome" |  | ||||||
| 	result := utils.IsRedirectSafe(redirectURL, domain) |  | ||||||
| 	assert.Equal(t, false, result) |  | ||||||
|  |  | ||||||
| 	// Case with different domain |  | ||||||
| 	redirectURL = "http://malicious.com/phishing" |  | ||||||
| 	result = utils.IsRedirectSafe(redirectURL, domain) |  | ||||||
| 	assert.Equal(t, false, result) |  | ||||||
|  |  | ||||||
| 	// Case with subdomain |  | ||||||
| 	redirectURL = "http://sub.example.com/page" |  | ||||||
| 	result = utils.IsRedirectSafe(redirectURL, domain) |  | ||||||
| 	assert.Equal(t, true, result) |  | ||||||
|  |  | ||||||
| 	// Case with empty redirect URL |  | ||||||
| 	redirectURL = "" |  | ||||||
| 	result = utils.IsRedirectSafe(redirectURL, domain) |  | ||||||
| 	assert.Equal(t, false, result) |  | ||||||
|  |  | ||||||
| 	// Case with invalid URL |  | ||||||
| 	redirectURL = "http://[::1]:namedport" |  | ||||||
| 	result = utils.IsRedirectSafe(redirectURL, domain) |  | ||||||
| 	assert.Equal(t, false, result) |  | ||||||
|  |  | ||||||
| 	// Case with URL having port |  | ||||||
| 	redirectURL = "http://sub.example.com:8080/page" |  | ||||||
| 	result = utils.IsRedirectSafe(redirectURL, domain) |  | ||||||
| 	assert.Equal(t, true, result) |  | ||||||
|  |  | ||||||
| 	// Case with URL having different subdomain |  | ||||||
| 	redirectURL = "http://another.example.com/page" |  | ||||||
| 	result = utils.IsRedirectSafe(redirectURL, domain) |  | ||||||
| 	assert.Equal(t, true, result) |  | ||||||
|  |  | ||||||
| 	// Case with URL having different TLD |  | ||||||
| 	redirectURL = "http://example.org/page" |  | ||||||
| 	result = utils.IsRedirectSafe(redirectURL, domain) |  | ||||||
| 	assert.Equal(t, false, result) |  | ||||||
| } |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| package decoders |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"tinyauth/internal/config" |  | ||||||
|  |  | ||||||
| 	"github.com/traefik/paerser/parser" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func DecodeLabels(labels map[string]string) (config.Apps, error) { |  | ||||||
| 	var appLabels config.Apps |  | ||||||
|  |  | ||||||
| 	err := parser.Decode(labels, &appLabels, "tinyauth", "tinyauth.apps") |  | ||||||
|  |  | ||||||
| 	if err != nil { |  | ||||||
| 		return config.Apps{}, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return appLabels, nil |  | ||||||
| } |  | ||||||
| @@ -1,73 +0,0 @@ | |||||||
| package decoders_test |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"reflect" |  | ||||||
| 	"testing" |  | ||||||
| 	"tinyauth/internal/config" |  | ||||||
| 	"tinyauth/internal/utils/decoders" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func TestDecodeLabels(t *testing.T) { |  | ||||||
| 	// Variables |  | ||||||
| 	expected := config.Apps{ |  | ||||||
| 		Apps: map[string]config.App{ |  | ||||||
| 			"foo": { |  | ||||||
| 				Config: config.AppConfig{ |  | ||||||
| 					Domain: "example.com", |  | ||||||
| 				}, |  | ||||||
| 				Users: config.AppUsers{ |  | ||||||
| 					Allow: "user1,user2", |  | ||||||
| 					Block: "user3", |  | ||||||
| 				}, |  | ||||||
| 				OAuth: config.AppOAuth{ |  | ||||||
| 					Whitelist: "somebody@example.com", |  | ||||||
| 					Groups:    "group3", |  | ||||||
| 				}, |  | ||||||
| 				IP: config.AppIP{ |  | ||||||
| 					Allow:  []string{"10.71.0.1/24", "10.71.0.2"}, |  | ||||||
| 					Block:  []string{"10.10.10.10", "10.0.0.0/24"}, |  | ||||||
| 					Bypass: []string{"192.168.1.1"}, |  | ||||||
| 				}, |  | ||||||
| 				Response: config.AppResponse{ |  | ||||||
| 					Headers: []string{"X-Foo=Bar", "X-Baz=Qux"}, |  | ||||||
| 					BasicAuth: config.AppBasicAuth{ |  | ||||||
| 						Username:     "admin", |  | ||||||
| 						Password:     "password", |  | ||||||
| 						PasswordFile: "/path/to/passwordfile", |  | ||||||
| 					}, |  | ||||||
| 				}, |  | ||||||
| 				Path: config.AppPath{ |  | ||||||
| 					Allow: "/public", |  | ||||||
| 					Block: "/private", |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	test := map[string]string{ |  | ||||||
| 		"tinyauth.apps.foo.config.domain":                   "example.com", |  | ||||||
| 		"tinyauth.apps.foo.users.allow":                     "user1,user2", |  | ||||||
| 		"tinyauth.apps.foo.users.block":                     "user3", |  | ||||||
| 		"tinyauth.apps.foo.oauth.whitelist":                 "somebody@example.com", |  | ||||||
| 		"tinyauth.apps.foo.oauth.groups":                    "group3", |  | ||||||
| 		"tinyauth.apps.foo.ip.allow":                        "10.71.0.1/24,10.71.0.2", |  | ||||||
| 		"tinyauth.apps.foo.ip.block":                        "10.10.10.10,10.0.0.0/24", |  | ||||||
| 		"tinyauth.apps.foo.ip.bypass":                       "192.168.1.1", |  | ||||||
| 		"tinyauth.apps.foo.response.headers":                "X-Foo=Bar,X-Baz=Qux", |  | ||||||
| 		"tinyauth.apps.foo.response.basicauth.username":     "admin", |  | ||||||
| 		"tinyauth.apps.foo.response.basicauth.password":     "password", |  | ||||||
| 		"tinyauth.apps.foo.response.basicauth.passwordfile": "/path/to/passwordfile", |  | ||||||
| 		"tinyauth.apps.foo.path.allow":                      "/public", |  | ||||||
| 		"tinyauth.apps.foo.path.block":                      "/private", |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Test |  | ||||||
| 	result, err := decoders.DecodeLabels(test) |  | ||||||
|  |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatalf("Unexpected error: %v", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if reflect.DeepEqual(expected, result) == false { |  | ||||||
| 		t.Fatalf("Expected %v but got %v", expected, result) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,31 +0,0 @@ | |||||||
| package utils |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"os" |  | ||||||
| 	"testing" |  | ||||||
|  |  | ||||||
| 	"gotest.tools/v3/assert" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func TestReadFile(t *testing.T) { |  | ||||||
| 	// Setup |  | ||||||
| 	file, err := os.Create("/tmp/tinyauth_test_file") |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	_, err = file.WriteString("file content\n") |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	err = file.Close() |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	defer os.Remove("/tmp/tinyauth_test_file") |  | ||||||
|  |  | ||||||
| 	// Normal case |  | ||||||
| 	content, err := ReadFile("/tmp/tinyauth_test_file") |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	assert.Equal(t, "file content\n", content) |  | ||||||
|  |  | ||||||
| 	// Non-existing file |  | ||||||
| 	content, err = ReadFile("/tmp/non_existing_file") |  | ||||||
| 	assert.ErrorContains(t, err, "no such file or directory") |  | ||||||
| 	assert.Equal(t, "", content) |  | ||||||
| } |  | ||||||
| @@ -3,8 +3,22 @@ package utils | |||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"tinyauth/internal/config" | ||||||
|  |  | ||||||
|  | 	"github.com/traefik/paerser/parser" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | func GetLabels(labels map[string]string) (config.Labels, error) { | ||||||
|  | 	var labelsParsed config.Labels | ||||||
|  |  | ||||||
|  | 	err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.apps") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return config.Labels{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return labelsParsed, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func ParseHeaders(headers []string) map[string]string { | func ParseHeaders(headers []string) map[string]string { | ||||||
| 	headerMap := make(map[string]string) | 	headerMap := make(map[string]string) | ||||||
| 	for _, header := range headers { | 	for _, header := range headers { | ||||||
|   | |||||||
| @@ -1,87 +0,0 @@ | |||||||
| package utils_test |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"testing" |  | ||||||
| 	"tinyauth/internal/utils" |  | ||||||
|  |  | ||||||
| 	"gotest.tools/v3/assert" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func TestParseHeaders(t *testing.T) { |  | ||||||
| 	// Normal case |  | ||||||
| 	headers := []string{ |  | ||||||
| 		"X-Custom-Header=Value", |  | ||||||
| 		"Another-Header=AnotherValue", |  | ||||||
| 	} |  | ||||||
| 	expected := map[string]string{ |  | ||||||
| 		"X-Custom-Header": "Value", |  | ||||||
| 		"Another-Header":  "AnotherValue", |  | ||||||
| 	} |  | ||||||
| 	assert.DeepEqual(t, expected, utils.ParseHeaders(headers)) |  | ||||||
|  |  | ||||||
| 	// Case insensitivity and trimming |  | ||||||
| 	headers = []string{ |  | ||||||
| 		"  x-custom-header =  Value  ", |  | ||||||
| 		"ANOTHER-HEADER=AnotherValue", |  | ||||||
| 	} |  | ||||||
| 	expected = map[string]string{ |  | ||||||
| 		"X-Custom-Header": "Value", |  | ||||||
| 		"Another-Header":  "AnotherValue", |  | ||||||
| 	} |  | ||||||
| 	assert.DeepEqual(t, expected, utils.ParseHeaders(headers)) |  | ||||||
|  |  | ||||||
| 	// Invalid headers (missing '=', empty key/value) |  | ||||||
| 	headers = []string{ |  | ||||||
| 		"InvalidHeader", |  | ||||||
| 		"=NoKey", |  | ||||||
| 		"NoValue=", |  | ||||||
| 		"   =   ", |  | ||||||
| 	} |  | ||||||
| 	expected = map[string]string{} |  | ||||||
| 	assert.DeepEqual(t, expected, utils.ParseHeaders(headers)) |  | ||||||
|  |  | ||||||
| 	// Headers with unsafe characters |  | ||||||
| 	headers = []string{ |  | ||||||
| 		"X-Custom-Header=Val\x00ue",       // Null byte |  | ||||||
| 		"Another-Header=Anoth\x7FerValue", // DEL character |  | ||||||
| 		"Good-Header=GoodValue", |  | ||||||
| 	} |  | ||||||
| 	expected = map[string]string{ |  | ||||||
| 		"X-Custom-Header": "Value", |  | ||||||
| 		"Another-Header":  "AnotherValue", |  | ||||||
| 		"Good-Header":     "GoodValue", |  | ||||||
| 	} |  | ||||||
| 	assert.DeepEqual(t, expected, utils.ParseHeaders(headers)) |  | ||||||
|  |  | ||||||
| 	// Header with spaces in key (should be ignored) |  | ||||||
| 	headers = []string{ |  | ||||||
| 		"X Custom Header=Value", |  | ||||||
| 		"Valid-Header=ValidValue", |  | ||||||
| 	} |  | ||||||
| 	expected = map[string]string{ |  | ||||||
| 		"Valid-Header": "ValidValue", |  | ||||||
| 	} |  | ||||||
| 	assert.DeepEqual(t, expected, utils.ParseHeaders(headers)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestSanitizeHeader(t *testing.T) { |  | ||||||
| 	// Normal case |  | ||||||
| 	header := "X-Custom-Header" |  | ||||||
| 	expected := "X-Custom-Header" |  | ||||||
| 	assert.Equal(t, expected, utils.SanitizeHeader(header)) |  | ||||||
|  |  | ||||||
| 	// Header with unsafe characters |  | ||||||
| 	header = "X-Cust\x00om-Hea\x7Fder" // Null byte and DEL character |  | ||||||
| 	expected = "X-Custom-Header" |  | ||||||
| 	assert.Equal(t, expected, utils.SanitizeHeader(header)) |  | ||||||
|  |  | ||||||
| 	// Header with only unsafe characters |  | ||||||
| 	header = "\x00\x01\x02\x7F" |  | ||||||
| 	expected = "" |  | ||||||
| 	assert.Equal(t, expected, utils.SanitizeHeader(header)) |  | ||||||
|  |  | ||||||
| 	// Header with spaces and tabs (should be preserved) |  | ||||||
| 	header = "X Custom\tHeader" |  | ||||||
| 	expected = "X Custom\tHeader" |  | ||||||
| 	assert.Equal(t, expected, utils.SanitizeHeader(header)) |  | ||||||
| } |  | ||||||
| @@ -48,12 +48,6 @@ func GetBasicAuth(username string, password string) string { | |||||||
| func FilterIP(filter string, ip string) (bool, error) { | func FilterIP(filter string, ip string) (bool, error) { | ||||||
| 	ipAddr := net.ParseIP(ip) | 	ipAddr := net.ParseIP(ip) | ||||||
|  |  | ||||||
| 	if ipAddr == nil { |  | ||||||
| 		return false, errors.New("invalid IP address") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	filter = strings.Replace(filter, "-", "/", -1) |  | ||||||
|  |  | ||||||
| 	if strings.Contains(filter, "/") { | 	if strings.Contains(filter, "/") { | ||||||
| 		_, cidr, err := net.ParseCIDR(filter) | 		_, cidr, err := net.ParseCIDR(filter) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|   | |||||||
| @@ -1,151 +0,0 @@ | |||||||
| package utils_test |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"os" |  | ||||||
| 	"testing" |  | ||||||
| 	"tinyauth/internal/utils" |  | ||||||
|  |  | ||||||
| 	"gotest.tools/v3/assert" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func TestGetSecret(t *testing.T) { |  | ||||||
| 	// Setup |  | ||||||
| 	file, err := os.Create("/tmp/tinyauth_test_secret") |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	_, err = file.WriteString("       secret       \n") |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	err = file.Close() |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	defer os.Remove("/tmp/tinyauth_test_secret") |  | ||||||
|  |  | ||||||
| 	// Get from config |  | ||||||
| 	assert.Equal(t, "mysecret", utils.GetSecret("mysecret", "")) |  | ||||||
|  |  | ||||||
| 	// Get from file |  | ||||||
| 	assert.Equal(t, "secret", utils.GetSecret("", "/tmp/tinyauth_test_secret")) |  | ||||||
|  |  | ||||||
| 	// Get from both (config should take precedence) |  | ||||||
| 	assert.Equal(t, "mysecret", utils.GetSecret("mysecret", "/tmp/tinyauth_test_secret")) |  | ||||||
|  |  | ||||||
| 	// Get from none |  | ||||||
| 	assert.Equal(t, "", utils.GetSecret("", "")) |  | ||||||
|  |  | ||||||
| 	// Get from non-existing file |  | ||||||
| 	assert.Equal(t, "", utils.GetSecret("", "/tmp/non_existing_file")) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestParseSecretFile(t *testing.T) { |  | ||||||
| 	// Normal case |  | ||||||
| 	content := "   mysecret   \n" |  | ||||||
| 	assert.Equal(t, "mysecret", utils.ParseSecretFile(content)) |  | ||||||
|  |  | ||||||
| 	// Multiple lines (should take the first non-empty line) |  | ||||||
| 	content = "\n\n   firstsecret   \nsecondsecret\n" |  | ||||||
| 	assert.Equal(t, "firstsecret", utils.ParseSecretFile(content)) |  | ||||||
|  |  | ||||||
| 	// All empty lines |  | ||||||
| 	content = "\n   \n  \n" |  | ||||||
| 	assert.Equal(t, "", utils.ParseSecretFile(content)) |  | ||||||
|  |  | ||||||
| 	// Empty content |  | ||||||
| 	content = "" |  | ||||||
| 	assert.Equal(t, "", utils.ParseSecretFile(content)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestGetBasicAuth(t *testing.T) { |  | ||||||
| 	// Normal case |  | ||||||
| 	username := "user" |  | ||||||
| 	password := "pass" |  | ||||||
| 	expected := "dXNlcjpwYXNz" // base64 of "user:pass" |  | ||||||
| 	assert.Equal(t, expected, utils.GetBasicAuth(username, password)) |  | ||||||
|  |  | ||||||
| 	// Empty username |  | ||||||
| 	username = "" |  | ||||||
| 	password = "pass" |  | ||||||
| 	expected = "OnBhc3M=" // base64 of ":pass" |  | ||||||
| 	assert.Equal(t, expected, utils.GetBasicAuth(username, password)) |  | ||||||
|  |  | ||||||
| 	// Empty password |  | ||||||
| 	username = "user" |  | ||||||
| 	password = "" |  | ||||||
| 	expected = "dXNlcjo=" // base64 of "user:" |  | ||||||
| 	assert.Equal(t, expected, utils.GetBasicAuth(username, password)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestFilterIP(t *testing.T) { |  | ||||||
| 	// Exact match IPv4 |  | ||||||
| 	ok, err := utils.FilterIP("10.10.0.1", "10.10.0.1") |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	assert.Equal(t, true, ok) |  | ||||||
|  |  | ||||||
| 	// Non-match IPv4 |  | ||||||
| 	ok, err = utils.FilterIP("10.10.0.1", "10.10.0.2") |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	assert.Equal(t, false, ok) |  | ||||||
|  |  | ||||||
| 	// CIDR match IPv4 |  | ||||||
| 	ok, err = utils.FilterIP("10.10.0.0/24", "10.10.0.2") |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	assert.Equal(t, true, ok) |  | ||||||
|  |  | ||||||
| 	// CIDR match IPv4 with '-' instead of '/' |  | ||||||
| 	ok, err = utils.FilterIP("10.10.10.0-24", "10.10.10.5") |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	assert.Equal(t, true, ok) |  | ||||||
|  |  | ||||||
| 	// CIDR non-match IPv4 |  | ||||||
| 	ok, err = utils.FilterIP("10.10.0.0/24", "10.5.0.1") |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	assert.Equal(t, false, ok) |  | ||||||
|  |  | ||||||
| 	// Invalid CIDR |  | ||||||
| 	ok, err = utils.FilterIP("10.10.0.0/222", "10.0.0.1") |  | ||||||
| 	assert.ErrorContains(t, err, "invalid CIDR address") |  | ||||||
| 	assert.Equal(t, false, ok) |  | ||||||
|  |  | ||||||
| 	// Invalid IP in filter |  | ||||||
| 	ok, err = utils.FilterIP("invalid_ip", "10.5.5.5") |  | ||||||
| 	assert.ErrorContains(t, err, "invalid IP address in filter") |  | ||||||
| 	assert.Equal(t, false, ok) |  | ||||||
|  |  | ||||||
| 	// Invalid IP to check |  | ||||||
| 	ok, err = utils.FilterIP("10.10.10.10", "invalid_ip") |  | ||||||
| 	assert.ErrorContains(t, err, "invalid IP address") |  | ||||||
| 	assert.Equal(t, false, ok) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestCheckFilter(t *testing.T) { |  | ||||||
| 	// Empty filter |  | ||||||
| 	assert.Equal(t, true, utils.CheckFilter("", "anystring")) |  | ||||||
|  |  | ||||||
| 	// Exact match |  | ||||||
| 	assert.Equal(t, true, utils.CheckFilter("hello", "hello")) |  | ||||||
|  |  | ||||||
| 	// Regex match |  | ||||||
| 	assert.Equal(t, true, utils.CheckFilter("/^h.*o$/", "hello")) |  | ||||||
|  |  | ||||||
| 	// Invalid regex |  | ||||||
| 	assert.Equal(t, false, utils.CheckFilter("/[unclosed", "test")) |  | ||||||
|  |  | ||||||
| 	// Comma-separated values |  | ||||||
| 	assert.Equal(t, true, utils.CheckFilter("apple, banana, cherry", "banana")) |  | ||||||
|  |  | ||||||
| 	// No match |  | ||||||
| 	assert.Equal(t, false, utils.CheckFilter("apple, banana, cherry", "grape")) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestGenerateIdentifier(t *testing.T) { |  | ||||||
| 	// Consistent output for same input |  | ||||||
| 	id1 := utils.GenerateIdentifier("teststring") |  | ||||||
| 	id2 := utils.GenerateIdentifier("teststring") |  | ||||||
| 	assert.Equal(t, id1, id2) |  | ||||||
|  |  | ||||||
| 	// Different output for different input |  | ||||||
| 	id3 := utils.GenerateIdentifier("differentstring") |  | ||||||
| 	assert.Assert(t, id1 != id3) |  | ||||||
|  |  | ||||||
| 	// Check length (should be 8 characters from first segment of UUID) |  | ||||||
| 	assert.Equal(t, 8, len(id1)) |  | ||||||
| } |  | ||||||
| @@ -1,50 +0,0 @@ | |||||||
| package utils_test |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"testing" |  | ||||||
| 	"tinyauth/internal/utils" |  | ||||||
|  |  | ||||||
| 	"gotest.tools/v3/assert" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func TestCapitalize(t *testing.T) { |  | ||||||
| 	// Test empty string |  | ||||||
| 	assert.Equal(t, "", utils.Capitalize("")) |  | ||||||
|  |  | ||||||
| 	// Test single character |  | ||||||
| 	assert.Equal(t, "A", utils.Capitalize("a")) |  | ||||||
|  |  | ||||||
| 	// Test multiple characters |  | ||||||
| 	assert.Equal(t, "Hello", utils.Capitalize("hello")) |  | ||||||
|  |  | ||||||
| 	// Test already capitalized |  | ||||||
| 	assert.Equal(t, "World", utils.Capitalize("World")) |  | ||||||
|  |  | ||||||
| 	// Test non-alphabetic first character |  | ||||||
| 	assert.Equal(t, "1number", utils.Capitalize("1number")) |  | ||||||
|  |  | ||||||
| 	// Test Unicode characters |  | ||||||
| 	assert.Equal(t, "Γειά", utils.Capitalize("γειά")) |  | ||||||
| 	assert.Equal(t, "Привет", utils.Capitalize("привет")) |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestCoalesceToString(t *testing.T) { |  | ||||||
| 	// Test with []any containing strings |  | ||||||
| 	assert.Equal(t, "a,b,c", utils.CoalesceToString([]any{"a", "b", "c"})) |  | ||||||
|  |  | ||||||
| 	// Test with []any containing mixed types |  | ||||||
| 	assert.Equal(t, "a,c", utils.CoalesceToString([]any{"a", 1, "c", true})) |  | ||||||
|  |  | ||||||
| 	// Test with []any containing no strings |  | ||||||
| 	assert.Equal(t, "", utils.CoalesceToString([]any{1, 2, 3})) |  | ||||||
|  |  | ||||||
| 	// Test with string input |  | ||||||
| 	assert.Equal(t, "hello", utils.CoalesceToString("hello")) |  | ||||||
|  |  | ||||||
| 	// Test with non-string, non-[]any input |  | ||||||
| 	assert.Equal(t, "", utils.CoalesceToString(123)) |  | ||||||
|  |  | ||||||
| 	// Test with nil input |  | ||||||
| 	assert.Equal(t, "", utils.CoalesceToString(nil)) |  | ||||||
| } |  | ||||||
| @@ -1,163 +0,0 @@ | |||||||
| package utils_test |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"os" |  | ||||||
| 	"testing" |  | ||||||
| 	"tinyauth/internal/utils" |  | ||||||
|  |  | ||||||
| 	"gotest.tools/v3/assert" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func TestGetUsers(t *testing.T) { |  | ||||||
| 	// Setup |  | ||||||
| 	file, err := os.Create("/tmp/tinyauth_users_test.txt") |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	_, err = file.WriteString("      user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G        \n         user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G                    ") // Spacing is on purpose |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	err = file.Close() |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
| 	defer os.Remove("/tmp/tinyauth_users_test.txt") |  | ||||||
|  |  | ||||||
| 	// Test file |  | ||||||
| 	users, err := utils.GetUsers("", "/tmp/tinyauth_users_test.txt") |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 2, len(users)) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, "user1", users[0].Username) |  | ||||||
| 	assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password) |  | ||||||
| 	assert.Equal(t, "user2", users[1].Username) |  | ||||||
| 	assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) |  | ||||||
|  |  | ||||||
| 	// Test config |  | ||||||
| 	users, err = utils.GetUsers("user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "") |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 2, len(users)) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, "user3", users[0].Username) |  | ||||||
| 	assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password) |  | ||||||
| 	assert.Equal(t, "user4", users[1].Username) |  | ||||||
| 	assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) |  | ||||||
|  |  | ||||||
| 	// Test both |  | ||||||
| 	users, err = utils.GetUsers("user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "/tmp/tinyauth_users_test.txt") |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 3, len(users)) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, "user5", users[0].Username) |  | ||||||
| 	assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password) |  | ||||||
| 	assert.Equal(t, "user1", users[1].Username) |  | ||||||
| 	assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) |  | ||||||
| 	assert.Equal(t, "user2", users[2].Username) |  | ||||||
| 	assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[2].Password) |  | ||||||
|  |  | ||||||
| 	// Test empty |  | ||||||
| 	users, err = utils.GetUsers("", "") |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 0, len(users)) |  | ||||||
|  |  | ||||||
| 	// Test non-existent file |  | ||||||
| 	users, err = utils.GetUsers("", "/tmp/non_existent_file.txt") |  | ||||||
|  |  | ||||||
| 	assert.ErrorContains(t, err, "no such file or directory") |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 0, len(users)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestParseUsers(t *testing.T) { |  | ||||||
| 	// Valid users |  | ||||||
| 	users, err := utils.ParseUsers("user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF") // user2 has TOTP |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 2, len(users)) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, "user1", users[0].Username) |  | ||||||
| 	assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password) |  | ||||||
| 	assert.Equal(t, "", users[0].TotpSecret) |  | ||||||
| 	assert.Equal(t, "user2", users[1].Username) |  | ||||||
| 	assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) |  | ||||||
| 	assert.Equal(t, "ABCDEF", users[1].TotpSecret) |  | ||||||
|  |  | ||||||
| 	// Valid weirdly spaced users |  | ||||||
| 	users, err = utils.ParseUsers("      user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G        ,         user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF                    ") // Spacing is on purpose |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, 2, len(users)) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, "user1", users[0].Username) |  | ||||||
| 	assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[0].Password) |  | ||||||
| 	assert.Equal(t, "", users[0].TotpSecret) |  | ||||||
| 	assert.Equal(t, "user2", users[1].Username) |  | ||||||
| 	assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) |  | ||||||
| 	assert.Equal(t, "ABCDEF", users[1].TotpSecret) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestParseUser(t *testing.T) { |  | ||||||
| 	// Valid user without TOTP |  | ||||||
| 	user, err := utils.ParseUser("user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G") |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, "user1", user.Username) |  | ||||||
| 	assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", user.Password) |  | ||||||
| 	assert.Equal(t, "", user.TotpSecret) |  | ||||||
|  |  | ||||||
| 	// Valid user with TOTP |  | ||||||
| 	user, err = utils.ParseUser("user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF") |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, "user2", user.Username) |  | ||||||
| 	assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", user.Password) |  | ||||||
| 	assert.Equal(t, "ABCDEF", user.TotpSecret) |  | ||||||
|  |  | ||||||
| 	// Valid user with $$ in password |  | ||||||
| 	user, err = utils.ParseUser("user3:pa$$word123") |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, "user3", user.Username) |  | ||||||
| 	assert.Equal(t, "pa$word123", user.Password) |  | ||||||
| 	assert.Equal(t, "", user.TotpSecret) |  | ||||||
|  |  | ||||||
| 	// User with spaces |  | ||||||
| 	user, err = utils.ParseUser("   user4   :   password123   :   TOTPSECRET   ") |  | ||||||
|  |  | ||||||
| 	assert.NilError(t, err) |  | ||||||
|  |  | ||||||
| 	assert.Equal(t, "user4", user.Username) |  | ||||||
| 	assert.Equal(t, "password123", user.Password) |  | ||||||
| 	assert.Equal(t, "TOTPSECRET", user.TotpSecret) |  | ||||||
|  |  | ||||||
| 	// Invalid users |  | ||||||
| 	_, err = utils.ParseUser("user1") // Missing password |  | ||||||
| 	assert.ErrorContains(t, err, "invalid user format") |  | ||||||
|  |  | ||||||
| 	_, err = utils.ParseUser("user1:") |  | ||||||
| 	assert.ErrorContains(t, err, "invalid user format") |  | ||||||
|  |  | ||||||
| 	_, err = utils.ParseUser(":password123") |  | ||||||
| 	assert.ErrorContains(t, err, "invalid user format") |  | ||||||
|  |  | ||||||
| 	_, err = utils.ParseUser("user1:password123:ABC:EXTRA") // Too many parts |  | ||||||
| 	assert.ErrorContains(t, err, "invalid user format") |  | ||||||
|  |  | ||||||
| 	_, err = utils.ParseUser("user1::ABC") |  | ||||||
| 	assert.ErrorContains(t, err, "invalid user format") |  | ||||||
|  |  | ||||||
| 	_, err = utils.ParseUser(":password123:ABC") |  | ||||||
| 	assert.ErrorContains(t, err, "invalid user format") |  | ||||||
|  |  | ||||||
| 	_, err = utils.ParseUser("   :   :   ") |  | ||||||
| 	assert.ErrorContains(t, err, "invalid user format") |  | ||||||
| } |  | ||||||
		Reference in New Issue
	
	Block a user