mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-03 23:55:44 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			51937906ad
			...
			feat/sqlit
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					32c7e8e045 | ||
| 
						 | 
					dc733a71c1 | ||
| 
						 | 
					3bc3cb9641 | ||
| 
						 | 
					7050e68c7c | 
@@ -1,7 +1,5 @@
 | 
			
		||||
PORT=3000
 | 
			
		||||
ADDRESS=0.0.0.0
 | 
			
		||||
SECRET=app_secret
 | 
			
		||||
SECRET_FILE=app_secret_file
 | 
			
		||||
APP_URL=http://localhost:3000
 | 
			
		||||
USERS=your_user_password_hash
 | 
			
		||||
USERS_FILE=users_file
 | 
			
		||||
@@ -31,3 +29,5 @@ FORGOT_PASSWORD_MESSAGE=Some message about resetting the password
 | 
			
		||||
OAUTH_AUTO_REDIRECT=none
 | 
			
		||||
BACKGROUND_IMAGE=some_image_url
 | 
			
		||||
GENERIC_SKIP_SSL=false
 | 
			
		||||
RESOURCES_DIR=/data/resources
 | 
			
		||||
DATABASE_PATH=/data/tinyauth.db
 | 
			
		||||
@@ -51,4 +51,6 @@ COPY --from=builder /tinyauth/tinyauth ./
 | 
			
		||||
 | 
			
		||||
EXPOSE 3000
 | 
			
		||||
 | 
			
		||||
VOLUME ["/data"]
 | 
			
		||||
 | 
			
		||||
ENTRYPOINT ["./tinyauth"]
 | 
			
		||||
