mirror of
https://github.com/MizuchiLabs/mantrae.git
synced 2025-12-21 06:10:04 -06:00
feat: added oidc support
This commit is contained in:
49
go.mod
49
go.mod
@@ -9,9 +9,10 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.0
|
||||
github.com/caarlos0/env/v11 v11.3.1
|
||||
github.com/cloudflare/cloudflare-go v0.115.0
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/docker/docker v27.4.1+incompatible
|
||||
github.com/domodwyer/mailyak/v3 v3.6.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
@@ -19,12 +20,13 @@ require (
|
||||
github.com/joeig/go-powerdns/v3 v3.16.0
|
||||
github.com/pressly/goose/v3 v3.24.3
|
||||
github.com/traefik/paerser v0.2.2
|
||||
github.com/traefik/traefik/v3 v3.4.0
|
||||
github.com/traefik/traefik/v3 v3.4.1
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
|
||||
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b
|
||||
golang.org/x/net v0.40.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
modernc.org/sqlite v1.37.0
|
||||
modernc.org/sqlite v1.37.1
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
||||
@@ -37,7 +39,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
||||
@@ -45,6 +47,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
||||
github.com/aws/smithy-go v1.22.3 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
@@ -55,7 +58,7 @@ require (
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
@@ -81,18 +84,18 @@ require (
|
||||
github.com/unrolled/render v1.7.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.11.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.11.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
@@ -100,11 +103,11 @@ require (
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
|
||||
google.golang.org/grpc v1.72.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/grpc v1.72.2 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
modernc.org/libc v1.65.6 // indirect
|
||||
modernc.org/libc v1.65.8 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.10.0 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
100
go.sum
100
go.sum
@@ -30,14 +30,14 @@ github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcu
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1 h1:4nm2G6A4pV9rdlWzGMPv4BNtQp22v1hg3yrtkYpeLl8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.1/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2 h1:BCG7DCXEXpNCcpwCxg1oi9pkJWH2+eZzTn9MY56MbVw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.2/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3 h1:BRXS0U76Z8wfF+bnkilA2QwpIch6URlm++yPUt9QPmQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.3/go.mod h1:bNXKFFyaiVvWuR6O16h/I1724+aXe/tAkA9/QS01t5k=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.0 h1:fV4XIU5sn/x8gjRouoJpDVHj+ExJaUk4prYF+eb6qTs=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.0/go.mod h1:qbn305Je/IofWBJ4bJz/Q7pDEtnnoInw/dGt71v6rHE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
|
||||
@@ -50,10 +50,14 @@ github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5m
|
||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM=
|
||||
github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -82,8 +86,8 @@ github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBj
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
@@ -179,8 +183,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ=
|
||||
github.com/traefik/paerser v0.2.2/go.mod h1:7BBDd4FANoVgaTZG+yh26jI6CA2nds7D/4VTEdIsh24=
|
||||
github.com/traefik/traefik/v3 v3.4.0 h1:DkHTzki3o1Fohgh0948JsFaeadZYOn8D8+9pZsQEOn0=
|
||||
github.com/traefik/traefik/v3 v3.4.0/go.mod h1:qZMqfdT077jnnWD14jnD10imucqdPFmCTBlOHrjNCfk=
|
||||
github.com/traefik/traefik/v3 v3.4.1 h1:QBO/C9ILViPVBhsmY8nEnoobTULxg6oW1jUTX8FFh8w=
|
||||
github.com/traefik/traefik/v3 v3.4.1/go.mod h1:8FHoFbX5P+zMQ3UUjjfrDH87BDSbHllcUQyiI2wCP3o=
|
||||
github.com/unrolled/render v1.7.0 h1:1yke01/tZiZpiXfUG+zqB+6fq3G4I+KDmnh0EhPq7So=
|
||||
github.com/unrolled/render v1.7.0/go.mod h1:LwQSeDhjml8NLjIO9GJO1/1qpFJxtfVIpzxXKjfVkoI=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -191,32 +195,34 @@ go.opentelemetry.io/collector/pdata v1.10.0 h1:oLyPLGvPTQrcRT64ZVruwvmH/u3SHTfNo
|
||||
go.opentelemetry.io/collector/pdata v1.10.0/go.mod h1:IHxHsp+Jq/xfjORQMDJjSH6jvedOSTOyu3nbxqhWSYE=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0 h1:HMUytBT3uGhPKYY/u/G5MR9itrlSO2SMOsSD3Tk3k7A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.11.0/go.mod h1:hdDXsiNLmdW/9BF2jQpnHHlhFajpWCEYfM6e5m2OAZg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0 h1:C/Wi2F8wEmbxJ9Kuzw/nhP+Z9XaHYMkyDmXy6yR2cjw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.11.0/go.mod h1:0Lr9vmGKzadCTgsiBydxr6GEZ8SsZ7Ks53LzjWG5Ar4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
|
||||
go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y=
|
||||
go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.11.0 h1:7bAOpjpGglWhdEzP8z0VXc4jObOiDEwr3IYbhBnjk2c=
|
||||
go.opentelemetry.io/otel/sdk/log v0.11.0/go.mod h1:dndLTxZbwBstZoqsJB3kGsRPkpAgaJrWfQg3lhlHFFY=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 h1:tPLwQlXbJ8NSOfZc4OkgU5h2A38M4c9kfHSVc4PFQGs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2/go.mod h1:QTnxBwT/1rBIgAG1goq6xMydfYOBKU6KTiYF4fp5zL8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
|
||||
go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc=
|
||||
go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0=
|
||||
go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0=
|
||||
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
|
||||
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
@@ -226,8 +232,8 @@ 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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA=
|
||||
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
@@ -240,6 +246,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
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/sync v0.0.0-20190227155943-e225da77a7e6/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=
|
||||
@@ -271,12 +279,12 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 h1:WvBuA5rjZx9SNIzgcU53OohgZy6lKSus++uY4xLaWKc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
||||
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -294,18 +302,18 @@ modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
|
||||
modernc.org/fileutil v1.3.1/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/libc v1.65.6 h1:OhJUhmuJ6MVZdqL5qmnd0/my46DKGFhSX4WOR7ijfyE=
|
||||
modernc.org/libc v1.65.6/go.mod h1:MOiGAM9lrMBT9L8xT1nO41qYl5eg9gCp9/kWhz5L7WA=
|
||||
modernc.org/libc v1.65.8 h1:7PXRJai0TXZ8uNA3srsmYzmTyrLoHImV5QxHeni108Q=
|
||||
modernc.org/libc v1.65.8/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
||||
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.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
|
||||
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
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.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
||||
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
||||
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
|
||||
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
|
||||
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=
|
||||
|
||||
@@ -85,7 +85,7 @@ func CreateAgent(a *config.App) http.HandlerFunc {
|
||||
claims := &agent.AgentClaims{
|
||||
AgentID: uuid.New().String(),
|
||||
ProfileID: profileID,
|
||||
ServerURL: serverUrl.String("http://localhost:3000"),
|
||||
ServerURL: serverUrl.String("http://127.0.0.1:3000"),
|
||||
}
|
||||
|
||||
// Generate a JWT for the agent and let it expire based on the cleanup interval
|
||||
@@ -184,7 +184,7 @@ func RotateAgentToken(a *config.App) http.HandlerFunc {
|
||||
claims := &agent.AgentClaims{
|
||||
AgentID: dbAgent.ID,
|
||||
ProfileID: dbAgent.ProfileID,
|
||||
ServerURL: serverUrl.String("http://localhost:3000"),
|
||||
ServerURL: serverUrl.String("http://127.0.0.1:3000"),
|
||||
}
|
||||
|
||||
agentInterval, err := a.SM.Get(r.Context(), settings.KeyAgentCleanupInterval)
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MizuchiLabs/mantrae/internal/api/middlewares"
|
||||
"github.com/MizuchiLabs/mantrae/internal/config"
|
||||
"github.com/MizuchiLabs/mantrae/internal/db"
|
||||
"github.com/MizuchiLabs/mantrae/internal/mail"
|
||||
@@ -45,64 +45,51 @@ func Login(a *config.App) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
remember := r.URL.Query().Get("remember") == "true"
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
if r.URL.Query().Get("remember") == "true" {
|
||||
expirationTime = time.Now().Add(7 * 24 * time.Hour)
|
||||
if remember {
|
||||
expirationTime = time.Now().Add(30 * 24 * time.Hour)
|
||||
}
|
||||
|
||||
token, err := util.EncodeUserJWT(request.Username, a.Config.Secret, expirationTime)
|
||||
jwt, err := util.EncodeUserJWT(request.Username, a.Config.Secret, expirationTime)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := q.UpdateUserLastLogin(r.Context(), user.ID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
fmt.Printf("Failed to update last login for user %s: %v\n", user.Username, err)
|
||||
}
|
||||
|
||||
response := map[string]any{
|
||||
"token": token,
|
||||
"user": user,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: util.CookieName,
|
||||
Value: jwt,
|
||||
Path: "/",
|
||||
MaxAge: int(expirationTime.Unix() - time.Now().Unix()),
|
||||
HttpOnly: true,
|
||||
Secure: r.TLS != nil,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyJWT checks the validity of a JWT token provided in cookies or Authorization header.
|
||||
func VerifyJWT(a *config.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
q := a.Conn.GetQuery()
|
||||
var token string
|
||||
func Logout(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: util.CookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: r.TLS != nil,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
// Check for token in cookies and Authorization header
|
||||
if cookie, err := r.Cookie("token"); err == nil {
|
||||
token = cookie.Value
|
||||
} else {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
|
||||
token = strings.TrimPrefix(authHeader, "Bearer ")
|
||||
} else {
|
||||
http.Error(w, "Token cannot be empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data, err := util.DecodeUserJWT(token, a.Config.Secret)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Invalid token: %s", err.Error()), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := q.GetUserByUsername(r.Context(), data.Username)
|
||||
if err != nil {
|
||||
http.Error(w, "User not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
// Verify returns the currently logged in user
|
||||
func Verify(w http.ResponseWriter, r *http.Request) {
|
||||
user := r.Context().Value(middlewares.AuthUserKey).(db.GetUserByUsernameRow)
|
||||
|
||||
response := map[string]any{"user": user}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -110,7 +97,6 @@ func VerifyJWT(a *config.App) http.HandlerFunc {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyOTP allows users to login using an OTP token, to reset their password
|
||||
@@ -149,7 +135,7 @@ func VerifyOTP(a *config.App) http.HandlerFunc {
|
||||
}
|
||||
|
||||
expirationTime := time.Now().Add(1 * time.Hour)
|
||||
token, err := util.EncodeUserJWT(user.Username, a.Config.Secret, expirationTime)
|
||||
jwt, err := util.EncodeUserJWT(user.Username, a.Config.Secret, expirationTime)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -160,10 +146,17 @@ func VerifyOTP(a *config.App) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]any{
|
||||
"token": token,
|
||||
"user": user,
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: util.CookieName,
|
||||
Value: jwt,
|
||||
Path: "/",
|
||||
MaxAge: int(expirationTime.Unix() - time.Now().Unix()),
|
||||
HttpOnly: true,
|
||||
Secure: r.TLS != nil,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
response := map[string]any{"user": user}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
429
internal/api/handler/oidc.go
Normal file
429
internal/api/handler/oidc.go
Normal file
@@ -0,0 +1,429 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MizuchiLabs/mantrae/internal/config"
|
||||
"github.com/MizuchiLabs/mantrae/internal/db"
|
||||
"github.com/MizuchiLabs/mantrae/internal/settings"
|
||||
"github.com/MizuchiLabs/mantrae/internal/util"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type OIDCConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
IssuerURL string `json:"issuer_url"`
|
||||
Scopes []string `json:"scopes"`
|
||||
Provider string `json:"provider"`
|
||||
UsePKCE bool `json:"use_pkce"`
|
||||
}
|
||||
|
||||
type OIDCUserInfo struct {
|
||||
Sub string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Name string `json:"name"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
}
|
||||
|
||||
func OIDCLogin(a *config.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
oidcConfig, oauth2Config, _, err := setupOIDCConfig(r.Context(), a)
|
||||
if err != nil {
|
||||
http.Error(w, "OIDC not configured: "+err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
if !oidcConfig.Enabled {
|
||||
http.Error(w, "OIDC disabled", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate state for CSRF protection
|
||||
state, err := generateRandomState()
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to generate state", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Store state in cookie
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "oauth_state",
|
||||
Value: state,
|
||||
Path: "/",
|
||||
MaxAge: 600,
|
||||
HttpOnly: true,
|
||||
Secure: r.TLS != nil,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
opts := []oauth2.AuthCodeOption{oauth2.AccessTypeOffline}
|
||||
|
||||
// Add PKCE if enabled
|
||||
if oidcConfig.UsePKCE {
|
||||
verifier := oauth2.GenerateVerifier()
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "pkce_verifier",
|
||||
Value: verifier,
|
||||
Path: "/",
|
||||
MaxAge: 600,
|
||||
HttpOnly: true,
|
||||
Secure: r.TLS != nil,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
opts = append(opts, oauth2.S256ChallengeOption(verifier))
|
||||
}
|
||||
|
||||
authURL := oauth2Config.AuthCodeURL(state, opts...)
|
||||
http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
|
||||
}
|
||||
}
|
||||
|
||||
func OIDCCallback(a *config.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
oidcConfig, oauth2Config, verifier, err := setupOIDCConfig(r.Context(), a)
|
||||
if err != nil {
|
||||
http.Error(w, "OIDC not configured: "+err.Error(), http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify state
|
||||
stateCookie, err := r.Cookie("oauth_state")
|
||||
if err != nil || stateCookie.Value != r.URL.Query().Get("state") {
|
||||
http.Error(w, "Invalid state parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear state cookie
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "oauth_state",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
})
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
if code == "" {
|
||||
http.Error(w, "No authorization code received", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
opts := []oauth2.AuthCodeOption{}
|
||||
|
||||
// Handle PKCE
|
||||
if oidcConfig.UsePKCE {
|
||||
verifierCookie, err := r.Cookie("pkce_verifier")
|
||||
if err != nil {
|
||||
http.Error(w, "PKCE verifier not found", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "pkce_verifier",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
})
|
||||
|
||||
opts = append(opts, oauth2.VerifierOption(verifierCookie.Value))
|
||||
}
|
||||
|
||||
// Exchange code for token
|
||||
token, err := oauth2Config.Exchange(r.Context(), code, opts...)
|
||||
if err != nil {
|
||||
http.Error(
|
||||
w,
|
||||
fmt.Sprintf("Token exchange failed: %v", err),
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify ID token
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
http.Error(w, "No id_token in response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
verifiedToken, err := verifier.Verify(r.Context(), idToken)
|
||||
if err != nil {
|
||||
http.Error(
|
||||
w,
|
||||
fmt.Sprintf("Token verification failed: %v", err),
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract user info from verified token
|
||||
var userInfo OIDCUserInfo
|
||||
if err := verifiedToken.Claims(&userInfo); err != nil {
|
||||
http.Error(
|
||||
w,
|
||||
fmt.Sprintf("Failed to parse claims: %v", err),
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Find or create user
|
||||
q := a.Conn.GetQuery()
|
||||
user, err := findOrCreateOIDCUser(r.Context(), q, &userInfo)
|
||||
if err != nil {
|
||||
http.Error(
|
||||
w,
|
||||
fmt.Sprintf("Failed to process user: %v", err),
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate JWT and set cookie
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
jwtToken, err := util.EncodeUserJWT(user.Username, a.Config.Secret, expirationTime)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to generate JWT", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := q.UpdateUserLastLogin(r.Context(), user.ID); err != nil {
|
||||
fmt.Printf("Failed to update last login for user %s: %v\n", user.Username, err)
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: util.CookieName,
|
||||
Value: jwtToken,
|
||||
Path: "/",
|
||||
MaxAge: int(expirationTime.Unix() - time.Now().Unix()),
|
||||
HttpOnly: true,
|
||||
Secure: r.TLS != nil,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
}
|
||||
}
|
||||
|
||||
func OIDCStatus(a *config.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
oidcConfig, _, _, err := setupOIDCConfig(r.Context(), a)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get OIDC config", "error", err)
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"enabled": false,
|
||||
"provider": "",
|
||||
}
|
||||
|
||||
if err == nil && oidcConfig != nil {
|
||||
providerName, _ := a.SM.Get(r.Context(), settings.KeyOIDCProviderName)
|
||||
response["enabled"] = oidcConfig.Enabled
|
||||
response["provider"] = providerName.String("")
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified helper function that handles both config and OIDC setup
|
||||
func setupOIDCConfig(
|
||||
ctx context.Context,
|
||||
a *config.App,
|
||||
) (*OIDCConfig, *oauth2.Config, *oidc.IDTokenVerifier, error) {
|
||||
config, err := getOIDCConfig(ctx, a)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Create OIDC provider
|
||||
provider, err := oidc.NewProvider(ctx, strings.TrimSuffix(config.IssuerURL, "/"))
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("failed to create OIDC provider: %w", err)
|
||||
}
|
||||
|
||||
// Create OAuth2 config
|
||||
oauth2Config := &oauth2.Config{
|
||||
ClientID: config.ClientID,
|
||||
ClientSecret: config.ClientSecret,
|
||||
RedirectURL: config.RedirectURL,
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: config.Scopes,
|
||||
}
|
||||
|
||||
// For PKCE, don't include client secret
|
||||
if config.UsePKCE {
|
||||
oauth2Config.ClientSecret = ""
|
||||
}
|
||||
|
||||
// Create ID token verifier
|
||||
verifier := provider.Verifier(&oidc.Config{
|
||||
ClientID: config.ClientID,
|
||||
})
|
||||
|
||||
return config, oauth2Config, verifier, nil
|
||||
}
|
||||
|
||||
func getOIDCConfig(ctx context.Context, a *config.App) (*OIDCConfig, error) {
|
||||
config := &OIDCConfig{Scopes: []string{"openid", "email", "profile"}}
|
||||
|
||||
sets, err := a.SM.GetAll(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get settings: %w", err)
|
||||
}
|
||||
|
||||
// Parse settings (same as before but simplified validation)
|
||||
if enabled, exists := sets[settings.KeyOIDCEnabled]; exists {
|
||||
config.Enabled = enabled.Bool(false)
|
||||
}
|
||||
if !config.Enabled {
|
||||
return config, nil // Return early if disabled
|
||||
}
|
||||
|
||||
if pkce, exists := sets[settings.KeyOIDCPKCE]; exists {
|
||||
config.UsePKCE = pkce.Bool(false)
|
||||
}
|
||||
|
||||
if clientID, exists := sets[settings.KeyOIDCClientID]; exists {
|
||||
config.ClientID = clientID.String("")
|
||||
}
|
||||
|
||||
if !config.UsePKCE {
|
||||
if clientSecret, exists := sets[settings.KeyOIDCClientSecret]; exists {
|
||||
config.ClientSecret = clientSecret.String("")
|
||||
}
|
||||
}
|
||||
|
||||
if serverURL, exists := sets[settings.KeyServerURL]; exists {
|
||||
if parsed, err := url.Parse(serverURL.String("")); err == nil {
|
||||
config.RedirectURL = strings.TrimSuffix(parsed.String(), "/") + "/api/oidc/callback"
|
||||
}
|
||||
}
|
||||
|
||||
if issuerURL, exists := sets[settings.KeyOIDCIssuerURL]; exists {
|
||||
config.IssuerURL = issuerURL.String("")
|
||||
}
|
||||
|
||||
if scopes, exists := sets["oauth_scopes"]; exists && scopes.String("") != "" {
|
||||
config.Scopes = strings.Split(scopes.String(""), ",")
|
||||
for i := range config.Scopes {
|
||||
config.Scopes[i] = strings.TrimSpace(config.Scopes[i])
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func findOrCreateOIDCUser(
|
||||
ctx context.Context,
|
||||
q *db.Queries,
|
||||
userInfo *OIDCUserInfo,
|
||||
) (*db.User, error) {
|
||||
var user *db.User
|
||||
|
||||
// Try to find existing user by email or username
|
||||
if userInfo.Email != "" {
|
||||
// First try to find by email
|
||||
if existingUser, emailErr := q.GetUserByEmail(ctx, &userInfo.Email); emailErr == nil {
|
||||
user = &db.User{
|
||||
ID: existingUser.ID,
|
||||
Username: existingUser.Username,
|
||||
Email: existingUser.Email,
|
||||
IsAdmin: existingUser.IsAdmin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil && userInfo.PreferredUsername != "" {
|
||||
// Try to find by username
|
||||
if existingUser, usernameErr := q.GetUserByUsername(ctx, userInfo.PreferredUsername); usernameErr == nil {
|
||||
user = &db.User{
|
||||
ID: existingUser.ID,
|
||||
Username: existingUser.Username,
|
||||
Email: existingUser.Email,
|
||||
IsAdmin: existingUser.IsAdmin,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
// Create new user
|
||||
username := userInfo.PreferredUsername
|
||||
if username == "" {
|
||||
// Fallback to email prefix if no preferred username
|
||||
if userInfo.Email != "" {
|
||||
username = strings.Split(userInfo.Email, "@")[0]
|
||||
} else {
|
||||
username = fmt.Sprintf("oidc_user_%s", userInfo.Sub)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure username is unique
|
||||
originalUsername := username
|
||||
counter := 1
|
||||
for {
|
||||
if _, err := q.GetUserByUsername(ctx, username); err != nil {
|
||||
break // Username is available
|
||||
}
|
||||
username = fmt.Sprintf("%s_%d", originalUsername, counter)
|
||||
counter++
|
||||
}
|
||||
|
||||
params := db.CreateUserParams{
|
||||
Username: username,
|
||||
Email: &userInfo.Email,
|
||||
IsAdmin: false,
|
||||
}
|
||||
|
||||
newUserID, err := q.CreateUser(ctx, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create OIDC user: %w", err)
|
||||
}
|
||||
user = &db.User{
|
||||
ID: newUserID,
|
||||
Username: username,
|
||||
Email: &userInfo.Email,
|
||||
IsAdmin: false,
|
||||
}
|
||||
} else {
|
||||
// Update existing user's email if needed and not set
|
||||
if user.Email == nil && userInfo.Email != "" {
|
||||
if err := q.UpdateUser(ctx, db.UpdateUserParams{
|
||||
Username: user.Username,
|
||||
Email: &userInfo.Email,
|
||||
IsAdmin: user.IsAdmin,
|
||||
ID: user.ID,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to update user email: %w", err)
|
||||
}
|
||||
user.Email = &userInfo.Email
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func generateRandomState() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func CreateUser(a *config.App) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
user.Password = hash
|
||||
if err := q.CreateUser(r.Context(), user); err != nil {
|
||||
if _, err := q.CreateUser(r.Context(), user); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -60,24 +60,29 @@ func (h *MiddlewareHandler) BasicAuth(next http.Handler) http.Handler {
|
||||
// JWT authentication middleware
|
||||
func (h *MiddlewareHandler) JWT(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
http.Error(w, "Authorization header required", http.StatusUnauthorized)
|
||||
cookie, err := r.Cookie(util.CookieName)
|
||||
if err != nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
if tokenString == authHeader {
|
||||
http.Error(w, "Invalid authorization format", http.StatusUnauthorized)
|
||||
token := cookie.Value
|
||||
if token == "" {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := util.DecodeUserJWT(tokenString, h.app.Config.Secret)
|
||||
claims, err := util.DecodeUserJWT(token, h.app.Config.Secret)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if claims.Username == "" {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify user exists in database
|
||||
q := h.app.Conn.GetQuery()
|
||||
user, err := q.GetUserByUsername(r.Context(), claims.Username)
|
||||
|
||||
@@ -2,15 +2,40 @@ package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
const (
|
||||
defaultDevOrigin = "http://127.0.0.1:5173"
|
||||
)
|
||||
|
||||
func CORS(allowedOrigins ...string) func(http.Handler) http.Handler {
|
||||
// Default to dev origin if none provided
|
||||
if len(allowedOrigins) == 0 {
|
||||
allowedOrigins = []string{defaultDevOrigin}
|
||||
}
|
||||
|
||||
// Create a map for faster origin lookup
|
||||
originMap := make(map[string]bool)
|
||||
for _, origin := range allowedOrigins {
|
||||
originMap[strings.TrimSpace(origin)] = true
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
|
||||
// Set CORS headers
|
||||
if origin != "" && originMap[origin] {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().
|
||||
Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||
|
||||
// Handle preflight requests
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
@@ -18,4 +43,5 @@ func CORS(next http.Handler) http.Handler {
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,13 @@ func (s *Server) routes() {
|
||||
|
||||
// Auth
|
||||
register("POST", "/login", logChain, handler.Login(s.app))
|
||||
register("POST", "/verify", logChain, handler.VerifyJWT(s.app))
|
||||
register("POST", "/logout", logChain, handler.Logout)
|
||||
register("GET", "/verify", jwtChain, handler.Verify)
|
||||
register("POST", "/verify/otp", logChain, handler.VerifyOTP(s.app))
|
||||
register("POST", "/reset/{name}", logChain, handler.SendResetEmail(s.app))
|
||||
register("GET", "/oidc/callback", logChain, handler.OIDCCallback(s.app))
|
||||
register("GET", "/oidc/login", logChain, handler.OIDCLogin(s.app))
|
||||
register("GET", "/oidc/status", logChain, handler.OIDCStatus(s.app))
|
||||
|
||||
// Events
|
||||
register("GET", "/events", logChain, handler.GetEvents)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
@@ -41,10 +42,11 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
defer s.app.Conn.Close()
|
||||
host := s.app.Config.Server.Host
|
||||
port := s.app.Config.Server.Port
|
||||
allowedOrigins := s.getAllowedOrigins(ctx)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: host + ":" + port,
|
||||
Handler: middlewares.CORS(s.mux),
|
||||
Handler: middlewares.CORS(allowedOrigins...)(s.mux),
|
||||
ReadHeaderTimeout: 3 * time.Second,
|
||||
ReadTimeout: 5 * time.Minute,
|
||||
WriteTimeout: 5 * time.Minute,
|
||||
@@ -79,6 +81,34 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) getAllowedOrigins(ctx context.Context) []string {
|
||||
var origins []string
|
||||
|
||||
// Always allow development frontend
|
||||
devOrigin := "http://127.0.0.1:5173"
|
||||
origins = append(origins, devOrigin)
|
||||
|
||||
// Get server URL from settings for production
|
||||
if serverURL, err := s.app.SM.Get(ctx, "server_url"); err == nil {
|
||||
if url := serverURL.String(""); url != "" && url != devOrigin {
|
||||
origins = append(origins, strings.TrimSuffix(url, "/"))
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
seen := make(map[string]bool)
|
||||
var uniqueOrigins []string
|
||||
for _, origin := range origins {
|
||||
if !seen[origin] {
|
||||
uniqueOrigins = append(uniqueOrigins, origin)
|
||||
seen[origin] = true
|
||||
}
|
||||
}
|
||||
|
||||
slog.Debug("CORS allowed origins", "origins", uniqueOrigins)
|
||||
return uniqueOrigins
|
||||
}
|
||||
|
||||
func (s *Server) registerServices() {
|
||||
// Common interceptors
|
||||
opts := []connect.HandlerOption{
|
||||
|
||||
@@ -96,7 +96,7 @@ func (a *App) setDefaultAdminUser(ctx context.Context) error {
|
||||
// If admin doesn't exist, create new admin
|
||||
if err != nil {
|
||||
adminMail := "admin@mantrae"
|
||||
if err = q.CreateUser(ctx, db.CreateUserParams{
|
||||
if _, err = q.CreateUser(ctx, db.CreateUserParams{
|
||||
Username: "admin",
|
||||
Email: &adminMail,
|
||||
Password: hash,
|
||||
|
||||
@@ -11,15 +11,16 @@ import (
|
||||
"github.com/MizuchiLabs/mantrae/internal/util"
|
||||
)
|
||||
|
||||
// setupBackgroundJobs initiates essential background operations for the application.
|
||||
func (a *App) setupBackgroundJobs(ctx context.Context) {
|
||||
slog.Info("Starting background tasks...")
|
||||
go a.traefikSync(ctx)
|
||||
go a.syncTraefik(ctx)
|
||||
go a.syncDNS(ctx)
|
||||
go a.cleanupAgents(ctx)
|
||||
}
|
||||
|
||||
// traefikSync periodically syncs the Traefik configuration
|
||||
func (a *App) traefikSync(ctx context.Context) {
|
||||
// syncTraefik periodically syncs the Traefik configuration
|
||||
func (a *App) syncTraefik(ctx context.Context) {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(a.Config.Background.Traefik))
|
||||
defer ticker.Stop()
|
||||
|
||||
|
||||
@@ -117,6 +117,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
|
||||
if q.getUserStmt, err = db.PrepareContext(ctx, getUser); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query GetUser: %w", err)
|
||||
}
|
||||
if q.getUserByEmailStmt, err = db.PrepareContext(ctx, getUserByEmail); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query GetUserByEmail: %w", err)
|
||||
}
|
||||
if q.getUserByUsernameStmt, err = db.PrepareContext(ctx, getUserByUsername); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query GetUserByUsername: %w", err)
|
||||
}
|
||||
@@ -349,6 +352,11 @@ func (q *Queries) Close() error {
|
||||
err = fmt.Errorf("error closing getUserStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.getUserByEmailStmt != nil {
|
||||
if cerr := q.getUserByEmailStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing getUserByEmailStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.getUserByUsernameStmt != nil {
|
||||
if cerr := q.getUserByUsernameStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing getUserByUsernameStmt: %w", cerr)
|
||||
@@ -539,6 +547,7 @@ type Queries struct {
|
||||
getTraefikConfigByIDStmt *sql.Stmt
|
||||
getTraefikConfigBySourceStmt *sql.Stmt
|
||||
getUserStmt *sql.Stmt
|
||||
getUserByEmailStmt *sql.Stmt
|
||||
getUserByUsernameStmt *sql.Stmt
|
||||
getUserPasswordStmt *sql.Stmt
|
||||
listAgentsStmt *sql.Stmt
|
||||
@@ -600,6 +609,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||
getTraefikConfigByIDStmt: q.getTraefikConfigByIDStmt,
|
||||
getTraefikConfigBySourceStmt: q.getTraefikConfigBySourceStmt,
|
||||
getUserStmt: q.getUserStmt,
|
||||
getUserByEmailStmt: q.getUserByEmailStmt,
|
||||
getUserByUsernameStmt: q.getUserByUsernameStmt,
|
||||
getUserPasswordStmt: q.getUserPasswordStmt,
|
||||
listAgentsStmt: q.listAgentsStmt,
|
||||
|
||||
@@ -13,7 +13,7 @@ type Querier interface {
|
||||
CreateAgent(ctx context.Context, arg CreateAgentParams) error
|
||||
CreateDNSProvider(ctx context.Context, arg CreateDNSProviderParams) error
|
||||
CreateProfile(ctx context.Context, arg CreateProfileParams) (int64, error)
|
||||
CreateUser(ctx context.Context, arg CreateUserParams) error
|
||||
CreateUser(ctx context.Context, arg CreateUserParams) (int64, error)
|
||||
DeleteAgent(ctx context.Context, id string) error
|
||||
DeleteDNSProvider(ctx context.Context, id int64) error
|
||||
DeleteErrorById(ctx context.Context, id int64) error
|
||||
@@ -40,6 +40,7 @@ type Querier interface {
|
||||
GetTraefikConfigByID(ctx context.Context, id int64) (Traefik, error)
|
||||
GetTraefikConfigBySource(ctx context.Context, arg GetTraefikConfigBySourceParams) ([]Traefik, error)
|
||||
GetUser(ctx context.Context, id int64) (GetUserRow, error)
|
||||
GetUserByEmail(ctx context.Context, email *string) (GetUserByEmailRow, error)
|
||||
GetUserByUsername(ctx context.Context, username string) (GetUserByUsernameRow, error)
|
||||
GetUserPassword(ctx context.Context, id int64) (string, error)
|
||||
ListAgents(ctx context.Context) ([]Agent, error)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
-- name: CreateUser :exec
|
||||
-- name: CreateUser :one
|
||||
INSERT INTO
|
||||
users (username, password, email, is_admin)
|
||||
VALUES
|
||||
(?, ?, ?, ?);
|
||||
(?, ?, ?, ?) RETURNING id;
|
||||
|
||||
-- name: GetUser :one
|
||||
SELECT
|
||||
@@ -36,6 +36,22 @@ FROM
|
||||
WHERE
|
||||
username = ?;
|
||||
|
||||
-- name: GetUserByEmail :one
|
||||
SELECT
|
||||
id,
|
||||
username,
|
||||
email,
|
||||
is_admin,
|
||||
otp,
|
||||
otp_expiry,
|
||||
last_login,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
email = ?;
|
||||
|
||||
-- name: GetUserPassword :one
|
||||
SELECT
|
||||
password
|
||||
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const createUser = `-- name: CreateUser :exec
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO
|
||||
users (username, password, email, is_admin)
|
||||
VALUES
|
||||
(?, ?, ?, ?)
|
||||
(?, ?, ?, ?) RETURNING id
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
@@ -24,14 +24,16 @@ type CreateUserParams struct {
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) error {
|
||||
_, err := q.exec(ctx, q.createUserStmt, createUser,
|
||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (int64, error) {
|
||||
row := q.queryRow(ctx, q.createUserStmt, createUser,
|
||||
arg.Username,
|
||||
arg.Password,
|
||||
arg.Email,
|
||||
arg.IsAdmin,
|
||||
)
|
||||
return err
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const deleteUser = `-- name: DeleteUser :exec
|
||||
@@ -91,6 +93,52 @@ func (q *Queries) GetUser(ctx context.Context, id int64) (GetUserRow, error) {
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT
|
||||
id,
|
||||
username,
|
||||
email,
|
||||
is_admin,
|
||||
otp,
|
||||
otp_expiry,
|
||||
last_login,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
email = ?
|
||||
`
|
||||
|
||||
type GetUserByEmailRow struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email *string `json:"email"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
Otp *string `json:"otp"`
|
||||
OtpExpiry *time.Time `json:"otpExpiry"`
|
||||
LastLogin *time.Time `json:"lastLogin"`
|
||||
CreatedAt *time.Time `json:"createdAt"`
|
||||
UpdatedAt *time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserByEmail(ctx context.Context, email *string) (GetUserByEmailRow, error) {
|
||||
row := q.queryRow(ctx, q.getUserByEmailStmt, getUserByEmail, email)
|
||||
var i GetUserByEmailRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Email,
|
||||
&i.IsAdmin,
|
||||
&i.Otp,
|
||||
&i.OtpExpiry,
|
||||
&i.LastLogin,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByUsername = `-- name: GetUserByUsername :one
|
||||
SELECT
|
||||
id,
|
||||
|
||||
@@ -27,6 +27,15 @@ const (
|
||||
KeyEmailPassword = "email_password"
|
||||
KeyEmailFrom = "email_from"
|
||||
|
||||
// OAuth settings
|
||||
KeyOIDCEnabled = "oidc_enabled"
|
||||
KeyOIDCClientID = "oidc_client_id"
|
||||
KeyOIDCClientSecret = "oidc_client_secret"
|
||||
KeyOIDCProviderName = "oidc_provider_name"
|
||||
KeyOIDCIssuerURL = "oidc_issuer_url"
|
||||
KeyOIDCScopes = "oidc_scopes"
|
||||
KeyOIDCPKCE = "oidc_pkce"
|
||||
|
||||
// Agent settings
|
||||
KeyAgentCleanupEnabled = "agent_cleanup_enabled"
|
||||
KeyAgentCleanupInterval = "agent_cleanup_interval"
|
||||
|
||||
@@ -18,7 +18,7 @@ type SettingWithDescription struct {
|
||||
|
||||
// Settings defines all application settings
|
||||
type Settings struct {
|
||||
ServerURL string `setting:"server_url" default:"http://localhost:3000" description:"Base URL for the server"`
|
||||
ServerURL string `setting:"server_url" default:"http://127.0.0.1:3000" description:"Base URL for the server"`
|
||||
BackupEnabled bool `setting:"backup_enabled" default:"true" description:"Enable scheduled backups"`
|
||||
BackupInterval time.Duration `setting:"backup_interval" default:"24h" description:"Time between each backup"`
|
||||
BackupKeep int `setting:"backup_keep" default:"3" description:"How many backups to keep"`
|
||||
@@ -35,6 +35,13 @@ type Settings struct {
|
||||
EmailUser string `setting:"email_user" default:"" description:"SMTP login username"`
|
||||
EmailPassword string `setting:"email_password" default:"" description:"SMTP login password"`
|
||||
EmailFrom string `setting:"email_from" default:"mantrae@localhost" description:"Default sender email address"`
|
||||
OIDCEnabled bool `setting:"oidc_enabled" default:"false" description:"Enable OAuth authentication"`
|
||||
OIDCClientID string `setting:"oidc_client_id" default:"" description:"OIDC client ID"`
|
||||
OIDCClientSecret string `setting:"oidc_client_secret" default:"" description:"OIDC client secret"`
|
||||
OIDCIssuerURL string `setting:"oidc_issuer_url" default:"" description:"OIDC issuer URL"`
|
||||
OIDCProviderName string `setting:"oidc_provider_name" default:"" description:"Display name of the OIDC provider shown on the login button (e.g., 'Google', 'Keycloak')"`
|
||||
OIDCScopes string `setting:"oidc_scopes" default:"" description:"OIDC scopes (comma-separated)"`
|
||||
OIDCPKCE bool `setting:"oidc_pkce" default:"false" description:"Enable PKCE for OAuth"`
|
||||
AgentCleanupEnabled bool `setting:"agent_cleanup_enabled" default:"true" description:"Enable automatic cleanup of agents"`
|
||||
AgentCleanupInterval time.Duration `setting:"agent_cleanup_interval" default:"24h" description:"Maximum duration an agent can remain offline before being removed. Also used as the expiration duration for agent tokens."`
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func UpdateTraefikAPI(DB *sql.DB, profile db.Profile) error {
|
||||
|
||||
// Clear api data
|
||||
if err = ClearTraefikAPI(DB, profile.ID); err != nil {
|
||||
slog.Error("Failed to update api data", "error", err)
|
||||
slog.Error("Failed to clear API data", "error", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func ClearTraefikAPI(DB *sql.DB, profileID int64) error {
|
||||
Version: nil,
|
||||
Config: nil,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to update api data: %w", err)
|
||||
return fmt.Errorf("Failed to clear API data: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
const CookieName = "auth_token"
|
||||
|
||||
type UserClaims struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
|
||||
@@ -11,39 +11,39 @@
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@eslint/js": "^9.28.0",
|
||||
"@lucide/svelte": "^0.488.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.21.0",
|
||||
"@sveltejs/kit": "^2.21.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/postcss": "^4.1.6",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/node": "^22.15.17",
|
||||
"@types/node": "^22.15.29",
|
||||
"bits-ui": "1.0.0-next.98",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-svelte": "^3.6.0",
|
||||
"eslint-plugin-svelte": "^3.9.0",
|
||||
"formsnap": "^2.0.1",
|
||||
"globals": "^16.1.0",
|
||||
"globals": "^16.2.0",
|
||||
"mode-watcher": "^0.5.1",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.28.6",
|
||||
"svelte-check": "^4.1.7",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.12",
|
||||
"svelte": "^5.33.11",
|
||||
"svelte-check": "^4.2.1",
|
||||
"svelte-highlight": "^7.8.3",
|
||||
"svelte-sonner": "^0.3.28",
|
||||
"sveltekit-superforms": "^2.25.0",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "^4.1.6",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.32.1",
|
||||
"typescript-eslint": "^8.33.0",
|
||||
"vite": "^6.3.5",
|
||||
"yaml": "^2.7.1",
|
||||
"zod": "^3.24.4"
|
||||
"yaml": "^2.8.0",
|
||||
"zod": "^3.25.46"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
1788
web/pnpm-lock.yaml
generated
1788
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@ import { goto } from '$app/navigation';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
import YAML from 'yaml';
|
||||
import { token } from './stores/common';
|
||||
import { profile } from './stores/profile';
|
||||
import { source } from './stores/source';
|
||||
import { user } from './stores/user';
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
type Agent,
|
||||
type BackupFile,
|
||||
type DNSProvider,
|
||||
type OAuthStatus,
|
||||
type Plugin,
|
||||
type Profile,
|
||||
type PublicIP,
|
||||
@@ -87,9 +87,7 @@ async function send(endpoint: string, options: APIOptions = {}, fetch?: typeof w
|
||||
// Custom fetch function that adds the Authorization header
|
||||
const customFetch: typeof window.fetch = async (url, options) => {
|
||||
const headers = new Headers(options?.headers); // Get existing headers
|
||||
if (token.value) {
|
||||
headers.set('Authorization', 'Bearer ' + token.value); // Add the Authorization header
|
||||
}
|
||||
|
||||
// Don't set Content-Type for FormData
|
||||
const isFormData = options?.body instanceof FormData;
|
||||
if (!isFormData) {
|
||||
@@ -98,6 +96,7 @@ async function send(endpoint: string, options: APIOptions = {}, fetch?: typeof w
|
||||
const customOptions = {
|
||||
...options,
|
||||
headers,
|
||||
credentials: 'include' as RequestCredentials, // Include cookies
|
||||
body: isFormData ? options?.body : options?.body ? JSON.stringify(options.body) : undefined
|
||||
};
|
||||
return fetch ? fetch(url, customOptions) : window.fetch(url, customOptions); // Use custom fetch or default
|
||||
@@ -108,7 +107,8 @@ async function send(endpoint: string, options: APIOptions = {}, fetch?: typeof w
|
||||
const response = await customFetch(`${BASE_URL}${endpoint}`, {
|
||||
method: options.method || 'GET',
|
||||
body: options.body,
|
||||
headers: options.headers
|
||||
headers: options.headers,
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -135,32 +135,21 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: { username, password }
|
||||
});
|
||||
if (data.token) {
|
||||
token.value = data.token;
|
||||
if (data.user) {
|
||||
user.value = data.user;
|
||||
await api.load();
|
||||
goto('/');
|
||||
}
|
||||
goto('/');
|
||||
},
|
||||
|
||||
async verify(fetch: typeof window.fetch = window.fetch) {
|
||||
try {
|
||||
const data = await send(
|
||||
'/verify',
|
||||
{
|
||||
method: 'POST',
|
||||
body: token.value
|
||||
},
|
||||
fetch
|
||||
);
|
||||
const data = await send('/verify', {}, fetch);
|
||||
if (data.user) {
|
||||
user.value = data.user;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const error = err instanceof Error ? err.message : String(err);
|
||||
toast.error('Session expired', { description: error });
|
||||
api.logout();
|
||||
return;
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -174,12 +163,18 @@ export const api = {
|
||||
body: { username, token: otp }
|
||||
});
|
||||
|
||||
if (data.token) {
|
||||
token.value = data.token;
|
||||
if (data.user) {
|
||||
user.value = data.user;
|
||||
await api.load();
|
||||
goto('/');
|
||||
}
|
||||
goto('/');
|
||||
},
|
||||
|
||||
async oauthStatus() {
|
||||
const data: OAuthStatus = await send('/oidc/status');
|
||||
if (!data) {
|
||||
throw new Error('Failed to fetch OAuth status');
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
async load() {
|
||||
@@ -197,8 +192,8 @@ export const api = {
|
||||
}
|
||||
},
|
||||
|
||||
logout() {
|
||||
token.value = null;
|
||||
async logout() {
|
||||
await send('/logout', { method: 'POST' });
|
||||
user.clear();
|
||||
goto('/login');
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createLocalStorage } from '$lib/storage.svelte';
|
||||
|
||||
export const token = createLocalStorage('auth_token', null);
|
||||
export const limit = createLocalStorage('limit', '10');
|
||||
export const routerColumns = createLocalStorage('router_columns', []);
|
||||
export const middlewareColumns = createLocalStorage('middleware_columns', []);
|
||||
|
||||
@@ -139,6 +139,11 @@ export interface Plugin {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface OAuthStatus {
|
||||
enabled: boolean;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export interface BackupFile {
|
||||
name: string;
|
||||
size: number;
|
||||
|
||||
@@ -1,48 +1,46 @@
|
||||
import type { LayoutLoad } from './$types';
|
||||
import { api } from '$lib/api';
|
||||
import { goto } from '$app/navigation';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { token } from '$lib/stores/common';
|
||||
|
||||
export const ssr = false;
|
||||
export const prerender = true;
|
||||
export const trailingSlash = 'always';
|
||||
|
||||
const isPublicRoute = (path: string) => path.startsWith('/login/');
|
||||
const isPublicRoute = (path: string) => {
|
||||
return path.startsWith('/login') || path === '/login';
|
||||
};
|
||||
|
||||
export const load: LayoutLoad = async ({ url, fetch }) => {
|
||||
// Case 1: No token and accessing protected route
|
||||
if (!token.value && !isPublicRoute(url.pathname)) {
|
||||
await goto('/login/');
|
||||
user.clear();
|
||||
return;
|
||||
}
|
||||
const currentPath = url.pathname;
|
||||
const isPublic = isPublicRoute(currentPath);
|
||||
|
||||
// Case 2: Has token, verify it
|
||||
if (token.value) {
|
||||
// Try to verify authentication via cookie
|
||||
try {
|
||||
await api.verify(fetch);
|
||||
const isVerified = await api.verify(fetch);
|
||||
|
||||
// Trying to access public route
|
||||
if (isPublicRoute(url.pathname)) {
|
||||
if (isVerified) {
|
||||
// User is authenticated
|
||||
if (isPublic) {
|
||||
// Authenticated user trying to access login page - redirect to home
|
||||
await goto('/');
|
||||
}
|
||||
return;
|
||||
} catch (err: unknown) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
// Token verification failed
|
||||
api.logout();
|
||||
if (!isPublicRoute(url.pathname)) {
|
||||
}
|
||||
// Continue to protected route
|
||||
return;
|
||||
} else {
|
||||
// Verification failed but no exception thrown
|
||||
throw new Error('Authentication failed');
|
||||
}
|
||||
} catch (_) {
|
||||
// Authentication failed
|
||||
user.clear();
|
||||
|
||||
if (!isPublic) {
|
||||
// User trying to access protected route without auth - redirect to login
|
||||
await goto('/login');
|
||||
}
|
||||
user.clear();
|
||||
toast.error('Session expired', { description: error.message });
|
||||
// If already on public route, stay there
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: No token and accessing public route
|
||||
user.clear();
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -10,10 +10,13 @@
|
||||
import Separator from '$lib/components/ui/separator/separator.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { user } from '$lib/stores/user';
|
||||
import type { OAuthStatus } from '$lib/types';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let username = $state('');
|
||||
let password = $state('');
|
||||
let remember = $state(false);
|
||||
let oauthStatus: OAuthStatus = $state({ enabled: false, provider: '' });
|
||||
const handleReset = async () => {
|
||||
if (username.length > 0) {
|
||||
const resetPromise = api
|
||||
@@ -51,6 +54,12 @@
|
||||
error: (error) => (error as Error).message
|
||||
});
|
||||
};
|
||||
const handleOIDCLogin = () => {
|
||||
window.location.href = '/api/oidc/login';
|
||||
};
|
||||
onMount(async () => {
|
||||
oauthStatus = await api.oauthStatus();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !user.isLoggedIn()}
|
||||
@@ -85,6 +94,12 @@
|
||||
<Separator />
|
||||
|
||||
<Button type="submit" class="w-full" disabled={$loading}>Login</Button>
|
||||
|
||||
{#if oauthStatus.enabled}
|
||||
<Button variant="outline" class="w-full" onclick={handleOIDCLogin}>
|
||||
Login with {oauthStatus.provider || 'OIDC'}
|
||||
</Button>
|
||||
{/if}
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
@@ -34,40 +34,62 @@
|
||||
general: {
|
||||
title: 'General Settings',
|
||||
description: 'Basic application configuration',
|
||||
keys: ['server_url']
|
||||
keys: [{ key: 'server_url', label: 'Server URL' }]
|
||||
},
|
||||
backup: {
|
||||
title: 'Backup Settings',
|
||||
description: 'Database backup configuration',
|
||||
keys: [
|
||||
'backup_enabled',
|
||||
'backup_interval',
|
||||
'backup_keep',
|
||||
'backup_path',
|
||||
'backup_storage_select'
|
||||
{ key: 'backup_enabled', label: 'Enable Backups' },
|
||||
{ key: 'backup_interval', label: 'Backup Interval' },
|
||||
{ key: 'backup_keep', label: 'Number of Backups to Keep' },
|
||||
{ key: 'backup_path', label: 'Backup Path' },
|
||||
{ key: 'backup_storage_select', label: 'Backup Storage Type' }
|
||||
]
|
||||
},
|
||||
s3: {
|
||||
title: 'S3 Storage Settings',
|
||||
description: 'Amazon S3 or compatible storage configuration',
|
||||
keys: [
|
||||
's3_endpoint',
|
||||
's3_bucket',
|
||||
's3_region',
|
||||
's3_access_key',
|
||||
's3_secret_key',
|
||||
's3_use_path_style'
|
||||
{ key: 's3_endpoint', label: 'Endpoint' },
|
||||
{ key: 's3_bucket', label: 'Bucket Name' },
|
||||
{ key: 's3_region', label: 'Region' },
|
||||
{ key: 's3_access_key', label: 'Access Key' },
|
||||
{ key: 's3_secret_key', label: 'Secret Key' },
|
||||
{ key: 's3_use_path_style', label: 'Use Path Style' }
|
||||
]
|
||||
},
|
||||
email: {
|
||||
title: 'Email Settings',
|
||||
description: 'SMTP server configuration for sending emails',
|
||||
keys: ['email_host', 'email_port', 'email_user', 'email_password', 'email_from']
|
||||
keys: [
|
||||
{ key: 'email_host', label: 'Host' },
|
||||
{ key: 'email_port', label: 'Port' },
|
||||
{ key: 'email_user', label: 'Username' },
|
||||
{ key: 'email_password', label: 'Password' },
|
||||
{ key: 'email_from', label: 'From Email Address' }
|
||||
]
|
||||
},
|
||||
oauth: {
|
||||
title: 'OIDC Settings',
|
||||
description: 'OIDC provider configuration',
|
||||
keys: [
|
||||
{ key: 'oidc_enabled', label: 'Enable OIDC' },
|
||||
{ key: 'oidc_client_id', label: 'Client ID' },
|
||||
{ key: 'oidc_client_secret', label: 'Client Secret' },
|
||||
{ key: 'oidc_issuer_url', label: 'Issuer URL' },
|
||||
{ key: 'oidc_provider_name', label: 'Provider Name' },
|
||||
{ key: 'oidc_scopes', label: 'Scopes' },
|
||||
{ key: 'oidc_pkce', label: 'Use PKCE' }
|
||||
]
|
||||
},
|
||||
agents: {
|
||||
title: 'Agent Settings',
|
||||
description: 'Agent management configuration',
|
||||
keys: ['agent_cleanup_enabled', 'agent_cleanup_interval']
|
||||
keys: [
|
||||
{ key: 'agent_cleanup_enabled', label: 'Enable Cleanup' },
|
||||
{ key: 'agent_cleanup_interval', label: 'Cleanup Interval' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -96,14 +118,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to convert camelCase/snake_case to Title Case
|
||||
const formatSettingName = (key: string) => {
|
||||
return key
|
||||
.split(/[_\s]/)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
// Determine the input type based on the setting value
|
||||
const getInputType = (key: string, value: Setting['value']) => {
|
||||
if (typeof value === 'boolean') return 'boolean';
|
||||
@@ -311,12 +325,12 @@
|
||||
<Separator class="mb-4" />
|
||||
|
||||
<!-- Loop through settings in this group -->
|
||||
{#each group.keys as key (key)}
|
||||
{#each group.keys as { key, label } (key)}
|
||||
{#if $settings[key]}
|
||||
<div class="mb-4 flex flex-col justify-start gap-4 sm:flex-row sm:justify-between">
|
||||
<div class="border-muted-foreground border-l-2 pl-4">
|
||||
<Label>
|
||||
{formatSettingName(key)}
|
||||
{label}
|
||||
{#if $settings[key].description}
|
||||
<p class="text-muted-foreground text-sm">{$settings[key].description}</p>
|
||||
{/if}
|
||||
@@ -337,12 +351,14 @@
|
||||
onValueChange={(value) => handleChange(key, value)}
|
||||
>
|
||||
<Select.Trigger>
|
||||
{changedValues[key] || $settings[key].value || 'Select storage type'}
|
||||
{changedValues[key] || $settings[key].value || 'Select...'}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#if key === 'backup_storage_select'}
|
||||
{#each storageTypes as option (option.value)}
|
||||
<Select.Item value={option.value}>{option.label}</Select.Item>
|
||||
{/each}
|
||||
{/if}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{:else if getInputType(key, $settings[key].value) === 'password'}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
define: {
|
||||
'process.env': process.env
|
||||
define: {},
|
||||
server: {
|
||||
host: '127.0.0.1'
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user