浏览代码

WebAdmin: allow to disable 2FA

Before it was only possible using REST API

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 1 年之前
父节点
当前提交
a577d8b3cd

+ 15 - 15
go.mod

@@ -10,14 +10,14 @@ require (
 	github.com/alexedwards/argon2id v1.0.0
 	github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
 	github.com/aws/aws-sdk-go-v2 v1.25.1
-	github.com/aws/aws-sdk-go-v2/config v1.27.2
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.2
+	github.com/aws/aws-sdk-go-v2/config v1.27.3
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.3
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1
-	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.4
-	github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.3
-	github.com/aws/aws-sdk-go-v2/service/s3 v1.50.3
-	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.3
-	github.com/aws/aws-sdk-go-v2/service/sts v1.27.2
+	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.5
+	github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.0
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.51.0
+	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.0
+	github.com/aws/aws-sdk-go-v2/service/sts v1.28.0
 	github.com/bmatcuk/doublestar/v4 v4.6.1
 	github.com/cockroachdb/cockroach-go/v2 v2.3.6
 	github.com/coreos/go-oidc/v3 v3.9.0
@@ -74,7 +74,7 @@ require (
 	golang.org/x/sys v0.17.0
 	golang.org/x/term v0.17.0
 	golang.org/x/time v0.5.0
-	google.golang.org/api v0.166.0
+	google.golang.org/api v0.167.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 )
 
@@ -94,8 +94,8 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.19.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.20.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.0 // indirect
 	github.com/aws/smithy-go v1.20.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/boombuler/barcode v1.0.1 // indirect
@@ -146,7 +146,7 @@ require (
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
 	github.com/prometheus/client_model v0.6.0 // indirect
-	github.com/prometheus/common v0.47.0 // indirect
+	github.com/prometheus/common v0.48.0 // indirect
 	github.com/prometheus/procfs v0.12.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/sagikazarmark/locafero v0.4.0 // indirect
@@ -162,11 +162,11 @@ require (
 	go.opencensus.io v0.24.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 // indirect
-	go.opentelemetry.io/otel v1.23.1 // indirect
-	go.opentelemetry.io/otel/metric v1.23.1 // indirect
-	go.opentelemetry.io/otel/trace v1.23.1 // indirect
+	go.opentelemetry.io/otel v1.24.0 // indirect
+	go.opentelemetry.io/otel/metric v1.24.0 // indirect
+	go.opentelemetry.io/otel/trace v1.24.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
+	golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
 	golang.org/x/mod v0.15.0 // indirect
 	golang.org/x/sync v0.6.0 // indirect
 	golang.org/x/text v0.14.0 // indirect

+ 30 - 30
go.sum

@@ -37,14 +37,14 @@ github.com/aws/aws-sdk-go-v2 v1.25.1 h1:P7hU6A5qEdmajGwvae/zDkOq+ULLC9tQBTwqqiwF
 github.com/aws/aws-sdk-go-v2 v1.25.1/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 h1:gTK2uhtAPtFcdRRJilZPx8uJLL2J85xK11nKtWL0wfU=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1/go.mod h1:sxpLb+nZk7tIfCWChfd+h4QwHNUR57d8hA1cleTkjJo=
-github.com/aws/aws-sdk-go-v2/config v1.27.2 h1:XnMKB9JRjfnxg9ZkUic4MiapnWJISWRo8HVM+7nx9qQ=
-github.com/aws/aws-sdk-go-v2/config v1.27.2/go.mod h1:z/XIktFoVIKNEqX/811vx4eHetrC3tAkgJKL1ZY/KM4=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.2 h1:tCZXWtH0HiIEZ50NJ7/QEaXmuzEd36L+2JUiZkp2nsc=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.2/go.mod h1:7Zo+D6q4auSIo3p4EItuTKTk7J+RqjASISZqLvmUgpc=
+github.com/aws/aws-sdk-go-v2/config v1.27.3 h1:0PRdb/q5a77HVYj+2rvPiCObfMfl/pWhwa5cs3cnl3c=
+github.com/aws/aws-sdk-go-v2/config v1.27.3/go.mod h1:WeRAr9ENap9NAegbfNsLqGQd8ERz5ypdIUx4j0/ZgKI=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.3 h1:dDM5wrgwOL5gTZ0Gv/bvewPldjBcJywoaO5ClERrOGE=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.3/go.mod h1:G96Nuaw9qJS+s3OnK8RW8VEKEOjXi8H5Jk4lC/ZyZbw=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1 h1:lk1ZZFbdb24qpOwVC1AwYNrswUjAxeyey6kFBVANudQ=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1/go.mod h1:/xJ6x1NehNGCX4tvGzzj2bq5TBOT/Yxq+qbL9Jpx2Vk=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.4 h1:yuhSpqtahkrC8kRCU5v4gEaTDy/ccTIPIkufIRF7YTk=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.4/go.mod h1:q3SxgP2WD9YRLCybtyse8EgO3vKKWVmxlTmBNeRXPyk=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.5 h1:IEv6homMJMnedG/2VWfNuV34ouXUmK8E7y4rAl59Fhs=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.5/go.mod h1:a+wq9mSuG13iSkVMR1O8VApmAISm1ca+E2RQpcB3flw=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1 h1:evvi7FbTAoFxdP/mixmP7LIYzQWAmzBcwNB/es9XPNc=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1/go.mod h1:rH61DT6FDdikhPghymripNUCsf+uVF4Cnk4c4DBKH64=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1 h1:RAnaIrbxPtlXNVI/OIlh1sidTQ3e1qM6LRjs7N0bE0I=
@@ -61,18 +61,18 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1 h1:cVP8mng1R
 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1/go.mod h1:C8sQjoyAsdfjC7hpy4+S6B92hnFzx0d0UAyHicaOTIE=
 github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1 h1:OYmmIcyw19f7x0qLBLQ3XsrCZSSyLhxd9GXng5evsN4=
 github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1/go.mod h1:s5rqdn74Vdg10k61Pwf4ZHEApOSD6CKRe6qpeHDq32I=
-github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.3 h1:uOHZ8HCjUHrRUi+sezA1yCeJVwa4Yy91tZDrWn1sT8w=
-github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.3/go.mod h1:TzgisXFoXgCssgP11SzC6KrvcyCErz5c3w++m3xFOfo=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.50.3 h1:Cv/HH7sLzEdJMYQi4MCNHxZeyubQNOOIdVc0VU0lo3Q=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.50.3/go.mod h1:lTW7O4iMAnO2o7H3XJTvqaWFZCH6zIPs+eP7RdG/yp0=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.3 h1:LP38dk6XSNKyWAr3ZNEVECBPjEnoP+/SGvOfX0tRy+U=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.3/go.mod h1:RA3ERghFSivbTf0Sbsxv/grUuLMcyAjm0F/PylJMmEs=
-github.com/aws/aws-sdk-go-v2/service/sso v1.19.2 h1:pnj8llQoBAHD4UmbM8UM5GdfycFJKMhgPSeaOyRaZ34=
-github.com/aws/aws-sdk-go-v2/service/sso v1.19.2/go.mod h1:x6/tCd1o/AOKQR+iYnjrzhJxD+w0xRN34asGPaSV7ew=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.2 h1:L4yhKxW6HbTSQ08OsvPJuaspaLE40qMgprgXUNFUiMg=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.2/go.mod h1:lZB123q0SVQ3dfIbEOcGzhQHrwVBcHVReNS9tm20oU4=
-github.com/aws/aws-sdk-go-v2/service/sts v1.27.2 h1:Dr+7r/p20XpN+1U5tVNZfA2bLq0kQ9IjVBM0iAyMMLg=
-github.com/aws/aws-sdk-go-v2/service/sts v1.27.2/go.mod h1:ozhhG9/NB5c9jcmhGq6tX9dpp21LYdmRWRQVppASim4=
+github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.0 h1:NEWfVJgrDwEXrKDNrrX+2LtWhKVLYpJ2d7/gr1N1B54=
+github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.0/go.mod h1:TzgisXFoXgCssgP11SzC6KrvcyCErz5c3w++m3xFOfo=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.51.0 h1:rNVsCe3bqTAhG+qjnHJKgYKdHEsqqo/GMK3gEYY8W6g=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.51.0/go.mod h1:lTW7O4iMAnO2o7H3XJTvqaWFZCH6zIPs+eP7RdG/yp0=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.0 h1:Xf3s55N9cqKvFK6D70zCXvXXN4ZovTCy7glL+gUhLEc=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.0/go.mod h1:RA3ERghFSivbTf0Sbsxv/grUuLMcyAjm0F/PylJMmEs=
+github.com/aws/aws-sdk-go-v2/service/sso v1.20.0 h1:6YL8G91QZ52KlPrLkEgEez5kejIVwChVCgND3qgY5j0=
+github.com/aws/aws-sdk-go-v2/service/sso v1.20.0/go.mod h1:x6/tCd1o/AOKQR+iYnjrzhJxD+w0xRN34asGPaSV7ew=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.0 h1:+DqIa5Ll7W311QLUvGFDdVit9uC4G0VioDdw08cXcow=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.0/go.mod h1:lZB123q0SVQ3dfIbEOcGzhQHrwVBcHVReNS9tm20oU4=
+github.com/aws/aws-sdk-go-v2/service/sts v1.28.0 h1:F7tQr61zYnTaeY50Rn4jwfVQbtcqJuBRwN/nGGNwzb0=
+github.com/aws/aws-sdk-go-v2/service/sts v1.28.0/go.mod h1:ozhhG9/NB5c9jcmhGq6tX9dpp21LYdmRWRQVppASim4=
 github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw=
 github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -333,8 +333,8 @@ github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlk
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
 github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
-github.com/prometheus/common v0.47.0 h1:p5Cz0FNHo7SnWOmWmoRozVcjEp0bIVU8cV7OShpjL1k=
-github.com/prometheus/common v0.47.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
+github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
+github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
 github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
 github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -419,14 +419,14 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.48.0/go.mod h1:tIKj3DbO8N9Y2xo52og3irLsPI4GW02DSMtrVgNMgxg=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA=
-go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY=
-go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
-go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo=
-go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
 go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
 go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
-go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8=
-go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
 go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
 go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -434,8 +434,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
 gocloud.dev v0.36.0 h1:q5zoXux4xkOZP473e1EZbG8Gq9f0vlg1VNH5Du/ybus=
 gocloud.dev v0.36.0/go.mod h1:bLxah6JQVKBaIxzsr5BQLYB4IYdWHkMZdzCXlo6F0gg=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
-golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
+golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
+golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -529,8 +529,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
-google.golang.org/api v0.166.0 h1:6m4NUwrZYhAaVIHZWxaKjw1L1vNAjtMwORmKRyEEo24=
-google.golang.org/api v0.166.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA=
+google.golang.org/api v0.167.0 h1:CKHrQD1BLRii6xdkatBDXyKzM0mkawt2QP+H3LtPmSE=
+google.golang.org/api v0.167.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=

+ 2 - 1
internal/dataprovider/admin.go

@@ -58,6 +58,7 @@ const (
 	PermAdminManageEventRules = "manage_event_rules"
 	PermAdminManageRoles      = "manage_roles"
 	PermAdminManageIPLists    = "manage_ip_lists"
+	PermAdminDisableMFA       = "disable_mfa"
 )
 
 const (
@@ -75,7 +76,7 @@ var (
 		PermAdminCloseConnections, PermAdminViewServerStatus, PermAdminManageAdmins, PermAdminManageRoles,
 		PermAdminManageEventRules, PermAdminManageAPIKeys, PermAdminQuotaScans, PermAdminManageSystem,
 		PermAdminManageDefender, PermAdminViewDefender, PermAdminManageIPLists, PermAdminRetentionChecks,
-		PermAdminViewEvents}
+		PermAdminViewEvents, PermAdminDisableMFA}
 	forbiddenPermsForRoleAdmins = []string{PermAdminAny, PermAdminManageAdmins, PermAdminManageSystem,
 		PermAdminManageEventRules, PermAdminManageIPLists, PermAdminManageRoles}
 )

+ 4 - 0
internal/httpd/api_admin.go

@@ -100,6 +100,10 @@ func disableAdmin2FA(w http.ResponseWriter, r *http.Request) {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return
 	}
+	if !admin.Filters.TOTPConfig.Enabled {
+		sendAPIResponse(w, r, nil, "two-factor authentication is not enabled", http.StatusBadRequest)
+		return
+	}
 	if admin.Username == claims.Username {
 		if admin.Filters.RequireTwoFactor {
 			err := util.NewValidationError("two-factor authentication must be enabled")

+ 4 - 0
internal/httpd/api_user.go

@@ -134,6 +134,10 @@ func disableUser2FA(w http.ResponseWriter, r *http.Request) {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
 		return
 	}
+	if !user.Filters.TOTPConfig.Enabled {
+		sendAPIResponse(w, r, nil, "two-factor authentication is not enabled", http.StatusBadRequest)
+		return
+	}
 	user.Filters.RecoveryCodes = nil
 	user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{
 		Enabled: false,

+ 36 - 8
internal/httpd/httpd_test.go

@@ -6812,14 +6812,6 @@ func TestNamingRules(t *testing.T) {
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusBadRequest, rr)
 	assert.Contains(t, rr.Body.String(), "the following characters are allowed")
-
-	req, err = http.NewRequest(http.MethodPut, adminPath+"/"+admin.Username+"/2fa/disable", nil)
-	assert.NoError(t, err)
-	setBearerForReq(req, adminAPIToken)
-	rr = executeRequest(req)
-	checkResponseCode(t, http.StatusBadRequest, rr)
-	assert.Contains(t, rr.Body.String(), "the following characters are allowed")
-
 	// test admin reset password
 	form = make(url.Values)
 	form.Set("username", admin.Username)
@@ -9086,6 +9078,18 @@ func TestAdminTwoFactorLogin(t *testing.T) {
 	admin.Password = altAdminPassword
 	admin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)
 	assert.NoError(t, err)
+	admin1 := getTestAdmin()
+	admin1.Username = altAdminUsername + "1"
+	admin1.Password = altAdminPassword
+	var permissions []string
+	for _, p := range admin1.GetValidPerms() {
+		if p != dataprovider.PermAdminAny && p != dataprovider.PermAdminDisableMFA {
+			permissions = append(permissions, p)
+		}
+	}
+	admin1.Permissions = permissions
+	admin1, _, err = httpdtest.AddAdmin(admin1, http.StatusCreated)
+	assert.NoError(t, err)
 	// enable two factor authentication
 	configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], admin.Username)
 	assert.NoError(t, err)
@@ -9369,12 +9373,27 @@ func TestAdminTwoFactorLogin(t *testing.T) {
 	assert.NoError(t, err)
 
 	// disable TOTP
+	altToken1, err := getJWTAPITokenFromTestServer(admin1.Username, altAdminPassword)
+	assert.NoError(t, err)
+	req, err = http.NewRequest(http.MethodPut, adminPath+"/"+altAdminUsername+"/2fa/disable", nil)
+	assert.NoError(t, err)
+	setBearerForReq(req, altToken1)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusForbidden, rr)
+
 	req, err = http.NewRequest(http.MethodPut, adminPath+"/"+altAdminUsername+"/2fa/disable", nil)
 	assert.NoError(t, err)
 	setBearerForReq(req, altToken)
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusOK, rr)
 
+	req, err = http.NewRequest(http.MethodPut, adminPath+"/"+altAdminUsername+"/2fa/disable", nil)
+	assert.NoError(t, err)
+	setBearerForReq(req, altToken)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusBadRequest, rr)
+	assert.Contains(t, rr.Body.String(), "two-factor authentication is not enabled")
+
 	form = make(url.Values)
 	form.Set("recovery_code", recoveryCode)
 	form.Set(csrfFormToken, csrfToken)
@@ -9399,6 +9418,8 @@ func TestAdminTwoFactorLogin(t *testing.T) {
 
 	_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
 	assert.NoError(t, err)
+	_, err = httpdtest.RemoveAdmin(admin1, http.StatusOK)
+	assert.NoError(t, err)
 
 	req, err = http.NewRequest(http.MethodPost, webAdminTwoFactorRecoveryPath, bytes.NewBuffer([]byte(form.Encode())))
 	assert.NoError(t, err)
@@ -10107,6 +10128,13 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusOK, rr)
 
+	req, err = http.NewRequest(http.MethodPut, userPath+"/"+user.Username+"/2fa/disable", nil)
+	assert.NoError(t, err)
+	setBearerForReq(req, adminToken)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusBadRequest, rr)
+	assert.Contains(t, rr.Body.String(), "two-factor authentication is not enabled")
+
 	form = make(url.Values)
 	form.Set("recovery_code", recoveryCode)
 	form.Set("passcode", passcode)

