diff --git a/doc/api.oa3 b/doc/api.oa3 index 44747dcb..2cda523b 100644 --- a/doc/api.oa3 +++ b/doc/api.oa3 @@ -236,6 +236,33 @@ paths: description: account not found '500': description: internal server error + + /admin/accounts/{accountId}/auth: + post: + tags: + - account + description: Generate token to reset authentication. + operationId: add-account-authentication + parameters: + - name: token + in: query + description: token for admin access + required: true + schema: + type: string + responses: + '201': + description: generated + content: + application/json: + schema: + type: string + '401': + description: invalid password + '500': + description: internal server error + + /admin/accounts/{accountId}/status: put: tags: - admin @@ -581,24 +608,6 @@ paths: description: internal server error /account/auth: - post: - tags: - - account - description: Generate token to reset authentication. Access granted to account's login and password. - operationId: add-account-authentication - security: - - basicAuth: [] - responses: - '201': - description: generated - content: - application/json: - schema: - type: string - '401': - description: invalid password - '500': - description: internal server error put: tags: - account diff --git a/net/server/internal/api_addNodeAccountAccess.go b/net/server/internal/api_addNodeAccountAccess.go new file mode 100644 index 00000000..3730c0cd --- /dev/null +++ b/net/server/internal/api_addNodeAccountAccess.go @@ -0,0 +1,49 @@ +package databag + +import ( + "net/http" + "time" + "strconv" + "encoding/hex" + "databag/internal/store" + "github.com/theckman/go-securerandom" + "github.com/gorilla/mux" +) + +func AddNodeAccountAccess(w http.ResponseWriter, r *http.Request) { + + params := mux.Vars(r) + accountId, res := strconv.ParseUint(params["accountId"], 10, 32) + if res != nil { + ErrResponse(w, http.StatusBadRequest, res) + return + } + + if code, err := ParamAdminToken(r); err != nil { + ErrResponse(w, code, err) + return + } + + data, ret := securerandom.Bytes(APP_RESETSIZE) + if ret != nil { + ErrResponse(w, http.StatusInternalServerError, ret) + return + } + token := hex.EncodeToString(data) + + accountToken := store.AccountToken{ + AccountID: uint(accountId), + TokenType: APP_TOKENRESET, + Token: token, + Expires: time.Now().Unix() + APP_RESETEXPIRE, + } + if err := store.DB.Create(&accountToken).Error; err != nil { + ErrResponse(w, http.StatusInternalServerError, err) + return + } + + WriteResponse(w, token) +} + + + diff --git a/net/server/internal/api_setAccountAccess.go b/net/server/internal/api_setAccountAccess.go new file mode 100644 index 00000000..6691822a --- /dev/null +++ b/net/server/internal/api_setAccountAccess.go @@ -0,0 +1,71 @@ +package databag + +import ( + "errors" + "net/http" + "encoding/hex" + "gorm.io/gorm" + "databag/internal/store" + "github.com/theckman/go-securerandom" +) + +func SetAccountAccess(w http.ResponseWriter, r *http.Request) { + + token, _, res := AccessToken(r) + if res != nil || token.TokenType != APP_TOKENRESET { + ErrResponse(w, http.StatusUnauthorized, res) + return + } + if token.Account == nil { + ErrResponse(w, http.StatusUnauthorized, errors.New("invalid reset token")) + return + } + account := token.Account; + + // parse app data + var appData AppData + if err := ParseRequest(r, w, &appData); err != nil { + ErrResponse(w, http.StatusBadRequest, err) + return + } + + // gernate app token + data, err := securerandom.Bytes(APP_TOKENSIZE) + if err != nil { + ErrResponse(w, http.StatusInternalServerError, err) + return + } + access := hex.EncodeToString(data) + + // create app entry + app := store.App { + AccountID: account.Guid, + Name: appData.Name, + Description: appData.Description, + Image: appData.Image, + Url: appData.Url, + Token: access, + }; + + // save app and delete token + err = store.DB.Transaction(func(tx *gorm.DB) error { + if res := tx.Create(&app).Error; res != nil { + return res; + } + if res := tx.Save(token.Account).Error; res != nil { + return res + } + if res := tx.Delete(token).Error; res != nil { + return res + } + return nil; + }); + if err != nil { + ErrResponse(w, http.StatusInternalServerError, err) + return + } + + WriteResponse(w, account.Guid + "." + access) +} + + diff --git a/net/server/internal/authUtil.go b/net/server/internal/authUtil.go index 3b2a5d0d..c84bca85 100644 --- a/net/server/internal/authUtil.go +++ b/net/server/internal/authUtil.go @@ -50,6 +50,25 @@ func BearerAccountToken(r *http.Request) (*store.AccountToken, error) { return &accountToken, nil } +func AccessToken(r *http.Request) (*store.AccountToken, int, error) { + + // parse authentication token + token := r.FormValue("token") + if token == "" { + return nil, http.StatusUnauthorized, errors.New("token not set"); + } + + // find token record + var accountToken store.AccountToken + if err := store.DB.Preload("Account").Where("token = ?", token).First(&accountToken).Error; err != nil { + return nil, http.StatusUnauthorized, err + } + if accountToken.Expires < time.Now().Unix() { + return nil, http.StatusUnauthorized, errors.New("expired token") + } + return &accountToken, http.StatusOK, nil +} + func ParamAdminToken(r *http.Request) (int, error) { // parse authentication token diff --git a/net/server/internal/routers.go b/net/server/internal/routers.go index af3235a4..cd8261dd 100644 --- a/net/server/internal/routers.go +++ b/net/server/internal/routers.go @@ -144,6 +144,13 @@ var routes = Routes{ RemoveAccountApp, }, + Route{ + "SetAccountAccess", + strings.ToUpper("Put"), + "/account/access", + SetAccountAccess, + }, + Route{ "SetAccountAuthentication", strings.ToUpper("Put"), @@ -200,6 +207,13 @@ var routes = Routes{ SetNodeAccountStatus, }, + Route{ + "AddNodeAccountAccess", + strings.ToUpper("Post"), + "/admin/accounts/{accountId}/auth", + AddNodeAccountAccess, + }, + Route{ "GetNodeAccounts", strings.ToUpper("Get"), diff --git a/net/web/src/Admin/Dashboard/AccountItem/AccountItem.jsx b/net/web/src/Admin/Dashboard/AccountItem/AccountItem.jsx index 3bb9e271..38ef868a 100644 --- a/net/web/src/Admin/Dashboard/AccountItem/AccountItem.jsx +++ b/net/web/src/Admin/Dashboard/AccountItem/AccountItem.jsx @@ -1,30 +1,38 @@ import { Avatar } from 'avatar/Avatar'; -import { AccountItemWrapper, DeleteButton, EnableButton, DisableButton, ResetButton } from './AccountItem.styled'; +import { AccountItemWrapper, AccessLayout, DeleteButton, EnableButton, DisableButton, ResetButton } from './AccountItem.styled'; import { useAccountItem } from './useAccountItem.hook'; -import { UserDeleteOutlined, UnlockOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons'; -import { Tooltip } from 'antd'; +import { CopyOutlined, UserDeleteOutlined, UnlockOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons'; +import { Modal, Tooltip, Button } from 'antd'; -export function AccountItem({ token, item }) { +export function AccountItem({ token, item, remove }) { - const { state, actions } = useAccountItem(token, item); + const { state, actions } = useAccountItem(token, item, remove); + + const onClipboard = (value) => { + navigator.clipboard.writeText(value); + }; const Enable = () => { if (state.disabled) { return ( } - onClick={() => actions.setStatus(false)}> + loading={state.statusBusy} onClick={() => actions.setStatus(false)}> ) } return ( } - onClick={() => actions.setStatus(true)}> + loading={state.statusBusy} onClick={() => actions.setStatus(true)}> ) } + const accessLink = () => { + return window.location.origin + '/#/login?access=' + state.accessToken; + }; + return (
@@ -35,14 +43,26 @@ export function AccountItem({ token, item }) {
{ state.guid }
- - }> + + } + onClick={() => actions.setAccessLink()}> - }> + } + loading={state.removeBusy} onClick={() => actions.remove()}>
+ actions.setShowAccess(false)}>OK ]} + onCancel={() => actions.setShowAccess(false)}> + +
{accessLink()}
+