mirror of
https://github.com/balzack/databag.git
synced 2025-02-11 19:19:16 +00:00
adding mfa to admin login
This commit is contained in:
parent
53bfc32d4f
commit
51306e92c4
149
doc/api.oa3
149
doc/api.oa3
@ -87,6 +87,139 @@ paths:
|
||||
'500':
|
||||
description: internal server error
|
||||
|
||||
/admin/access:
|
||||
put:
|
||||
tags:
|
||||
- admin
|
||||
description: Acquire new session token for admin endpoints
|
||||
operationId: set-admin-session
|
||||
parameters:
|
||||
- name: token
|
||||
in: query
|
||||
description: access token
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: code
|
||||
in: query
|
||||
description: totp code
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'201':
|
||||
description: generated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
'401':
|
||||
description: invalid token
|
||||
'405':
|
||||
description: totp code required but not set
|
||||
'429':
|
||||
description: temporarily locked due to too many failures
|
||||
'500':
|
||||
description: internal server error
|
||||
|
||||
/admin/mfauth:
|
||||
get:
|
||||
tags:
|
||||
- admin
|
||||
description: check if multi-factor authentication enabled
|
||||
operationId: get-admin-mfa
|
||||
parameters:
|
||||
- name: token
|
||||
in: query
|
||||
description: session token
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: boolean
|
||||
'401':
|
||||
description: permission denied
|
||||
'500':
|
||||
description: internal server error
|
||||
post:
|
||||
tags:
|
||||
- admin
|
||||
description: Enable multi-factor authentication
|
||||
operationId: add-admin-mfa
|
||||
parameters:
|
||||
- name: token
|
||||
in: query
|
||||
description: session token
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'201':
|
||||
description: success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
'401':
|
||||
description: permission denied
|
||||
'500':
|
||||
description: internal server error
|
||||
put:
|
||||
tags:
|
||||
- admin
|
||||
description: Confirm multi-factor authentication
|
||||
operationId: confirm-admin-mfa
|
||||
parameters:
|
||||
- name: token
|
||||
in: query
|
||||
description: session token
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: code
|
||||
in: query
|
||||
description: totp code generated from secret
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: success
|
||||
'401':
|
||||
description: permission denied
|
||||
'403':
|
||||
description: totp code not correct
|
||||
'405':
|
||||
description: totp code required but not set
|
||||
'429':
|
||||
description: temporarily locked due to too many failures
|
||||
'500':
|
||||
description: internal server error
|
||||
delete:
|
||||
tags:
|
||||
- admin
|
||||
description: Disable multi-factor authentication
|
||||
operationId: remove-admin-mfa
|
||||
parameters:
|
||||
- name: token
|
||||
in: query
|
||||
description: session token
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: success
|
||||
'401':
|
||||
description: permission denied
|
||||
'500':
|
||||
description: internal server error
|
||||
|
||||
/admin/config:
|
||||
get:
|
||||
tags:
|
||||
@ -96,7 +229,7 @@ paths:
|
||||
parameters:
|
||||
- name: token
|
||||
in: query
|
||||
description: token for admin access
|
||||
description: session token for admin access
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
@ -119,7 +252,7 @@ paths:
|
||||
parameters:
|
||||
- name: token
|
||||
in: query
|
||||
description: token for admin access
|
||||
description: session token for admin access
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
@ -146,7 +279,7 @@ paths:
|
||||
parameters:
|
||||
- name: token
|
||||
in: query
|
||||
description: token for admin access
|
||||
description: session token for admin access
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
@ -173,7 +306,7 @@ paths:
|
||||
parameters:
|
||||
- name: token
|
||||
in: query
|
||||
description: token for admin access
|
||||
description: session token for admin access
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
@ -204,7 +337,7 @@ paths:
|
||||
type: string
|
||||
- name: token
|
||||
in: query
|
||||
description: token for admin access
|
||||
description: session token for admin access
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
@ -238,7 +371,7 @@ paths:
|
||||
type: string
|
||||
- name: token
|
||||
in: query
|
||||
description: token for admin access
|
||||
description: session token for admin access
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
@ -261,7 +394,7 @@ paths:
|
||||
parameters:
|
||||
- name: token
|
||||
in: query
|
||||
description: token for admin access
|
||||
description: session token for admin access
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
@ -298,7 +431,7 @@ paths:
|
||||
type: string
|
||||
- name: token
|
||||
in: query
|
||||
description: token for admin access
|
||||
description: session token for admin access
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
|
73
net/server/internal/api_addAdminMFAuth.go
Normal file
73
net/server/internal/api_addAdminMFAuth.go
Normal file
@ -0,0 +1,73 @@
|
||||
package databag
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"image/png"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"databag/internal/store"
|
||||
"encoding/base64"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
//AddAdminMFAuth enables multi-factor auth on the given account
|
||||
func AddAdminMFAuth(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// validate login
|
||||
if code, err := ParamSessionToken(r); err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: APPMFAIssuer,
|
||||
AccountName: "admin",
|
||||
Digits: otp.DigitsSix,
|
||||
Algorithm: otp.AlgorithmSHA256,
|
||||
})
|
||||
|
||||
err = store.DB.Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
// upsert mfa enabled
|
||||
if res := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "config_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"bool_value"}),
|
||||
}).Create(&store.Config{ConfigID: CNFMFAEnabled, BoolValue: false}).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
|
||||
// upsert mfa confirmed
|
||||
if res := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "config_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"bool_value"}),
|
||||
}).Create(&store.Config{ConfigID: CNFMFAConfirmed, BoolValue: true}).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
|
||||
// upsert mfa secret
|
||||
if res := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "config_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"str_value"}),
|
||||
}).Create(&store.Config{ConfigID: CNFMFASecret, StrValue: key.Secret()}).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
ErrResponse(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
img, err := key.Image(200, 200)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
png.Encode(&buf, img)
|
||||
enc := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
|
||||
WriteResponse(w, MFASecret{ Image: "data:image/png;base64," + enc, Text: key.Secret() })
|
||||
}
|
@ -11,7 +11,7 @@ import (
|
||||
//AddNodeAccount generate a new token to be used for account creation
|
||||
func AddNodeAccount(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if code, err := ParamAdminToken(r); err != nil {
|
||||
if code, err := ParamSessionToken(r); err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ func AddNodeAccountAccess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if code, err := ParamAdminToken(r); err != nil {
|
||||
if code, err := ParamSessionToken(r); err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
20
net/server/internal/api_getAdminMFAuth.go
Normal file
20
net/server/internal/api_getAdminMFAuth.go
Normal file
@ -0,0 +1,20 @@
|
||||
package databag
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//GetAdminMFAuth checks if mfa enabled for admin
|
||||
func GetAdminMFAuth(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// validate login
|
||||
if code, err := ParamSessionToken(r); err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
||||
enabled := getBoolConfigValue(CNFMFAEnabled, false);
|
||||
confirmed := getBoolConfigValue(CNFMFAConfirmed, false);
|
||||
|
||||
WriteResponse(w, enabled && confirmed)
|
||||
}
|
@ -23,7 +23,7 @@ func GetNodeAccountImage(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if code, err := ParamAdminToken(r); err != nil {
|
||||
if code, err := ParamSessionToken(r); err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
//GetNodeAccounts retrieves profiles of hosted accounts for the admin
|
||||
func GetNodeAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if code, err := ParamAdminToken(r); err != nil {
|
||||
if code, err := ParamSessionToken(r); err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
func GetNodeConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// validate login
|
||||
if code, err := ParamAdminToken(r); err != nil {
|
||||
if code, err := ParamSessionToken(r); err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
40
net/server/internal/api_removeAdminMFAuth.go
Normal file
40
net/server/internal/api_removeAdminMFAuth.go
Normal file
@ -0,0 +1,40 @@
|
||||
package databag
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"databag/internal/store"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
//Disable multi-factor auth for admin
|
||||
func RemoveAdminMFAuth(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// validate login
|
||||
if code, err := ParamSessionToken(r); err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
||||
err := store.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if res := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "config_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"str_value"}),
|
||||
}).Create(&store.Config{ConfigID: CNFMFAConfirmed, BoolValue: false}).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
if res := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "config_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"str_value"}),
|
||||
}).Create(&store.Config{ConfigID: CNFMFAEnabled, BoolValue: false}).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
ErrResponse(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
WriteResponse(w, nil)
|
||||
}
|
@ -21,7 +21,7 @@ func RemoveNodeAccount(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if code, err := ParamAdminToken(r); err != nil {
|
||||
if code, err := ParamSessionToken(r); err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
45
net/server/internal/api_setAdminAccess.go
Normal file
45
net/server/internal/api_setAdminAccess.go
Normal file
@ -0,0 +1,45 @@
|
||||
package databag
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/theckman/go-securerandom"
|
||||
"databag/internal/store"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//SetAdminAccess begins a session for admin access
|
||||
func SetAdminAccess(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// validate login
|
||||
if code, err := ParamAdminToken(r); err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
||||
// gernate app token
|
||||
data, err := securerandom.Bytes(APPTokenSize)
|
||||
if err != nil {
|
||||
ErrResponse(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
access := hex.EncodeToString(data)
|
||||
|
||||
err = store.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// upsert mfa enabled
|
||||
if res := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "config_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"str_value"}),
|
||||
}).Create(&store.Config{ConfigID: CNFAdminSession, StrValue: access}).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
WriteResponse(w, access)
|
||||
}
|
92
net/server/internal/api_setAdminMFAuth.go
Normal file
92
net/server/internal/api_setAdminMFAuth.go
Normal file
@ -0,0 +1,92 @@
|
||||
package databag
|
||||
|
||||
import (
|
||||
"databag/internal/store"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"net/http"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
//SetMultiFactorAuth
|
||||
func SetAdminMFAuth(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// validate login
|
||||
if code, err := ParamSessionToken(r); err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !getBoolConfigValue(CNFMFAEnabled, false) {
|
||||
ErrResponse(w, http.StatusMethodNotAllowed, errors.New("totp not enabled"))
|
||||
return;
|
||||
}
|
||||
code := r.FormValue("code")
|
||||
if code == "" {
|
||||
ErrResponse(w, http.StatusMethodNotAllowed, errors.New("totp code required"))
|
||||
return;
|
||||
}
|
||||
|
||||
curTime := time.Now().Unix()
|
||||
failedTime := getNumConfigValue(CNFMFAFailedTime, 0);
|
||||
failedCount := getNumConfigValue(CNFMFAFailedCount, 0);
|
||||
if failedTime + APPMFAFailPeriod > curTime && failedCount > APPMFAFailCount {
|
||||
ErrResponse(w, http.StatusTooManyRequests, errors.New("temporarily locked"))
|
||||
return;
|
||||
}
|
||||
|
||||
secret := getStrConfigValue(CNFMFASecret, "");
|
||||
opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA256}
|
||||
if valid, _ := totp.ValidateCustom(code, secret, time.Now(), opts); !valid {
|
||||
err := store.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if failedTime + APPMFAFailPeriod > curTime {
|
||||
if res := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "config_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"num_value"}),
|
||||
}).Create(&store.Config{ConfigID: CNFMFAFailedCount, NumValue: failedCount + 1}).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
} else {
|
||||
if res := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "config_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"num_value"}),
|
||||
}).Create(&store.Config{ConfigID: CNFMFAFailedTime, NumValue: curTime}).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
if res := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "config_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"num_value"}),
|
||||
}).Create(&store.Config{ConfigID: CNFMFAFailedCount, NumValue: failedCount + 1}).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
LogMsg("failed to increment fail count");
|
||||
}
|
||||
|
||||
ErrResponse(w, http.StatusUnauthorized, errors.New("invalid code"))
|
||||
return
|
||||
}
|
||||
|
||||
err := store.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// upsert mfa confirmed
|
||||
if res := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "config_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"str_value"}),
|
||||
}).Create(&store.Config{ConfigID: CNFMFAConfirmed, BoolValue: true}).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
ErrResponse(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
WriteResponse(w, nil)
|
||||
}
|
@ -18,7 +18,7 @@ func SetNodeAccountStatus(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if code, err := ParamAdminToken(r); err != nil {
|
||||
if code, err := ParamSessionToken(r); err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
func SetNodeConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// validate login
|
||||
if code, err := ParamAdminToken(r); err != nil {
|
||||
if code, err := ParamSessionToken(r); err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
@ -95,6 +95,30 @@ func ParamAdminToken(r *http.Request) (int, error) {
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
//ParamSessionToken compares session token with token query param
|
||||
func ParamSessionToken(r *http.Request) (int, error) {
|
||||
|
||||
// parse authentication token
|
||||
token := r.FormValue("token")
|
||||
if token == "" {
|
||||
return http.StatusUnauthorized, errors.New("token not set")
|
||||
}
|
||||
|
||||
// nothing to do if not configured
|
||||
if !getBoolConfigValue(CNFConfigured, false) {
|
||||
return http.StatusUnauthorized, errors.New("node not configured")
|
||||
}
|
||||
|
||||
// compare password
|
||||
value := getStrConfigValue(CNFAdminSession, "")
|
||||
if value != token {
|
||||
return http.StatusUnauthorized, errors.New("invalid session token")
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
|
||||
//GetSessionDetail retrieves account detail specified by agent query param
|
||||
func GetSessionDetail(r *http.Request) (*store.Session, int, error) {
|
||||
|
||||
|
@ -63,6 +63,23 @@ const CNFIceUsername = "ice_username"
|
||||
//CNFIceUrl specifies the ice candidate url
|
||||
const CNFIcePassword = "ice_password"
|
||||
|
||||
//CNFMFAFailedTime start of mfa failure window
|
||||
const CNFMFAFailedTime = "mfa_failed_time"
|
||||
|
||||
//CNFMFAFailedCount number of failures in window
|
||||
const CNFMFAFailedCount = "mfa_failed_count"
|
||||
|
||||
//CNFMFARequired specified if mfa enabled for admin
|
||||
const CNFMFAEnabled = "mfa_enabled"
|
||||
|
||||
//CNFMFAConfirmed specified if mfa has been confirmed for admin
|
||||
const CNFMFAConfirmed = "mfa_confirmed"
|
||||
|
||||
//CNFMFASecret specified the mfa secret
|
||||
const CNFMFASecret = "mfa_secret"
|
||||
|
||||
//CNFAdminSession sepcifies the admin session token
|
||||
const CNFAdminSession = "admin_session"
|
||||
|
||||
func getStrConfigValue(configID string, empty string) string {
|
||||
var config store.Config
|
||||
|
@ -279,6 +279,41 @@ var endpoints = routes{
|
||||
ImportAccount,
|
||||
},
|
||||
|
||||
route{
|
||||
"SetAdminAccess",
|
||||
strings.ToUpper("Put"),
|
||||
"/admin/access",
|
||||
SetAdminAccess,
|
||||
},
|
||||
|
||||
route{
|
||||
"GetAdminMFAuth",
|
||||
strings.ToUpper("Get"),
|
||||
"/admin/mfauth",
|
||||
GetAdminMFAuth,
|
||||
},
|
||||
|
||||
route{
|
||||
"AddAdminMFAuth",
|
||||
strings.ToUpper("Post"),
|
||||
"/admin/mfauth",
|
||||
AddAdminMFAuth,
|
||||
},
|
||||
|
||||
route{
|
||||
"SetAdminMFAuth",
|
||||
strings.ToUpper("Put"),
|
||||
"/admin/mfauth",
|
||||
SetAdminMFAuth,
|
||||
},
|
||||
|
||||
route{
|
||||
"RemoveAdminMFAuth",
|
||||
strings.ToUpper("Delete"),
|
||||
"/admin/mfauth",
|
||||
RemoveAdminMFAuth,
|
||||
},
|
||||
|
||||
route{
|
||||
"RemoveNodeAccount",
|
||||
strings.ToUpper("Delete"),
|
||||
|
@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { getNodeStatus } from 'api/getNodeStatus';
|
||||
import { setNodeStatus } from 'api/setNodeStatus';
|
||||
import { getNodeConfig } from 'api/getNodeConfig';
|
||||
import { setNodeAccess } from 'api/setNodeAccess';
|
||||
import { AppContext } from 'context/AppContext';
|
||||
import { SettingsContext } from 'context/SettingsContext';
|
||||
|
||||
@ -52,9 +53,10 @@ export function useAdmin() {
|
||||
if (state.unclaimed === true) {
|
||||
await setNodeStatus(state.password);
|
||||
}
|
||||
await getNodeConfig(state.password);
|
||||
const session = await setNodeAccess(state.password);
|
||||
|
||||
updateState({ busy: false });
|
||||
app.actions.setAdmin(state.password);
|
||||
app.actions.setAdmin(session);
|
||||
}
|
||||
catch(err) {
|
||||
console.log(err);
|
||||
|
8
net/web/src/api/getAdminMFAuth.js
Normal file
8
net/web/src/api/getAdminMFAuth.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
||||
|
||||
export async function getAdminMFAuth(token) {
|
||||
const mfa = await fetchWithTimeout(`/admin/mfauth?token=${encodeURIComponent(token)}`, { method: 'GET' });
|
||||
checkResponse(mfa);
|
||||
return await mfa.json();
|
||||
}
|
||||
|
8
net/web/src/api/setNodeAccess.js
Normal file
8
net/web/src/api/setNodeAccess.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
||||
|
||||
export async function setNodeAccess(token) {
|
||||
const access = await fetchWithTimeout(`/admin/access?token=${encodeURIComponent(token)}`, { method: 'PUT' });
|
||||
checkResponse(access);
|
||||
return access.json()
|
||||
}
|
||||
|
@ -195,6 +195,13 @@ export const en = {
|
||||
mfaDisabled: 'verification temporarily disabled',
|
||||
mfaConfirm: 'Confirm',
|
||||
mfaEnter: 'Enter your verification code',
|
||||
|
||||
enableMultifactor: 'Enable multi-factor authentication',
|
||||
disableMultifactor: 'Disable multi-factor authentication',
|
||||
|
||||
disable: 'Disable',
|
||||
confirmDisable: 'Disabling Multi-Factor Authentication',
|
||||
disablePrompt: 'Are you sure you want to disable multi-factor authentication',
|
||||
};
|
||||
|
||||
export const fr = {
|
||||
@ -395,6 +402,13 @@ export const fr = {
|
||||
mfaError: 'erreur de code de vérification',
|
||||
mfaDisabled: 'vérification temporairement désactivée',
|
||||
mfaConfirm: 'Confirmer',
|
||||
|
||||
enableMultifactor: 'Activer l\'authentification multifacteur',
|
||||
disableMultifactor: 'Désactiver l\'authentification multifacteur',
|
||||
|
||||
disable: 'Désactiver',
|
||||
confirmDisable: 'Désactivation de l\'authentification multi-facteurs',
|
||||
disablePrompt: 'Êtes-vous sûr de vouloir désactiver l\'authentification multi-facteurs',
|
||||
};
|
||||
|
||||
export const sp = {
|
||||
@ -594,6 +608,13 @@ export const sp = {
|
||||
mfaError: 'error de código de verificación',
|
||||
mfaDisabled: 'verificación temporalmente deshabilitada',
|
||||
mfaConfirm: 'Confirmar',
|
||||
|
||||
enableMultifactor: 'Habilitar la autenticación multifactor',
|
||||
disableMultifactor: 'Deshabilitar la autenticación multifactor',
|
||||
|
||||
disable: 'Desactivar',
|
||||
confirmDisable: 'Desactivación de la autenticación de dos factores',
|
||||
disablePrompt: '¿Estás seguro de que quieres desactivar la autenticación de dos factores?',
|
||||
};
|
||||
|
||||
export const pt = {
|
||||
@ -793,6 +814,13 @@ export const pt = {
|
||||
mfaError: 'erro de código de verificação',
|
||||
mfaDisabled: 'verificação temporariamente desativada',
|
||||
mfaConfirm: 'Confirmar',
|
||||
|
||||
enableMultifactor: 'Habilitar autenticação multifator',
|
||||
disableMultifactor: 'Desativar autenticação multifator',
|
||||
|
||||
disable: 'Desativar',
|
||||
confirmDisable: 'Desativando Autenticação de Dois Fatores',
|
||||
disablePrompt: 'Tem certeza de que deseja desativar a autenticação de dois fatores?',
|
||||
};
|
||||
|
||||
export const de = {
|
||||
@ -992,6 +1020,13 @@ export const de = {
|
||||
mfaError: 'Verifizierungscodefehler',
|
||||
mfaDisabled: 'Verifizierung vorübergehend deaktiviert',
|
||||
mfaConfirm: 'Bestätigen',
|
||||
|
||||
enableMultifactor: 'Aktivieren Sie die Multi-Faktor-Authentifizierung',
|
||||
disableMultifactor: 'Deaktivieren Sie die Multi-Faktor-Authentifizierung',
|
||||
|
||||
disable: 'Deaktivieren',
|
||||
confirmDisable: 'Deaktivierung der Zwei-Faktor-Authentifizierung',
|
||||
disablePrompt: 'Sind Sie sicher, dass Sie die Zwei-Faktor-Authentifizierung deaktivieren möchten?',
|
||||
};
|
||||
|
||||
export const ru = {
|
||||
@ -1191,4 +1226,11 @@ export const ru = {
|
||||
mfaError: 'ошибка проверочного кода',
|
||||
mfaDisabled: 'проверка временно отключена',
|
||||
mfaConfirm: 'Подтвердить',
|
||||
|
||||
enableMultifactor: 'Включить многофакторную аутентификацию',
|
||||
disableMultifactor: 'Отключить многофакторную аутентификацию',
|
||||
|
||||
disable: 'Отключить',
|
||||
confirmDisable: 'Отключение двухфакторной аутентификации',
|
||||
disablePrompt: 'Вы уверены, что хотите отключить двухфакторную аутентификацию?',
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AlertIcon, DashboardWrapper, SettingsButton, AddButton, SettingsLayout, CreateLayout } from './Dashboard.styled';
|
||||
import { Tooltip, Switch, Select, Button, Space, Modal, Input, InputNumber, List } from 'antd';
|
||||
import { ExclamationCircleOutlined, SettingOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { ExclamationCircleOutlined, SettingOutlined, UserAddOutlined, LogoutOutlined, ReloadOutlined, LockOutlined, UnlockOutlined } from '@ant-design/icons';
|
||||
import { ThemeProvider } from "styled-components";
|
||||
import { useDashboard } from './useDashboard.hook';
|
||||
import { AccountItem } from './accountItem/AccountItem';
|
||||
@ -8,6 +8,7 @@ import { CopyButton } from '../copyButton/CopyButton';
|
||||
|
||||
export function Dashboard() {
|
||||
|
||||
const [ modal, modalContext ] = Modal.useModal();
|
||||
const { state, actions } = useDashboard();
|
||||
|
||||
const onClipboard = async (value) => {
|
||||
@ -18,9 +19,26 @@ export function Dashboard() {
|
||||
return window.location.origin + '/#/create?add=' + state.createToken;
|
||||
};
|
||||
|
||||
const disableMFA = () => {
|
||||
modal.confirm({
|
||||
title: <span style={state.menuStyle}>{state.strings.confirmDisable}</span>,
|
||||
content: <span style={state.menuStyle}>{state.strings.disablePrompt}</span>,
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
bodyStyle: { borderRadius: 8, padding: 16, ...state.menuStyle },
|
||||
okText: state.strings.disable,
|
||||
cancelText: state.strings.cancel,
|
||||
onOk() {
|
||||
actions.disableMFA();
|
||||
},
|
||||
onCancel() {},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={state.colors}>
|
||||
<DashboardWrapper>
|
||||
{ modalContext }
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<div className="label">{ state.strings.accounts }</div>
|
||||
@ -34,6 +52,18 @@ export function Dashboard() {
|
||||
<SettingsButton type="text" size="small" icon={<SettingOutlined />}
|
||||
onClick={() => actions.setShowSettings(true)}></SettingsButton>
|
||||
</div>
|
||||
{ (state.mfAuthSet && state.mfaAuthEnabled) && (
|
||||
<div className="settings">
|
||||
<SettingsButton type="text" size="small" icon={<UnlockOutlined />}
|
||||
onClick={disableMFA}></SettingsButton>
|
||||
</div>
|
||||
)}
|
||||
{ (state.mfAuthSet && !state.mfaAuthEnabled) && (
|
||||
<div className="settings">
|
||||
<SettingsButton type="text" size="small" icon={<LockOutlined />}
|
||||
onClick={actions.enableMFA}></SettingsButton>
|
||||
</div>
|
||||
)}
|
||||
<div className="settings">
|
||||
<SettingsButton type="text" size="small" icon={<LogoutOutlined />}
|
||||
onClick={() => actions.logout()}></SettingsButton>
|
||||
@ -63,6 +93,22 @@ export function Dashboard() {
|
||||
onClick={() => actions.setShowSettings(true)}></SettingsButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{ (state.mfAuthSet && state.mfaAuthEnabled) && (
|
||||
<div className="settings">
|
||||
<Tooltip placement="topRight" title={state.strings.disableMultifactor}>
|
||||
<SettingsButton type="text" size="small" icon={<LockOutlined />}
|
||||
onClick={disableMFA}></SettingsButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{ (state.mfAuthSet && !state.mfaAuthEnabled) && (
|
||||
<div className="settings">
|
||||
<Tooltip placement="topRight" title={state.strings.enableMultifactor}>
|
||||
<SettingsButton type="text" size="small" icon={<UnlockOutlined />}
|
||||
onClick={actions.enableMFA}></SettingsButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<div className="settings">
|
||||
<Tooltip placement="topRight" title={state.strings.logout}>
|
||||
<SettingsButton type="text" size="small" icon={<LogoutOutlined />}
|
||||
|
@ -8,7 +8,10 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { AppContext } from 'context/AppContext';
|
||||
import { SettingsContext } from 'context/SettingsContext';
|
||||
|
||||
export function useDashboard() {
|
||||
import { getAdminMFAuth } from 'api/getAdminMFAuth';
|
||||
|
||||
|
||||
export function useDashboard(token) {
|
||||
|
||||
const [state, setState] = useState({
|
||||
domain: "",
|
||||
@ -38,6 +41,12 @@ export function useDashboard() {
|
||||
colors: {},
|
||||
menuStyle: {},
|
||||
strings: {},
|
||||
|
||||
mfAuthSet: false,
|
||||
mfAuthEnabled: false,
|
||||
mfAuthSecretText: null,
|
||||
mfAuthSecretImage: null,
|
||||
mfaAuthError: null,
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
@ -140,6 +149,10 @@ export function useDashboard() {
|
||||
await syncConfig();
|
||||
await syncAccounts();
|
||||
},
|
||||
enableMFA: async () => {
|
||||
},
|
||||
disableMFA: async () => {
|
||||
},
|
||||
setSettings: async () => {
|
||||
if (!state.busy) {
|
||||
updateState({ busy: true });
|
||||
@ -161,10 +174,11 @@ export function useDashboard() {
|
||||
|
||||
const syncConfig = async () => {
|
||||
try {
|
||||
const enabled = await getAdminMFAuth(app.state.adminToken);
|
||||
const config = await getNodeConfig(app.state.adminToken);
|
||||
const { accountStorage, domain, keyType, pushSupported, transformSupported, allowUnsealed, enableImage, enableAudio, enableVideo, enableBinary, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit } = config;
|
||||
const storage = Math.ceil(accountStorage / 1073741824);
|
||||
updateState({ configError: false, domain, accountStorage: storage, keyType, enableImage, enableAudio, enableVideo, enableBinary, pushSupported, transformSupported, allowUnsealed, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit });
|
||||
updateState({ mfAuthSet: true, mfaAuthEnabled: enabled, configError: false, domain, accountStorage: storage, keyType, enableImage, enableAudio, enableVideo, enableBinary, pushSupported, transformSupported, allowUnsealed, enableIce, iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit });
|
||||
}
|
||||
catch(err) {
|
||||
console.log(err);
|
||||
|
Loading…
Reference in New Issue
Block a user