+ 6 - 2
internal/httpd/server.go

@@ -1364,7 +1364,7 @@ func (s *httpdServer) initializeRouter() {
 				router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath+"/{username}", getUserByUsername) //nolint:goconst
 				router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}", updateUser)
 				router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(userPath+"/{username}", deleteUser)
-				router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}/2fa/disable", disableUser2FA)
+				router.With(s.checkPerm(dataprovider.PermAdminDisableMFA)).Put(userPath+"/{username}/2fa/disable", disableUser2FA) //nolint:goconst
 				router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath, getFolders)
 				router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath+"/{name}", getFolderByName) //nolint:goconst
 				router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(folderPath, addFolder)
@@ -1392,7 +1392,7 @@ func (s *httpdServer) initializeRouter() {
 				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername)
 				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin)
 				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin)
-				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}/2fa/disable", disableAdmin2FA)
+				router.With(s.checkPerm(dataprovider.PermAdminDisableMFA)).Put(adminPath+"/{username}/2fa/disable", disableAdmin2FA)
 				router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Get(retentionChecksPath, getRetentionChecks)
 				router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Post(retentionBasePath+"/{username}/check",
 					startRetentionCheck)
@@ -1752,6 +1752,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
 					s.handleWebUpdateAdminPost)
 				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), verifyCSRFHeader).
 					Delete(webAdminPath+"/{username}", deleteAdmin)
