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()}
+ } size="small"
+ onClick={() => onClipboard(accessLink())}
+ />
+
+
);
}
diff --git a/net/web/src/Admin/Dashboard/AccountItem/AccountItem.styled.js b/net/web/src/Admin/Dashboard/AccountItem/AccountItem.styled.js
index da814787..072287ca 100644
--- a/net/web/src/Admin/Dashboard/AccountItem/AccountItem.styled.js
+++ b/net/web/src/Admin/Dashboard/AccountItem/AccountItem.styled.js
@@ -1,4 +1,4 @@
-import { Button } from 'antd';
+import { Space, Button } from 'antd';
import styled from 'styled-components';
export const AccountItemWrapper = styled.div`
@@ -74,3 +74,7 @@ export const ResetButton = styled(Button)`
export const DeleteButton = styled(Button)`
color: red;
`
+
+export const AccessLayout = styled(Space)`
+ white-space: nowrap;
+`
diff --git a/net/web/src/Admin/Dashboard/AccountItem/useAccountItem.hook.js b/net/web/src/Admin/Dashboard/AccountItem/useAccountItem.hook.js
index cc453933..81700fa2 100644
--- a/net/web/src/Admin/Dashboard/AccountItem/useAccountItem.hook.js
+++ b/net/web/src/Admin/Dashboard/AccountItem/useAccountItem.hook.js
@@ -1,11 +1,14 @@
import { useContext, useState, useEffect } from 'react';
import { getAccountImageUrl } from 'api/getAccountImageUrl';
import { setAccountStatus } from 'api/setAccountStatus';
+import { addAccountAccess } from 'api/addAccountAccess';
-export function useAccountItem(token, item) {
+export function useAccountItem(token, item, remove) {
const [state, setState] = useState({
statusBusy: false,
+ removeBusy: false,
+ showAccess: false,
});
const updateState = (value) => {
@@ -25,6 +28,26 @@ export function useAccountItem(token, item) {
}, [token, item]);
const actions = {
+ setAccessLink: async () => {
+ let access = await addAccountAccess(token, item.accountId);
+ updateState({ accessToken: access, showAccess: true });
+ },
+ setShowAccess: (showAccess) => {
+ updateState({ showAccess });
+ },
+ remove: async () => {
+ if (!state.removeBusy) {
+ updateState({ removeBusy: true });
+ try {
+ await remove(state.accountId);
+ }
+ catch(err) {
+ console.log(err);
+ window.alert(err);
+ }
+ updateState({ removeBusy: false });
+ }
+ },
setStatus: async (disabled) => {
if (!state.statusBusy) {
updateState({ statusBusy: true });
diff --git a/net/web/src/Admin/Dashboard/Dashboard.jsx b/net/web/src/Admin/Dashboard/Dashboard.jsx
index 5a2bc670..a200d73f 100644
--- a/net/web/src/Admin/Dashboard/Dashboard.jsx
+++ b/net/web/src/Admin/Dashboard/Dashboard.jsx
@@ -33,7 +33,7 @@ export function Dashboard({ token, config, logout }) {
@@ -45,7 +45,8 @@ export function Dashboard({ token, config, logout }) {
itemLayout="horizontal"
dataSource={state.accounts}
loading={state.loading}
- renderItem={item => ()}
+ renderItem={item => ()}
/>
diff --git a/net/web/src/Admin/Dashboard/Dashboard.styled.js b/net/web/src/Admin/Dashboard/Dashboard.styled.js
index fdbceef9..fa7339be 100644
--- a/net/web/src/Admin/Dashboard/Dashboard.styled.js
+++ b/net/web/src/Admin/Dashboard/Dashboard.styled.js
@@ -13,7 +13,9 @@ export const DashboardWrapper = styled.div`
background-color: #ffffff;
display: flex;
flex-direction: column;
- padding: 16px;
+ padding-top: 16px;
+ padding-left: 16px;
+ padding-right: 16px;
border-radius: 4px;
min-width: 800px;
max-width: 900px;
@@ -25,13 +27,15 @@ export const DashboardWrapper = styled.div`
display: flex;
flex-direction: row;
font-size: 20px;
- border-bottom: 1px solid #444444;
+ border-bottom: 1px solid #aaaaaa;
}
.body {
padding-top: 8px;
min-height: 0;
overflow: auto;
+ border-bottom: 1px solid #aaaaaa;
+ margin-bottom: 16px;
}
.label {
diff --git a/net/web/src/Admin/Dashboard/useDashboard.hook.js b/net/web/src/Admin/Dashboard/useDashboard.hook.js
index b95f84db..219d3e81 100644
--- a/net/web/src/Admin/Dashboard/useDashboard.hook.js
+++ b/net/web/src/Admin/Dashboard/useDashboard.hook.js
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import { setNodeConfig } from 'api/setNodeConfig';
import { getNodeAccounts } from 'api/getNodeAccounts';
+import { removeAccount } from 'api/removeAccount';
export function useDashboard(token, config) {
@@ -18,6 +19,10 @@ export function useDashboard(token, config) {
}
const actions = {
+ removeAccount: async (accountId) => {
+ await removeAccount(token, accountId);
+ actions.getAccounts();
+ },
setHost: (value) => {
updateState({ host: value });
},
diff --git a/net/web/src/Login/useLogin.hook.js b/net/web/src/Login/useLogin.hook.js
index 1da66997..5d534b97 100644
--- a/net/web/src/Login/useLogin.hook.js
+++ b/net/web/src/Login/useLogin.hook.js
@@ -1,6 +1,6 @@
import { useContext, useState, useEffect } from 'react';
import { AppContext } from 'context/AppContext';
-import { useNavigate } from "react-router-dom";
+import { useNavigate, useLocation, useParams } from "react-router-dom";
export function useLogin() {
@@ -12,6 +12,7 @@ export function useLogin() {
});
const navigate = useNavigate();
+ const { search } = useLocation();
const app = useContext(AppContext);
const actions = {
@@ -42,6 +43,16 @@ export function useLogin() {
actions.updateState({ spinning: false })
}
},
+ onAccess: async (token) => {
+ actions.updateState({ spinning: true })
+ try {
+ await app.actions.access(token)
+ }
+ catch (err) {
+ window.alert(err);
+ }
+ actions.updateState({ spinning: false })
+ },
onCreate: () => {
navigate('/create')
},
@@ -56,8 +67,12 @@ export function useLogin() {
if (app.state.access === 'user') {
navigate('/user')
}
- if (app.state.access === 'admin') {
- navigate('/admin')
+ else {
+ let params = new URLSearchParams(search);
+ let token = params.get("access");
+ if (token) {
+ actions.onAccess(token);
+ }
}
}
if (app.actions && app.actions.available) {
diff --git a/net/web/src/api/addAccountAccess.js b/net/web/src/api/addAccountAccess.js
new file mode 100644
index 00000000..d0494332
--- /dev/null
+++ b/net/web/src/api/addAccountAccess.js
@@ -0,0 +1,8 @@
+import { checkResponse, fetchWithTimeout } from './fetchUtil';
+
+export async function addAccountAccess(token, accountId) {
+ let access = await fetchWithTimeout(`/admin/accounts/${accountId}/auth?token=${token}`, { method: 'POST' })
+ checkResponse(access);
+ return await access.json()
+}
+
diff --git a/net/web/src/api/removeAccount.js b/net/web/src/api/removeAccount.js
new file mode 100644
index 00000000..ff45a8b1
--- /dev/null
+++ b/net/web/src/api/removeAccount.js
@@ -0,0 +1,7 @@
+import { checkResponse, fetchWithTimeout } from './fetchUtil';
+
+export async function removeAccount(token, accountId) {
+ let res = await fetchWithTimeout(`/admin/accounts/${accountId}?token=${token}`, { method: 'DELETE' })
+ checkResponse(res);
+}
+
diff --git a/net/web/src/api/setAccountAccess.js b/net/web/src/api/setAccountAccess.js
new file mode 100644
index 00000000..48a947d6
--- /dev/null
+++ b/net/web/src/api/setAccountAccess.js
@@ -0,0 +1,9 @@
+import { checkResponse, fetchWithTimeout } from './fetchUtil';
+
+export async function setAccountAccess(token) {
+ let app = { Name: "indicom", Description: "decentralized communication" }
+ let access = await fetchWithTimeout(`/account/access?token=${token}`, { method: 'PUT', body: JSON.stringify(app) })
+ checkResponse(access)
+ return await access.json()
+}
+
diff --git a/net/web/src/context/useAppContext.hook.js b/net/web/src/context/useAppContext.hook.js
index ece0bf9d..18644ecf 100644
--- a/net/web/src/context/useAppContext.hook.js
+++ b/net/web/src/context/useAppContext.hook.js
@@ -1,5 +1,7 @@
import { useEffect, useState, useRef, useContext } from 'react';
+import { useNavigate, useLocation, useParams } from "react-router-dom";
import { getAvailable, getUsername, setLogin, createAccount } from './fetchUtil';
+import { setAccountAccess } from 'api/setAccountAccess';
import { AccountContext } from './AccountContext';
import { ProfileContext } from './ProfileContext';
import { ArticleContext } from './ArticleContext';
@@ -22,6 +24,13 @@ async function appLogin(username, password, updateState, setWebsocket) {
localStorage.setItem("session", JSON.stringify({ token: access, access: 'user' }));
}
+async function appAccess(token, updateState, setWebsocket) {
+ let access = await setAccountAccess(token)
+ updateState({ token: access, access: 'user' });
+ setWebsocket(access)
+ localStorage.setItem("session", JSON.stringify({ token: access, access: 'user' }));
+}
+
function appLogout(updateState, clearWebsocket) {
updateState({ token: null, access: null });
clearWebsocket()
@@ -80,6 +89,9 @@ export function useAppContext() {
}
const accessActions = {
+ access: async (token) => {
+ await appAccess(token, updateState, setWebsocket)
+ },
login: async (username, password) => {
await appLogin(username, password, updateState, setWebsocket)
},
diff --git a/net/web/src/context/useGroupContext.hook.js b/net/web/src/context/useGroupContext.hook.js
index 5e8d991f..1b5c2234 100644
--- a/net/web/src/context/useGroupContext.hook.js
+++ b/net/web/src/context/useGroupContext.hook.js
@@ -19,10 +19,10 @@ export function useGroupContext() {
let delta = await getGroups(access.current, revision.current);
for (let group of delta) {
if (group.data) {
- groups.set(group.id, group);
+ groups.current.set(group.id, group);
}
else {
- groups.delete(group.id);
+ groups.current.delete(group.id);
}
}
}