@@ -28,7 +28,6 @@ var rootCmd = &cobra.Command{
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Check if secrets have a file associated with them
 | 
			
		||||
		conf.Secret = utils.GetSecret(conf.Secret, conf.SecretFile)
 | 
			
		||||
		conf.GithubClientSecret = utils.GetSecret(conf.GithubClientSecret, conf.GithubClientSecretFile)
 | 
			
		||||
		conf.GoogleClientSecret = utils.GetSecret(conf.GoogleClientSecret, conf.GoogleClientSecretFile)
 | 
			
		||||
		conf.GenericClientSecret = utils.GetSecret(conf.GenericClientSecret, conf.GenericClientSecretFile)
 | 
			
		||||
@@ -77,8 +76,6 @@ func init() {
 | 
			
		||||
	}{
 | 
			
		||||
		{"port", 3000, "Port to run the server on."},
 | 
			
		||||
		{"address", "0.0.0.0", "Address to bind the server to."},
 | 
			
		||||
		{"secret", "", "Secret to use for the cookie."},
 | 
			
		||||
		{"secret-file", "", "Path to a file containing the secret."},
 | 
			
		||||
		{"app-url", "", "The Tinyauth URL."},
 | 
			
		||||
		{"users", "", "Comma separated list of users in the format username:hash."},
 | 
			
		||||
		{"users-file", "", "Path to a file containing users in the format username:hash."},
 | 
			
		||||
@@ -115,6 +112,7 @@ func init() {
 | 
			
		||||
		{"ldap-insecure", false, "Skip certificate verification for the LDAP server."},
 | 
			
		||||
		{"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)."},
 | 
			
		||||
		{"database-path", "/data/tinyauth.db", "Path to the Sqlite database file."},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, opt := range configOptions {
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,7 @@ services:
 | 
			
		||||
      - ./cmd:/tinyauth/cmd
 | 
			
		||||
      - ./main.go:/tinyauth/main.go
 | 
			
		||||
      - /var/run/docker.sock:/var/run/docker.sock
 | 
			
		||||
      - ./data:/data
 | 
			
		||||
    ports:
 | 
			
		||||
      - 3000:3000
 | 
			
		||||
      - 4000:4000
 | 
			
		||||
 
 | 
			
		||||
@@ -20,9 +20,10 @@ services:
 | 
			
		||||
    container_name: tinyauth
 | 
			
		||||
    image: ghcr.io/steveiliop56/tinyauth:v3
 | 
			
		||||
    environment:
 | 
			
		||||
      - SECRET=some-random-32-chars-string
 | 
			
		||||
      - APP_URL=https://tinyauth.example.com
 | 
			
		||||
      - USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./data:/data
 | 
			
		||||
    labels:
 | 
			
		||||
      traefik.enable: true
 | 
			
		||||
      traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								go.mod
									
									
									
									
									
								
							@@ -6,6 +6,7 @@ require (
 | 
			
		||||
	github.com/cenkalti/backoff/v5 v5.0.3
 | 
			
		||||
	github.com/gin-gonic/gin v1.10.1
 | 
			
		||||
	github.com/go-playground/validator/v10 v10.27.0
 | 
			
		||||
	github.com/golang-migrate/migrate/v4 v4.18.3
 | 
			
		||||
	github.com/google/go-querystring v1.1.0
 | 
			
		||||
	github.com/google/uuid v1.6.0
 | 
			
		||||
	github.com/mdp/qrterminal/v3 v3.2.1
 | 
			
		||||
@@ -14,6 +15,9 @@ require (
 | 
			
		||||
	github.com/spf13/viper v1.20.1
 | 
			
		||||
	github.com/traefik/paerser v0.2.2
 | 
			
		||||
	golang.org/x/crypto v0.41.0
 | 
			
		||||
	gorm.io/driver/sqlite v1.6.0
 | 
			
		||||
	gorm.io/gorm v1.30.1
 | 
			
		||||
	modernc.org/sqlite v1.38.2
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
@@ -23,22 +27,34 @@ require (
 | 
			
		||||
	github.com/containerd/errdefs v1.0.0 // indirect
 | 
			
		||||
	github.com/containerd/errdefs/pkg v0.3.0 // indirect
 | 
			
		||||
	github.com/containerd/log v0.1.0 // indirect
 | 
			
		||||
	github.com/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-viper/mapstructure/v2 v2.4.0 // indirect
 | 
			
		||||
	github.com/hashicorp/errwrap v1.1.0 // indirect
 | 
			
		||||
	github.com/hashicorp/go-multierror v1.1.1 // indirect
 | 
			
		||||
	github.com/jinzhu/inflection v1.0.0 // indirect
 | 
			
		||||
	github.com/jinzhu/now v1.1.5 // indirect
 | 
			
		||||
	github.com/mattn/go-sqlite3 v1.14.32 // indirect
 | 
			
		||||
	github.com/moby/sys/atomicwriter v0.1.0 // indirect
 | 
			
		||||
	github.com/moby/term v0.5.2 // indirect
 | 
			
		||||
	github.com/morikuni/aec v1.0.0 // indirect
 | 
			
		||||
	github.com/ncruces/go-strftime v0.1.9 // indirect
 | 
			
		||||
	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 | 
			
		||||
	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // 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/sdk v1.34.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/mathutil v1.7.1 // indirect
 | 
			
		||||
	modernc.org/memory v1.11.0 // indirect
 | 
			
		||||
	rsc.io/qr v0.2.0 // indirect
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/Microsoft/go-winio v0.4.14 // indirect
 | 
			
		||||
	github.com/Microsoft/go-winio v0.6.2 // indirect
 | 
			
		||||
	github.com/atotto/clipboard v0.1.4 // indirect
 | 
			
		||||
	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 | 
			
		||||
	github.com/boombuler/barcode v1.0.2 // indirect
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										74
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										74
									
								
								go.sum
									
									
									
									
									
								
							@@ -4,8 +4,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
 | 
			
		||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
 | 
			
		||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
 | 
			
		||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
 | 
			
		||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
 | 
			
		||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
 | 
			
		||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
 | 
			
		||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
 | 
			
		||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
 | 
			
		||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
 | 
			
		||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
 | 
			
		||||
@@ -96,6 +96,10 @@ github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E
 | 
			
		||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
 | 
			
		||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
 | 
			
		||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
 | 
			
		||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
 | 
			
		||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
 | 
			
		||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
 | 
			
		||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
 | 
			
		||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
 | 
			
		||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 | 
			
		||||
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
 | 
			
		||||
@@ -120,6 +124,8 @@ github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
 | 
			
		||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 | 
			
		||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 | 
			
		||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 | 
			
		||||
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
 | 
			
		||||
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
 | 
			
		||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 | 
			
		||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
			
		||||
@@ -128,6 +134,8 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
 | 
			
		||||
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/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
 | 
			
		||||
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/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
 | 
			
		||||
@@ -136,6 +144,11 @@ github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzq
 | 
			
		||||
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/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.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
 | 
			
		||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 | 
			
		||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 | 
			
		||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 | 
			
		||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
 | 
			
		||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 | 
			
		||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 | 
			
		||||
@@ -152,6 +165,10 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
 | 
			
		||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
 | 
			
		||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
 | 
			
		||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
 | 
			
		||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 | 
			
		||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 | 
			
		||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
 | 
			
		||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 | 
			
		||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 | 
			
		||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 | 
			
		||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 | 
			
		||||
@@ -160,13 +177,14 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
 | 
			
		||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
 | 
			
		||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 | 
			
		||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 | 
			
		||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 | 
			
		||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 | 
			
		||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 | 
			
		||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 | 
			
		||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 | 
			
		||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 | 
			
		||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 | 
			
		||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 | 
			
		||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 | 
			
		||||
@@ -180,6 +198,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
 | 
			
		||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 | 
			
		||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
 | 
			
		||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 | 
			
		||||
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
 | 
			
		||||
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
 | 
			
		||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
 | 
			
		||||
@@ -205,19 +225,22 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
 | 
			
		||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 | 
			
		||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
 | 
			
		||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
 | 
			
		||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
 | 
			
		||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
 | 
			
		||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 | 
			
		||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 | 
			
		||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
 | 
			
		||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
 | 
			
		||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 | 
			
		||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
 | 
			
		||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
 | 
			
		||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 | 
			
		||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 | 
			
		||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 | 
			
		||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 | 
			
		||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 | 
			
		||||
@@ -229,7 +252,6 @@ github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6
 | 
			
		||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 | 
			
		||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
 | 
			
		||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
 | 
			
		||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 | 
			
		||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
 | 
			
		||||
@@ -245,11 +267,9 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
 | 
			
		||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
 | 
			
		||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
 | 
			
		||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 | 
			
		||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 | 
			
		||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 | 
			
		||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 | 
			
		||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
			
		||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
@@ -299,10 +319,12 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 | 
			
		||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
 | 
			
		||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
 | 
			
		||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
 | 
			
		||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
 | 
			
		||||
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/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.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
 | 
			
		||||
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-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=
 | 
			
		||||
@@ -316,10 +338,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
 | 
			
		||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
 | 
			
		||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
@@ -339,6 +359,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
 | 
			
		||||
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-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 | 
			
		||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
 | 
			
		||||
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
@@ -358,8 +380,38 @@ 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
			
		||||
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/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
 | 
			
		||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
 | 
			
		||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
 | 
			
		||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
 | 
			
		||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
 | 
			
		||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
 | 
			
		||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
 | 
			
		||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
 | 
			
		||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
 | 
			
		||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
 | 
			
		||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
 | 
			
		||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
 | 
			
		||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
 | 
			
		||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
 | 
			
		||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
 | 
			
		||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
 | 
			
		||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
 | 
			
		||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
 | 
			
		||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
 | 
			
		||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
 | 
			
		||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
 | 
			
		||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
 | 
			
		||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
 | 
			
		||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
 | 
			
		||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
 | 
			
		||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
 | 
			
		||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
 | 
			
		||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
 | 
			
		||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
 | 
			
		||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
 | 
			
		||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
 | 
			
		||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,12 @@ import (
 | 
			
		||||
	"embed"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Frontend assets
 | 
			
		||||
// Frontend
 | 
			
		||||
//
 | 
			
		||||
//go:embed dist
 | 
			
		||||
var FrontendAssets embed.FS
 | 
			
		||||
 | 
			
		||||
// Migrations
 | 
			
		||||
//
 | 
			
		||||
//go:embed migrations/*.sql
 | 
			
		||||
var Migrations embed.FS
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								internal/assets/migrations/000001_init_sqlite.down.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								internal/assets/migrations/000001_init_sqlite.down.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
DROP TABLE IF EXISTS "sessions";
 | 
			
		||||
							
								
								
									
										10
									
								
								internal/assets/migrations/000001_init_sqlite.up.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								internal/assets/migrations/000001_init_sqlite.up.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "sessions" (
 | 
			
		||||
    "uuid" TEXT NOT NULL PRIMARY KEY UNIQUE,
 | 
			
		||||
    "username" TEXT NOT NULL,
 | 
			
		||||
    "email" TEXT NOT NULL,
 | 
			
		||||
    "name" TEXT NOT NULL,
 | 
			
		||||
    "provider" TEXT NOT NULL,
 | 
			
		||||
    "totp_pending" BOOLEAN NOT NULL,
 | 
			
		||||
    "oauth_groups" TEXT NULL,
 | 
			
		||||
    "expiry" INTEGER NOT NULL
 | 
			
		||||
);
 | 
			
		||||
@@ -57,19 +57,6 @@ func (app *BootstrapApp) Setup() error {
 | 
			
		||||
	csrfCookieName := fmt.Sprintf("%s-%s", config.CSRFCookieName, cookieId)
 | 
			
		||||
	redirectCookieName := fmt.Sprintf("%s-%s", config.RedirectCookieName, cookieId)
 | 
			
		||||
 | 
			
		||||
	// Secrets
 | 
			
		||||
	encryptionSecret, err := utils.DeriveKey(app.Config.Secret, "encryption")
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hmacSecret, err := utils.DeriveKey(app.Config.Secret, "hmac")
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create configs
 | 
			
		||||
	authConfig := service.AuthServiceConfig{
 | 
			
		||||
		Users:             users,
 | 
			
		||||
@@ -80,8 +67,6 @@ func (app *BootstrapApp) Setup() error {
 | 
			
		||||
		LoginTimeout:      app.Config.LoginTimeout,
 | 
			
		||||
		LoginMaxRetries:   app.Config.LoginMaxRetries,
 | 
			
		||||
		SessionCookieName: sessionCookieName,
 | 
			
		||||
		HMACSecret:        hmacSecret,
 | 
			
		||||
		EncryptionSecret:  encryptionSecret,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Setup services
 | 
			
		||||
@@ -107,8 +92,24 @@ func (app *BootstrapApp) Setup() error {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Bootstrap database
 | 
			
		||||
	databaseService := service.NewDatabaseService(service.DatabaseServiceConfig{
 | 
			
		||||
		DatabasePath: app.Config.DatabasePath,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	log.Debug().Str("service", fmt.Sprintf("%T", databaseService)).Msg("Initializing service")
 | 
			
		||||
 | 
			
		||||
	err = databaseService.Init()
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("failed to initialize database service: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	database := databaseService.GetDatabase()
 | 
			
		||||
 | 
			
		||||
	// Create services
 | 
			
		||||
	dockerService := service.NewDockerService()
 | 
			
		||||
	authService := service.NewAuthService(authConfig, dockerService, ldapService)
 | 
			
		||||
	authService := service.NewAuthService(authConfig, dockerService, ldapService, database)
 | 
			
		||||
	oauthBrokerService := service.NewOAuthBrokerService(app.getOAuthBrokerConfig())
 | 
			
		||||
 | 
			
		||||
	// Initialize services
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,6 @@ var RedirectCookieName = "tinyauth-redirect"
 | 
			
		||||
type Config struct {
 | 
			
		||||
	Port                    int    `mapstructure:"port" validate:"required"`
 | 
			
		||||
	Address                 string `validate:"required,ip4_addr" mapstructure:"address"`
 | 
			
		||||
	Secret                  string `validate:"required,len=32" mapstructure:"secret"`
 | 
			
		||||
	SecretFile              string `mapstructure:"secret-file"`
 | 
			
		||||
	AppURL                  string `validate:"required,url" mapstructure:"app-url"`
 | 
			
		||||
	Users                   string `mapstructure:"users"`
 | 
			
		||||
	UsersFile               string `mapstructure:"users-file"`
 | 
			
		||||
@@ -56,6 +54,7 @@ type Config struct {
 | 
			
		||||
	LdapInsecure            bool   `mapstructure:"ldap-insecure"`
 | 
			
		||||
	LdapSearchFilter        string `mapstructure:"ldap-search-filter"`
 | 
			
		||||
	ResourcesDir            string `mapstructure:"resources-dir"`
 | 
			
		||||
	DatabasePath            string `mapstructure:"database-path" validate:"required"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type OAuthLabels struct {
 | 
			
		||||
@@ -112,6 +111,7 @@ type UserSearch struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SessionCookie struct {
 | 
			
		||||
	UUID        string
 | 
			
		||||
	Username    string
 | 
			
		||||
	Name        string
 | 
			
		||||
	Email       string
 | 
			
		||||
 
 | 
			
		||||
@@ -144,7 +144,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !controller.Auth.EmailWhitelisted(user.Email) {
 | 
			
		||||
	if !controller.Auth.IsEmailWhitelisted(user.Email) {
 | 
			
		||||
		queries, err := query.Values(config.UnauthorizedQuery{
 | 
			
		||||
			Username: user.Email,
 | 
			
		||||
		})
 | 
			
		||||
@@ -169,8 +169,18 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
 | 
			
		||||
		name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var usename string
 | 
			
		||||
 | 
			
		||||
	if user.PreferredUsername != "" {
 | 
			
		||||
		log.Debug().Msg("Using preferred username from OAuth provider")
 | 
			
		||||
		usename = user.PreferredUsername
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Debug().Msg("No preferred username from OAuth provider, using pseudo username")
 | 
			
		||||
		usename = strings.Replace(user.Email, "@", "_", -1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	controller.Auth.CreateSessionCookie(c, &config.SessionCookie{
 | 
			
		||||
		Username:    user.Email,
 | 
			
		||||
		Username:    usename,
 | 
			
		||||
		Name:        name,
 | 
			
		||||
		Email:       user.Email,
 | 
			
		||||
		Provider:    req.Provider,
 | 
			
		||||
 
 | 
			
		||||
@@ -89,7 +89,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	clientIP := c.ClientIP()
 | 
			
		||||
 | 
			
		||||
	if controller.Auth.BypassedIP(labels, clientIP) {
 | 
			
		||||
	if controller.Auth.IsBypassedIP(labels, clientIP) {
 | 
			
		||||
		c.Header("Authorization", c.Request.Header.Get("Authorization"))
 | 
			
		||||
 | 
			
		||||
		headers := utils.ParseHeaders(labels.Headers)
 | 
			
		||||
@@ -135,7 +135,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	authEnabled, err := controller.Auth.AuthEnabled(uri, labels)
 | 
			
		||||
	authEnabled, err := controller.Auth.IsAuthEnabled(uri, labels)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error().Err(err).Msg("Failed to check if auth is enabled for resource")
 | 
			
		||||
@@ -195,7 +195,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if userContext.IsLoggedIn {
 | 
			
		||||
		appAllowed := controller.Auth.ResourceAllowed(c, userContext, labels)
 | 
			
		||||
		appAllowed := controller.Auth.IsResourceAllowed(c, userContext, labels)
 | 
			
		||||
 | 
			
		||||
		if !appAllowed {
 | 
			
		||||
			log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource")
 | 
			
		||||
@@ -229,7 +229,7 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if userContext.OAuth {
 | 
			
		||||
			groupOK := controller.Auth.OAuthGroup(c, userContext, labels)
 | 
			
		||||
			groupOK := controller.Auth.IsInOAuthGroup(c, userContext, labels)
 | 
			
		||||
 | 
			
		||||
			if !groupOK {
 | 
			
		||||
				log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User OAuth groups do not match resource requirements")
 | 
			
		||||
 
 | 
			
		||||
@@ -83,7 +83,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
 | 
			
		||||
				goto basic
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if !m.Auth.EmailWhitelisted(cookie.Email) {
 | 
			
		||||
			if !m.Auth.IsEmailWhitelisted(cookie.Email) {
 | 
			
		||||
				log.Debug().Msg("Email from session cookie not whitelisted")
 | 
			
		||||
				m.Auth.DeleteSessionCookie(c)
 | 
			
		||||
				goto basic
 | 
			
		||||
 
 | 
			
		||||
@@ -49,18 +49,24 @@ func (m *ZerologMiddleware) Middleware() gin.HandlerFunc {
 | 
			
		||||
 | 
			
		||||
		latency := time.Since(tStart).String()
 | 
			
		||||
 | 
			
		||||
		// logPath check if the path should be logged normally or with debug
 | 
			
		||||
		subLogger := log.With().Str("method", method).
 | 
			
		||||
			Str("path", path).
 | 
			
		||||
			Str("address", address).
 | 
			
		||||
			Str("client_ip", clientIP).
 | 
			
		||||
			Int("status", code).
 | 
			
		||||
			Str("latency", latency).Logger()
 | 
			
		||||
 | 
			
		||||
		if m.logPath(method + " " + path) {
 | 
			
		||||
			switch {
 | 
			
		||||
			case code >= 200 && code < 300:
 | 
			
		||||
				log.Info().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request")
 | 
			
		||||
			case code >= 300 && code < 400:
 | 
			
		||||
				log.Warn().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request")
 | 
			
		||||
			case code >= 400:
 | 
			
		||||
				log.Error().Str("method", method).Str("path", path).Str("address", address).Str("clientIp", clientIP).Int("status", code).Str("latency", latency).Msg("Request")
 | 
			
		||||
			case code >= 400 && code < 500:
 | 
			
		||||
				subLogger.Warn().Msg("Client Error")
 | 
			
		||||
			case code >= 500:
 | 
			
		||||
				subLogger.Error().Msg("Server Error")
 | 
			
		||||
			default:
 | 
			
		||||
				subLogger.Info().Msg("Request")
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			log.Debug().Str("method", method).Str("path", path).Str("address", address).Int("status", code).Str("latency", latency).Msg("Request")
 | 
			
		||||
			subLogger.Debug().Msg("Request")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								internal/model/session_model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/model/session_model.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
type Session struct {
 | 
			
		||||
	UUID        string `gorm:"column:uuid;primaryKey"`
 | 
			
		||||
	Username    string `gorm:"column:username"`
 | 
			
		||||
	Email       string `gorm:"column:email"`
 | 
			
		||||
	Name        string `gorm:"column:name"`
 | 
			
		||||
	Provider    string `gorm:"column:provider"`
 | 
			
		||||
	TOTPPending bool   `gorm:"column:totp_pending"`
 | 
			
		||||
	OAuthGroups string `gorm:"column:oauth_groups"`
 | 
			
		||||
	Expiry      int64  `gorm:"column:expiry"`
 | 
			
		||||
}
 | 
			
		||||
@@ -7,12 +7,14 @@ import (
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
	"tinyauth/internal/config"
 | 
			
		||||
	"tinyauth/internal/model"
 | 
			
		||||
	"tinyauth/internal/utils"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/gorilla/sessions"
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	"github.com/rs/zerolog/log"
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type LoginAttempt struct {
 | 
			
		||||
@@ -30,8 +32,6 @@ type AuthServiceConfig struct {
 | 
			
		||||
	LoginTimeout      int
 | 
			
		||||
	LoginMaxRetries   int
 | 
			
		||||
	SessionCookieName string
 | 
			
		||||
	HMACSecret        string
 | 
			
		||||
	EncryptionSecret  string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AuthService struct {
 | 
			
		||||
@@ -39,49 +39,24 @@ type AuthService struct {
 | 
			
		||||
	Docker        *DockerService
 | 
			
		||||
	LoginAttempts map[string]*LoginAttempt
 | 
			
		||||
	LoginMutex    sync.RWMutex
 | 
			
		||||
	Store         *sessions.CookieStore
 | 
			
		||||
	LDAP          *LdapService
 | 
			
		||||
	Database      *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService) *AuthService {
 | 
			
		||||
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, database *gorm.DB) *AuthService {
 | 
			
		||||
	return &AuthService{
 | 
			
		||||
		Config:        config,
 | 
			
		||||
		Docker:        docker,
 | 
			
		||||
		LoginAttempts: make(map[string]*LoginAttempt),
 | 
			
		||||
		LDAP:          ldap,
 | 
			
		||||
		Database:      database,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) Init() error {
 | 
			
		||||
	store := sessions.NewCookieStore([]byte(auth.Config.HMACSecret), []byte(auth.Config.EncryptionSecret))
 | 
			
		||||
	store.Options = &sessions.Options{
 | 
			
		||||
		Path:     "/",
 | 
			
		||||
		MaxAge:   auth.Config.SessionExpiry,
 | 
			
		||||
		Secure:   auth.Config.SecureCookie,
 | 
			
		||||
		HttpOnly: true,
 | 
			
		||||
		Domain:   fmt.Sprintf(".%s", auth.Config.Domain),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auth.Store = store
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) GetSession(c *gin.Context) (*sessions.Session, error) {
 | 
			
		||||
	session, err := auth.Store.Get(c.Request, auth.Config.SessionCookieName)
 | 
			
		||||
 | 
			
		||||
	// If there was an error getting the session, it might be invalid so let's clear it and retry
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Debug().Err(err).Msg("Error getting session, creating a new one")
 | 
			
		||||
		c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true)
 | 
			
		||||
		session, err = auth.Store.New(c.Request, auth.Config.SessionCookieName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return session, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) SearchUser(username string) config.UserSearch {
 | 
			
		||||
	if auth.GetLocalUser(username).Username != "" {
 | 
			
		||||
		return config.UserSearch{
 | 
			
		||||
@@ -158,30 +133,24 @@ func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) {
 | 
			
		||||
	auth.LoginMutex.RLock()
 | 
			
		||||
	defer auth.LoginMutex.RUnlock()
 | 
			
		||||
 | 
			
		||||
	// Return false if rate limiting is not configured
 | 
			
		||||
	if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
 | 
			
		||||
		return false, 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check if the identifier exists in the map
 | 
			
		||||
	attempt, exists := auth.LoginAttempts[identifier]
 | 
			
		||||
	if !exists {
 | 
			
		||||
		return false, 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If account is locked, check if lock time has expired
 | 
			
		||||
	if attempt.LockedUntil.After(time.Now()) {
 | 
			
		||||
		// Calculate remaining lockout time in seconds
 | 
			
		||||
		remaining := int(time.Until(attempt.LockedUntil).Seconds())
 | 
			
		||||
		return true, remaining
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Lock has expired
 | 
			
		||||
	return false, 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
 | 
			
		||||
	// Skip if rate limiting is not configured
 | 
			
		||||
	if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -189,133 +158,132 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
 | 
			
		||||
	auth.LoginMutex.Lock()
 | 
			
		||||
	defer auth.LoginMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	// Get current attempt record or create a new one
 | 
			
		||||
	attempt, exists := auth.LoginAttempts[identifier]
 | 
			
		||||
	if !exists {
 | 
			
		||||
		attempt = &LoginAttempt{}
 | 
			
		||||
		auth.LoginAttempts[identifier] = attempt
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update last attempt time
 | 
			
		||||
	attempt.LastAttempt = time.Now()
 | 
			
		||||
 | 
			
		||||
	// If successful login, reset failed attempts
 | 
			
		||||
	if success {
 | 
			
		||||
		attempt.FailedAttempts = 0
 | 
			
		||||
		attempt.LockedUntil = time.Time{} // Reset lock time
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Increment failed attempts
 | 
			
		||||
	attempt.FailedAttempts++
 | 
			
		||||
 | 
			
		||||
	// If max retries reached, lock the account
 | 
			
		||||
	if attempt.FailedAttempts >= auth.Config.LoginMaxRetries {
 | 
			
		||||
		attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second)
 | 
			
		||||
		log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) EmailWhitelisted(email string) bool {
 | 
			
		||||
func (auth *AuthService) IsEmailWhitelisted(email string) bool {
 | 
			
		||||
	return utils.CheckFilter(auth.Config.OauthWhitelist, email)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error {
 | 
			
		||||
	session, err := auth.GetSession(c)
 | 
			
		||||
	uuid, err := uuid.NewRandom()
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var sessionExpiry int
 | 
			
		||||
	var expiry int
 | 
			
		||||
 | 
			
		||||
	if data.TotpPending {
 | 
			
		||||
		sessionExpiry = 3600
 | 
			
		||||
		expiry = 3600
 | 
			
		||||
	} else {
 | 
			
		||||
		sessionExpiry = auth.Config.SessionExpiry
 | 
			
		||||
		expiry = auth.Config.SessionExpiry
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session.Values["username"] = data.Username
 | 
			
		||||
	session.Values["name"] = data.Name
 | 
			
		||||
	session.Values["email"] = data.Email
 | 
			
		||||
	session.Values["provider"] = data.Provider
 | 
			
		||||
	session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
 | 
			
		||||
	session.Values["totpPending"] = data.TotpPending
 | 
			
		||||
	session.Values["oauthGroups"] = data.OAuthGroups
 | 
			
		||||
	session := model.Session{
 | 
			
		||||
		UUID:        uuid.String(),
 | 
			
		||||
		Username:    data.Username,
 | 
			
		||||
		Email:       data.Email,
 | 
			
		||||
		Name:        data.Name,
 | 
			
		||||
		Provider:    data.Provider,
 | 
			
		||||
		TOTPPending: data.TotpPending,
 | 
			
		||||
		OAuthGroups: data.OAuthGroups,
 | 
			
		||||
		Expiry:      time.Now().Add(time.Duration(expiry) * time.Second).Unix(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = auth.Database.Create(&session).Error
 | 
			
		||||
 | 
			
		||||
	err = session.Save(c.Request, c.Writer)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.SetCookie(auth.Config.SessionCookieName, session.UUID, expiry, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) DeleteSessionCookie(c *gin.Context) error {
 | 
			
		||||
	session, err := auth.GetSession(c)
 | 
			
		||||
	cookie, err := c.Cookie(auth.Config.SessionCookieName)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Delete all values in the session
 | 
			
		||||
	for key := range session.Values {
 | 
			
		||||
		delete(session.Values, key)
 | 
			
		||||
	res := auth.Database.Unscoped().Where("uuid = ?", cookie).Delete(&model.Session{})
 | 
			
		||||
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		return res.Error
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = session.Save(c.Request, c.Writer)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Clear the cookie in the browser
 | 
			
		||||
	c.SetCookie(auth.Config.SessionCookieName, "", -1, "/", fmt.Sprintf(".%s", auth.Config.Domain), auth.Config.SecureCookie, true)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, error) {
 | 
			
		||||
	session, err := auth.GetSession(c)
 | 
			
		||||
	cookie, err := c.Cookie(auth.Config.SessionCookieName)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return config.SessionCookie{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	username, usernameOk := session.Values["username"].(string)
 | 
			
		||||
	email, emailOk := session.Values["email"].(string)
 | 
			
		||||
	name, nameOk := session.Values["name"].(string)
 | 
			
		||||
	provider, providerOK := session.Values["provider"].(string)
 | 
			
		||||
	expiry, expiryOk := session.Values["expiry"].(int64)
 | 
			
		||||
	totpPending, totpPendingOk := session.Values["totpPending"].(bool)
 | 
			
		||||
	oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string)
 | 
			
		||||
	var session model.Session
 | 
			
		||||
 | 
			
		||||
	// If any data is missing, delete the session cookie
 | 
			
		||||
	if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk {
 | 
			
		||||
		log.Warn().Msg("Session cookie is invalid")
 | 
			
		||||
		auth.DeleteSessionCookie(c)
 | 
			
		||||
		return config.SessionCookie{}, nil
 | 
			
		||||
	res := auth.Database.Unscoped().Where("uuid = ?", cookie).First(&session)
 | 
			
		||||
 | 
			
		||||
	if res.Error != nil {
 | 
			
		||||
		return config.SessionCookie{}, res.Error
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If the session cookie has expired, delete it
 | 
			
		||||
	if time.Now().Unix() > expiry {
 | 
			
		||||
		log.Warn().Msg("Session cookie expired")
 | 
			
		||||
		auth.DeleteSessionCookie(c)
 | 
			
		||||
		return config.SessionCookie{}, nil
 | 
			
		||||
	if res.RowsAffected == 0 {
 | 
			
		||||
		return config.SessionCookie{}, fmt.Errorf("session not found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	currentTime := time.Now().Unix()
 | 
			
		||||
 | 
			
		||||
	if currentTime > session.Expiry {
 | 
			
		||||
		res := auth.Database.Unscoped().Where("uuid = ?", session.UUID).Delete(&model.Session{})
 | 
			
		||||
		if res.Error != nil {
 | 
			
		||||
			log.Error().Err(res.Error).Msg("Failed to delete expired session")
 | 
			
		||||
		}
 | 
			
		||||
		return config.SessionCookie{}, fmt.Errorf("session expired")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return config.SessionCookie{
 | 
			
		||||
		Username:    username,
 | 
			
		||||
		Name:        name,
 | 
			
		||||
		Email:       email,
 | 
			
		||||
		Provider:    provider,
 | 
			
		||||
		TotpPending: totpPending,
 | 
			
		||||
		OAuthGroups: oauthGroups,
 | 
			
		||||
		UUID:        session.UUID,
 | 
			
		||||
		Username:    session.Username,
 | 
			
		||||
		Email:       session.Email,
 | 
			
		||||
		Name:        session.Name,
 | 
			
		||||
		Provider:    session.Provider,
 | 
			
		||||
		TotpPending: session.TOTPPending,
 | 
			
		||||
		OAuthGroups: session.OAuthGroups,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) UserAuthConfigured() bool {
 | 
			
		||||
	// If there are users or LDAP is configured, return true
 | 
			
		||||
	return len(auth.Config.Users) > 0 || auth.LDAP != nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) ResourceAllowed(c *gin.Context, context config.UserContext, labels config.Labels) bool {
 | 
			
		||||
func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, labels config.Labels) bool {
 | 
			
		||||
	if context.OAuth {
 | 
			
		||||
		log.Debug().Msg("Checking OAuth whitelist")
 | 
			
		||||
		return utils.CheckFilter(labels.OAuth.Whitelist, context.Email)
 | 
			
		||||
@@ -325,7 +293,7 @@ func (auth *AuthService) ResourceAllowed(c *gin.Context, context config.UserCont
 | 
			
		||||
	return utils.CheckFilter(labels.Users, context.Username)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) OAuthGroup(c *gin.Context, context config.UserContext, labels config.Labels) bool {
 | 
			
		||||
func (auth *AuthService) IsInOAuthGroup(c *gin.Context, context config.UserContext, labels config.Labels) bool {
 | 
			
		||||
	if labels.OAuth.Groups == "" {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
@@ -335,23 +303,20 @@ func (auth *AuthService) OAuthGroup(c *gin.Context, context config.UserContext,
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Split the groups by comma (no need to parse since they are from the API response)
 | 
			
		||||
	// No need to parse since they are from the API response
 | 
			
		||||
	oauthGroups := strings.Split(context.OAuthGroups, ",")
 | 
			
		||||
 | 
			
		||||
	// For every group check if it is in the required groups
 | 
			
		||||
	for _, group := range oauthGroups {
 | 
			
		||||
		if utils.CheckFilter(labels.OAuth.Groups, group) {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// No groups matched
 | 
			
		||||
	log.Debug().Msg("No groups matched")
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) AuthEnabled(uri string, labels config.Labels) (bool, error) {
 | 
			
		||||
	// If the label is empty, auth is enabled
 | 
			
		||||
func (auth *AuthService) IsAuthEnabled(uri string, labels config.Labels) (bool, error) {
 | 
			
		||||
	if labels.Allowed == "" {
 | 
			
		||||
		return true, nil
 | 
			
		||||
	}
 | 
			
		||||
@@ -362,12 +327,10 @@ func (auth *AuthService) AuthEnabled(uri string, labels config.Labels) (bool, er
 | 
			
		||||
		return true, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If the regex matches the URI, auth is not enabled
 | 
			
		||||
	if regex.MatchString(uri) {
 | 
			
		||||
		return false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Auth enabled
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -384,7 +347,6 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool {
 | 
			
		||||
	// Check if the IP is in block list
 | 
			
		||||
	for _, blocked := range labels.IP.Block {
 | 
			
		||||
		res, err := utils.FilterIP(blocked, ip)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -397,7 +359,6 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// For every IP in the allow list, check if the IP matches
 | 
			
		||||
	for _, allowed := range labels.IP.Allow {
 | 
			
		||||
		res, err := utils.FilterIP(allowed, ip)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -410,7 +371,6 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If not in allowed range and allowed range is not empty, deny access
 | 
			
		||||
	if len(labels.IP.Allow) > 0 {
 | 
			
		||||
		log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access")
 | 
			
		||||
		return false
 | 
			
		||||
@@ -420,8 +380,7 @@ func (auth *AuthService) CheckIP(labels config.Labels, ip string) bool {
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *AuthService) BypassedIP(labels config.Labels, ip string) bool {
 | 
			
		||||
	// For every IP in the bypass list, check if the IP matches
 | 
			
		||||
func (auth *AuthService) IsBypassedIP(labels config.Labels, ip string) bool {
 | 
			
		||||
	for _, bypassed := range labels.IP.Bypass {
 | 
			
		||||
		res, err := utils.FilterIP(bypassed, ip)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										78
									
								
								internal/service/database_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								internal/service/database_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
package service
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"tinyauth/internal/assets"
 | 
			
		||||
 | 
			
		||||
	"github.com/glebarez/sqlite"
 | 
			
		||||
	"github.com/golang-migrate/migrate/v4"
 | 
			
		||||
	sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
 | 
			
		||||
	"github.com/golang-migrate/migrate/v4/source/iofs"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DatabaseServiceConfig struct {
 | 
			
		||||
	DatabasePath string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DatabaseService struct {
 | 
			
		||||
	Config   DatabaseServiceConfig
 | 
			
		||||
	Database *gorm.DB
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewDatabaseService(config DatabaseServiceConfig) *DatabaseService {
 | 
			
		||||
	return &DatabaseService{
 | 
			
		||||
		Config: config,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ds *DatabaseService) Init() error {
 | 
			
		||||
	gormDB, err := gorm.Open(sqlite.Open(ds.Config.DatabasePath), &gorm.Config{})
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sqlDB, err := gormDB.DB()
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sqlDB.SetMaxOpenConns(1)
 | 
			
		||||
 | 
			
		||||
	err = ds.migrateDatabase(sqlDB)
 | 
			
		||||
 | 
			
		||||
	if err != nil && err != migrate.ErrNoChange {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ds.Database = gormDB
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ds *DatabaseService) migrateDatabase(sqlDB *sql.DB) error {
 | 
			
		||||
	data, err := iofs.New(assets.Migrations, "migrations")
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	target, err := sqliteMigrate.WithInstance(sqlDB, &sqliteMigrate.Config{})
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	migrator, err := migrate.NewWithInstance("iofs", data, "tinyauth", target)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return migrator.Up()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ds *DatabaseService) GetDatabase() *gorm.DB {
 | 
			
		||||
	return ds.Database
 | 
			
		||||
}
 | 
			
		||||
@@ -1,17 +1,13 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	"golang.org/x/crypto/hkdf"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func GetSecret(conf string, file string) string {
 | 
			
		||||
@@ -49,24 +45,6 @@ func GetBasicAuth(username string, password string) string {
 | 
			
		||||
	return base64.StdEncoding.EncodeToString([]byte(auth))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func DeriveKey(secret string, info string) (string, error) {
 | 
			
		||||
	hash := sha256.New
 | 
			
		||||
	hkdf := hkdf.New(hash, []byte(secret), nil, []byte(info)) // I am not using a salt because I just want two different keys from one secret, maybe bad practice
 | 
			
		||||
	key := make([]byte, 24)
 | 
			
		||||
 | 
			
		||||
	_, err := io.ReadFull(hkdf, key)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if bytes.Equal(key, make([]byte, 24)) {
 | 
			
		||||
		return "", errors.New("derived key is empty")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	encodedKey := base64.StdEncoding.EncodeToString(key)
 | 
			
		||||
	return encodedKey, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func FilterIP(filter string, ip string) (bool, error) {
 | 
			
		||||
	ipAddr := net.ParseIP(ip)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user