+				router.With(s.checkPerm(dataprovider.PermAdminDisableMFA), verifyCSRFHeader).
+					Put(webAdminPath+"/{username}/2fa/disable", disableAdmin2FA)
 				router.With(s.checkPerm(dataprovider.PermAdminCloseConnections), verifyCSRFHeader).
 					Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection)
 				router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
@@ -1764,6 +1766,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
 					Post(webScanVFolderPath+"/{name}", startFolderQuotaScan)
 				router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
 					Delete(webUserPath+"/{username}", deleteUser)
+				router.With(s.checkPerm(dataprovider.PermAdminDisableMFA), verifyCSRFHeader).
+					Put(webUserPath+"/{username}/2fa/disable", disableUser2FA)
 				router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
 					Post(webQuotaScanPath+"/{username}", startUserQuotaScan)
 				router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webMaintenancePath, s.handleWebMaintenance)

+ 2 - 0
openapi/openapi.yaml

@@ -4948,6 +4948,7 @@ components:
         - manage_event_rules
         - manage_roles
         - manage_ip_lists
+        - disable_mfa
       description: |
         Admin permissions:
           * `*` - all permissions are granted
@@ -4971,6 +4972,7 @@ components:
           * `manage_event_rules` - manage event actions and rules is allowed
           * `manage_roles` - manage roles is allowed
           * `manage_ip_lists` - manage global and ratelimter allow lists and defender block and safe lists is allowed
