mirror of
https://github.com/balzack/databag.git
synced 2025-02-12 03:29:16 +00:00
added magic access link
This commit is contained in:
parent
8ff747f90f
commit
74244b920d
45
doc/api.oa3
45
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
|
||||
|
49
net/server/internal/api_addNodeAccountAccess.go
Normal file
49
net/server/internal/api_addNodeAccountAccess.go
Normal file
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
71
net/server/internal/api_setAccountAccess.go
Normal file
71
net/server/internal/api_setAccountAccess.go
Normal file
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"),
|
||||
|
@ -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 (
|
||||
<Tooltip placement="topLeft" title="Enable Account">
|
||||
<EnableButton type="text" size="large" icon={<CheckCircleOutlined />}
|
||||
onClick={() => actions.setStatus(false)}></EnableButton>
|
||||
loading={state.statusBusy} onClick={() => actions.setStatus(false)}></EnableButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Tooltip placement="topLeft" title="Disable Account">
|
||||
<DisableButton type="text" size="large" icon={<CloseCircleOutlined />}
|
||||
onClick={() => actions.setStatus(true)}></DisableButton>
|
||||
loading={state.statusBusy} onClick={() => actions.setStatus(true)}></DisableButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const accessLink = () => {
|
||||
return window.location.origin + '/#/login?access=' + state.accessToken;
|
||||
};
|
||||
|
||||
return (
|
||||
<AccountItemWrapper>
|
||||
<div class="avatar">
|
||||
@ -35,14 +43,26 @@ export function AccountItem({ token, item }) {
|
||||
<div class="guid">{ state.guid }</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<Tooltip placement="topLeft" title="Reset Password">
|
||||
<ResetButton type="text" size="large" icon={<UnlockOutlined />}></ResetButton>
|
||||
<Tooltip placement="topLeft" title="Account Login Link">
|
||||
<ResetButton type="text" size="large" icon={<UnlockOutlined />}
|
||||
onClick={() => actions.setAccessLink()}></ResetButton>
|
||||
</Tooltip>
|
||||
<Enable />
|
||||
<Tooltip placement="topLeft" title="Delete Account">
|
||||
<DeleteButton type="text" size="large" icon={<UserDeleteOutlined />}></DeleteButton>
|
||||
<DeleteButton type="text" size="large" icon={<UserDeleteOutlined />}
|
||||
loading={state.removeBusy} onClick={() => actions.remove()}></DeleteButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Modal title="Access Link" visible={state.showAccess} centered width="fitContent"
|
||||
footer={[ <Button type="primary" onClick={() => actions.setShowAccess(false)}>OK</Button> ]}
|
||||
onCancel={() => actions.setShowAccess(false)}>
|
||||
<AccessLayout>
|
||||
<div>{accessLink()}</div>
|
||||
<Button icon={<CopyOutlined />} size="small"
|
||||
onClick={() => onClipboard(accessLink())}
|
||||
/>
|
||||
</AccessLayout>
|
||||
</Modal>
|
||||
</AccountItemWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
`
|
||||
|
@ -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 });
|
||||
|
@ -33,7 +33,7 @@ export function Dashboard({ token, config, logout }) {
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="add">
|
||||
<Tooltip placement="topRight" title="Add Account">
|
||||
<Tooltip placement="topRight" title="Create Account Link">
|
||||
<AddButton type="text" size="large" icon={<UserAddOutlined />}></AddButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@ -45,7 +45,8 @@ export function Dashboard({ token, config, logout }) {
|
||||
itemLayout="horizontal"
|
||||
dataSource={state.accounts}
|
||||
loading={state.loading}
|
||||
renderItem={item => (<AccountItem token={token} item={item} />)}
|
||||
renderItem={item => (<AccountItem token={token} item={item}
|
||||
remove={actions.removeAccount}/>)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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 {
|
||||
|
@ -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 });
|
||||
},
|
||||
|
@ -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) {
|
||||
|
8
net/web/src/api/addAccountAccess.js
Normal file
8
net/web/src/api/addAccountAccess.js
Normal file
@ -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()
|
||||
}
|
||||
|
7
net/web/src/api/removeAccount.js
Normal file
7
net/web/src/api/removeAccount.js
Normal file
@ -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);
|
||||
}
|
||||
|
9
net/web/src/api/setAccountAccess.js
Normal file
9
net/web/src/api/setAccountAccess.js
Normal file
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user