+          * `disable_mfa` - allow to disable two-factor authentication for users and admins
     FsProviders:
       type: integer
       enum:

+ 4 - 1
static/locales/en/translation.json

@@ -132,6 +132,7 @@
         "add": "Add",
         "enable": "Enable",
         "disable": "Disable",
+        "disable_confirm_btn": "Yes, disable",
         "close": "Close",
         "search": "Search",
         "configuration": "Configuration",
@@ -403,6 +404,8 @@
         "setup_title": "Set up two-factor authentication",
         "setup_msg": "Use your preferred Authenticator App (e.g. Microsoft Authenticator, Google Authenticator, Authy, 1Password etc. ) to scan the QR code. It will generate an authentication code for you to enter below.",
         "setup_help": "If you have trouble using the QR code, select manual entry on your app, and enter the code:",
+        "disable_msg": "Disable 2FA",
+        "disable_confirm": "Do you want to disable two-factor authentication for the selected user? This action is generally only required if the user has lost access to the second authentication factor",
         "disable_question": "Do you want to disable two-factor authentication?",
         "generate_question": "Do you want to generate a new secret key and invalidate the previous one? Any registered Authenticator App will stop working",
         "disabled": "Two-factor authentication is disabled",
@@ -410,7 +413,7 @@
         "recovery_codes_get_err": "Unable to obtain recovery codes",
         "auth_code_invalid": "Failed to validate the provided authentication code",
         "auth_secret_gen_err": "Failed to generate authentication secret",
-        "save_err": "Failed to save configuration",
+        "save_err": "Failed to save two-factor authentication configuration",
         "auth_code_required": "The authentication code is required",
         "no_protocol": "Please select at least a protocol",
         "required_protocols": "Unable to disable two-factor authentication. The security policy configured for your account requires two-factor authentication for the following protocols: {{val}}",

+ 4 - 1
static/locales/it/translation.json

@@ -132,6 +132,7 @@
         "add": "Aggiungi",
         "enable": "Abilita",
         "disable": "Disabilita",
+        "disable_confirm_btn": "Si, disabilita",
         "close": "Chiudi",
         "search": "Cerca",
         "configuration": "Configurazione",
@@ -403,6 +404,8 @@
         "setup_title": "Configura l'autenticazione a due fattori",
         "setup_msg": "Utilizza la tua app di autenticazione preferita (ad esempio Microsoft Authenticator, Google Authenticator, Authy, 1Password ecc.) per scansionare il codice QR. Verrà generato un codice di autenticazione da inserire di seguito.",
         "setup_help": "Se hai problemi con l'utilizzo del codice QR, seleziona l'inserimento manuale sulla tua app e inserisci il codice:",
+        "disable_msg": "Disabilita 2FA",
+        "disable_confirm": "Vuoi disabilitare l'autenticazione a due fattori per l'utente selezionato? Questa azione è generalmente necessaria solo se l'utente ha perso l'accesso al secondo fattore di autenticazione",
         "disable_question": "Vuoi disattivare l'autenticazione a due fattori?",
         "generate_question": "Vuoi generare un nuova chiave segreta e invalidare quella precedente? Qualsiasi app di autenticazione registrata smetterà di funzionare",
         "disabled": "L'autenticazione a due fattori è disabilitata",
@@ -410,7 +413,7 @@
         "recovery_codes_get_err": "Impossibile ottenere i codici di ripristino",
         "auth_code_invalid": "Impossibile convalidare il codice di autenticazione fornito",
         "auth_secret_gen_err": "Impossibile generare il segreto di autenticazione",
-        "save_err": "Impossibile salvare la configurazione",
+        "save_err": "Impossibile salvare la configurazione dell'autenticazione a due fattori",
         "auth_code_required": "Il codice di autenticazione è obbligatorio",
         "no_protocol": "Seleziona almeno un protocollo",
         "required_protocols": "Impossibile disabilitare l'autenticazione a due fattori. La politica di sicurezza configurata per il tuo account richiede l'autenticazione a due fattori per i seguenti protocolli: {{val}}",

+ 84 - 13
templates/webadmin/admins.html

@@ -175,6 +175,47 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
         });
     }
 
+    function disableSeconFactorAction(username) {
+        ModalAlert.fire({
+            text: $.t('2fa.disable_confirm'),
+            icon: "warning",
+            confirmButtonText: $.t('general.disable_confirm_btn'),
+            cancelButtonText: $.t('general.cancel'),
+            customClass: {
+                confirmButton: "btn btn-danger",
+                cancelButton: 'btn btn-secondary'
+            }
+        }).then((result) => {
+            if (result.isConfirmed){
+                $('#loading_message').text("");
+                KTApp.showPageLoading();
+                let path = '{{.AdminURL}}' + "/" + encodeURIComponent(username)+"/2fa/disable";
+
+                axios.put(path, null, {
+                    timeout: 15000,
+                    headers: {
+                        'X-CSRF-TOKEN': '{{.CSRFToken}}'
+                    },
+                    validateStatus: function (status) {
+                        return status == 200;
+                    }
+                }).then(function(response){
+                    location.reload();
+                }).catch(function(error){
+                    KTApp.hidePageLoading();
+                    ModalAlert.fire({
+                        text: $.t('2fa.save_err'),
+                        icon: "warning",
+                        confirmButtonText: $.t('general.ok'),
+                        customClass: {
+                            confirmButton: "btn btn-primary"
+                        }
+                    });
+                });
+            }
+        });
+    }
+
     var datatable = function(){
         var dt;
 
@@ -273,17 +314,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
                         }
                     },
                     {
-                        data: "filters.totp_config.enabled",
+                        data: "filters.totp_config",
                         visible: false,
                         defaultContent: false,
                         render: function(data, type, row) {
                             if (type === 'display') {
-                                if (data){
+                                if (data && data.enabled){
                                     return $.t('general.active');
                                 }
                                 return $.t('general.inactive')
                             }
-                            return data;
+                            return data && data.enabled;
                         }
                     },
                     {
@@ -307,20 +348,39 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
                         className: 'text-end',
                         render: function (data, type, row) {
                             if (type === 'display') {
-                                //{{- if .LoggedUser.HasPermission "manage_admins"}}
-                                return `<button class="btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
+                                let numActions = 0;
+                                let username = "{{.LoggedUser.Username}}";
+                                let actions = `<button class="btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
                                                     <span data-i18n="general.actions" class="fs-6">Actions</span>
 										            <i class="ki-duotone ki-down fs-5 ms-1 rotate-180"></i>
                                                 </button>
-                                                <div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4" data-kt-menu="true">
-                                                    <div class="menu-item px-3">
-										                <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
-										            </div>
-                                                    <div class="menu-item px-3">
-                                                        <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
-										            </div>
-                                                </div>`;
+                                                <div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4" data-kt-menu="true">`;
+                                //{{- if .LoggedUser.HasPermission "manage_admins"}}
+                                numActions++;
+                                actions+=`<div class="menu-item px-3">
+										      <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
+										  </div>`;
+                                //{{- end}}
+                                //{{- if .LoggedUser.HasPermission "disable_mfa"}}
+                                if (row.filters.totp_config && row.filters.totp_config.enabled && username != row.username){
+                                    numActions++;
+                                    actions+=`<div class="menu-item px-3">
+                                                <a data-i18n="2fa.disable_msg" href="#" class="menu-link text-danger px-3" data-table-action="disable_2fa_row">Disable 2FA</a>
+										      </div>`;
+                                }
+                                //{{- end}}
+                                //{{- if .LoggedUser.HasPermission "manage_admins"}}
+                                if (username != row.username){
+                                    numActions++;
+                                    actions+=`<div class="menu-item px-3">
+                                                <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
+										     </div>`;
+                                }
                                 //{{- end}}
+                                if (numActions > 0){
+                                    actions+=`</div>`;
+                                    return actions;
+                                }
                             }
                             return "";
                         }
@@ -406,6 +466,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
                 });
             });
 
+            const diable2FAButtons = document.querySelectorAll('[data-table-action="disable_2fa_row"]');
+            diable2FAButtons.forEach(d => {
+                let el = $(d);
+                el.off("click");
+                el.on("click", function(e){
+                    e.preventDefault();
+                    let rowData = dt.row(e.target.closest('tr')).data();
+                    disableSeconFactorAction(rowData['username']);
+                });
+            });
+
             const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
             deleteButtons.forEach(d => {
                 let el = $(d);

+ 2 - 2
templates/webadmin/eventactions.html

@@ -230,11 +230,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
 										      <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
-										  </div>`
+										  </div>`;
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
                                              <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
-										  </div>`
+										  </div>`;
                                 //{{- end}}
                                 if (numActions > 0){
                                     actions+=`</div>`;

+ 3 - 3
templates/webadmin/eventrules.html

@@ -286,17 +286,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
 										      <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
-										  </div>`
+										  </div>`;
                                 numActions++;
                                 if (row.trigger === 6){
                                     actions+=`<div class="menu-item px-3">
 										      <a data-i18n="rules.run" href="#" class="menu-link px-3" data-table-action="run_row">Run</a>
-										  </div>`
+										  </div>`;
                                 numActions++;
                                 }
                                 actions+=`<div class="menu-item px-3">
                                              <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
-										  </div>`
+										  </div>`;
                                 //{{- end}}
                                 if (numActions > 0){
                                     actions+=`</div>`;

+ 3 - 3
templates/webadmin/folders.html

@@ -287,19 +287,19 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
 										      <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
-										  </div>`
+										  </div>`;
                                 //{{- end}}
                                 //{{- if .LoggedUser.HasPermission "manage_system"}}
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
 										      <a data-i18n="general.template" href="#" class="menu-link px-3" data-table-action="template_row">Template</a>
-										  </div>`
+										  </div>`;
                                 //{{- end}}
                                 //{{- if .LoggedUser.HasPermission "manage_folders"}}
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
                                              <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
-										  </div>`
+										  </div>`;
                                 //{{- end}}
                                 if (numActions > 0){
                                     actions+=`</div>`;

+ 2 - 2
templates/webadmin/groups.html

@@ -228,11 +228,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
 										      <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
-										  </div>`
+										  </div>`;
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
                                              <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
-										  </div>`
+										  </div>`;
                                 //{{- end}}
                                 if (numActions > 0){
                                     actions+=`</div>`;

+ 2 - 2
templates/webadmin/roles.html

@@ -229,11 +229,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
 										      <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
-										  </div>`
+										  </div>`;
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
                                              <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
-										  </div>`
+										  </div>`;
                                 //{{- end}}
                                 if (numActions > 0){
                                     actions+=`</div>`;

+ 106 - 43
templates/webadmin/users.html

@@ -188,44 +188,85 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
         });
     }
 
-    function quotaScanAction(username) {
-            $('#loading_message').text("");
-            KTApp.showPageLoading();
-            let path = '{{.QuotaScanURL}}' + "/" + encodeURIComponent(username);
-            axios.post(path, null, {
-                timeout: 15000,
-                headers: {
-                    'X-CSRF-TOKEN': '{{.CSRFToken}}'
-                },
-                validateStatus: function (status) {
-                    return status == 202;
-                }
-            }).then(function (response) {
-                KTApp.hidePageLoading();
-                showToast(1, 'general.quota_scan_started');
-            }).catch(function (error) {
-                KTApp.hidePageLoading();
-                let errorMessage;
-                if (error && error.response) {
-                    switch (error.response.status) {
-                        case 409:
-                            errorMessage = "general.quota_scan_conflit";
-                            break;
+    function disableSeconFactorAction(username) {
+        ModalAlert.fire({
+            text: $.t('2fa.disable_confirm'),
+            icon: "warning",
+            confirmButtonText: $.t('general.disable_confirm_btn'),
+            cancelButtonText: $.t('general.cancel'),
+            customClass: {
+                confirmButton: "btn btn-danger",
+                cancelButton: 'btn btn-secondary'
+            }
+        }).then((result) => {
+            if (result.isConfirmed){
+                $('#loading_message').text("");
+                KTApp.showPageLoading();
+                let path = '{{.UserURL}}' + "/" + encodeURIComponent(username)+"/2fa/disable";
+
+                axios.put(path, null, {
+                    timeout: 15000,
+                    headers: {
+                        'X-CSRF-TOKEN': '{{.CSRFToken}}'
+                    },
+                    validateStatus: function (status) {
+                        return status == 200;
                     }
+                }).then(function(response){
+                    location.reload();
+                }).catch(function(error){
+                    KTApp.hidePageLoading();
+                    ModalAlert.fire({
+                        text: $.t('2fa.save_err'),
+                        icon: "warning",
+                        confirmButtonText: $.t('general.ok'),
+                        customClass: {
+                            confirmButton: "btn btn-primary"
+                        }
+                    });
+                });
+            }
+        });
+    }
+
+    function quotaScanAction(username) {
+        $('#loading_message').text("");
+        KTApp.showPageLoading();
+        let path = '{{.QuotaScanURL}}' + "/" + encodeURIComponent(username);
+        axios.post(path, null, {
+            timeout: 15000,
+            headers: {
+                'X-CSRF-TOKEN': '{{.CSRFToken}}'
+            },
+            validateStatus: function (status) {
+                return status == 202;
+            }
+        }).then(function (response) {
+            KTApp.hidePageLoading();
+            showToast(1, 'general.quota_scan_started');
+        }).catch(function (error) {
+            KTApp.hidePageLoading();
+            let errorMessage;
+            if (error && error.response) {
+                switch (error.response.status) {
+                    case 409:
+                        errorMessage = "general.quota_scan_conflit";
+                        break;
                 }
-                if (!errorMessage) {
-                    errorMessage = "general.quota_scan_error";
+            }
+            if (!errorMessage) {
+                errorMessage = "general.quota_scan_error";
+            }
+            ModalAlert.fire({
+                text: $.t(errorMessage),
+                icon: "warning",
+                confirmButtonText: $.t('general.ok'),
+                customClass: {
+                    confirmButton: "btn btn-primary"
                 }
-                ModalAlert.fire({
-                    text: $.t(errorMessage),
-                    icon: "warning",
-                    confirmButtonText: $.t('general.ok'),
-                    customClass: {
-                        confirmButton: "btn btn-primary"
-                    }
-                });
             });
-        }
+        });
+    }
 
     var datatable = function(){
         var dt;
@@ -359,17 +400,20 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
                         }
                     },
                     {
-                        data: "filters.two_factor_protocols",
+                        data: "filters.totp_config",
                         visible: false,
                         defaultContent: "",
                         render: function(data, type, row) {
+                            let protocols = "";
+                            if (data && data.enabled){
+                                protocols = data.protocols.join(', ');
+                            }
                             if (type === 'display') {
-                                if (data){
-                                    return escapeHTML(data.join(', '));
+                                if (protocols){
+                                    return escapeHTML(protocols);
                                 }
-                                return ""
                             }
-                            return data;
+                            return protocols;
                         }
                     },
                     {
@@ -486,25 +530,33 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
 										      <a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
-										  </div>`
+										  </div>`;
                                 //{{- end}}
                                 //{{- if .LoggedUser.HasPermission "manage_system"}}
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
 										      <a data-i18n="general.template" href="#" class="menu-link px-3" data-table-action="template_row">Template</a>
-										  </div>`
+										  </div>`;
                                 //{{- end}}
                                 //{{- if .LoggedUser.HasPermission "quota_scans"}}
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
 										      <a data-i18n="general.quota_scan" href="#" class="menu-link px-3" data-table-action="quota_scan_row">Quota scan</a>
-										  </div>`
+										  </div>`;
+                                //{{- end}}
+                                //{{- if .LoggedUser.HasPermission "disable_mfa"}}
+                                if (row.filters.totp_config && row.filters.totp_config.enabled){
+                                    numActions++;
+                                    actions+=`<div class="menu-item px-3">
+                                                <a data-i18n="2fa.disable_msg" href="#" class="menu-link text-danger px-3" data-table-action="disable_2fa_row">Disable 2FA</a>
+										      </div>`;
+                                }
                                 //{{- end}}
                                 //{{- if .LoggedUser.HasPermission "del_users"}}
                                 numActions++;
                                 actions+=`<div class="menu-item px-3">
                                              <a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
-										  </div>`
+										  </div>`;
                                 //{{- end}}
                                 if (numActions > 0){
                                     actions+=`</div>`;
@@ -620,6 +672,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
                 });
             });
 
+            const diable2FAButtons = document.querySelectorAll('[data-table-action="disable_2fa_row"]');
+            diable2FAButtons.forEach(d => {
+                let el = $(d);
+                el.off("click");
+                el.on("click", function(e){
+                    e.preventDefault();
+                    let rowData = dt.row(e.target.closest('tr')).data();
+                    disableSeconFactorAction(rowData['username']);
+                });
+            });
+
             const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
             deleteButtons.forEach(d => {
                 let el = $(d);