diff --git a/net/server/internal/api_addChannelTopicAsset.go b/net/server/internal/api_addChannelTopicAsset.go index 42220a33..5fd39c86 100644 --- a/net/server/internal/api_addChannelTopicAsset.go +++ b/net/server/internal/api_addChannelTopicAsset.go @@ -68,6 +68,8 @@ func AddChannelTopicAsset(w http.ResponseWriter, r *http.Request) { garbageSync.Lock() defer garbageSync.Unlock() +PrintMsg("SAVE NEW ASSET"); + // save new file id := uuid.New().String() path := getStrConfigValue(CONFIG_ASSETPATH, APP_DEFAULTPATH) + "/" + channelSlot.Account.Guid + "/" + id diff --git a/net/server/internal/api_setAccountLogin.go b/net/server/internal/api_setAccountLogin.go new file mode 100644 index 00000000..cc59078f --- /dev/null +++ b/net/server/internal/api_setAccountLogin.go @@ -0,0 +1,46 @@ +package databag + +import ( + "net/http" + "gorm.io/gorm" + "databag/internal/store" +) + +func SetAccountLogin(w http.ResponseWriter, r *http.Request) { + + account, code, err := ParamAgentToken(r, true); + if err != nil { + ErrResponse(w, code, err) + return + } + + username, password, ret := BasicCredentials(r); + if ret != nil { + ErrResponse(w, http.StatusUnauthorized, ret) + return + } + + err = store.DB.Transaction(func(tx *gorm.DB) error { + if res := tx.Model(&account).Update("account_revision", account.AccountRevision + 1).Error; res != nil { + return res + } + if res := tx.Model(&account).Update("profile_revision", account.AccountRevision + 1).Error; res != nil { + return res + } + if res := tx.Model(&account).Update("Username", username).Error; res != nil { + return res + } + if res := tx.Model(&account).Update("Password", password).Error; res != nil { + return res + } + return nil + }) + if err != nil { + ErrResponse(w, http.StatusInternalServerError, err) + return + } + + SetStatus(account) + WriteResponse(w, nil) +} + diff --git a/net/server/internal/routers.go b/net/server/internal/routers.go index fd86ba45..44ef51b4 100644 --- a/net/server/internal/routers.go +++ b/net/server/internal/routers.go @@ -165,6 +165,13 @@ var routes = Routes{ SetAccountNode, }, + Route{ + "SetAccountLogin", + strings.ToUpper("Put"), + "/account/login", + SetAccountLogin, + }, + Route{ "SetAccountSerchable", strings.ToUpper("Put"), diff --git a/net/web/src/User/SideBar/Identity/Identity.jsx b/net/web/src/User/SideBar/Identity/Identity.jsx index 7eb4c5f3..b79aa4fc 100644 --- a/net/web/src/User/SideBar/Identity/Identity.jsx +++ b/net/web/src/User/SideBar/Identity/Identity.jsx @@ -1,13 +1,14 @@ -import { Avatar, Image } from 'antd'; -import React from 'react' +import { Avatar, Space, Image, Modal, Form, Input } from 'antd'; +import React, { useState } from 'react' import { IdentityWrapper, IdentityDropdown, MenuWrapper } from './Identity.styled'; -import { RightOutlined, EditOutlined, UserOutlined } from '@ant-design/icons'; +import { RightOutlined, EditOutlined, UserOutlined, LockOutlined } from '@ant-design/icons'; import { useIdentity } from './useIdentity.hook'; import { Menu, Dropdown } from 'antd'; import { Logo } from '../../../Logo/Logo'; export function Identity() { + const [ showLogin, setShowLogin ] = useState(false); const { state, actions } = useIdentity() const menu = ( @@ -16,7 +17,7 @@ export function Identity() {
actions.editProfile()}>Edit Profile
-
Change Login
+
setShowLogin(true)}>Change Login
actions.logout()}>Sign Out
@@ -24,6 +25,11 @@ export function Identity() { ); + const onChangeLogin = () => { + let saved = actions.setLogin(); + setShowLogin(false); + }; + return ( @@ -37,7 +43,24 @@ export function Identity() { - + + onChangeLogin()} onCancel={() => setShowLogin(false)}> + + + } + onChange={(e) => actions.setUsername(e.target.value)} defaultValue={state.handle} + addonAfter={state.usernameStatus} /> + + } + onChange={(e) => actions.setPassword(e.target.value)} + addonAfter={state.passwordStatus} /> + + } + onChange={(e) => actions.setConfirm(e.target.value)} + addonAfter={state.confirmStatus} /> + + ) } diff --git a/net/web/src/User/SideBar/Identity/useIdentity.hook.js b/net/web/src/User/SideBar/Identity/useIdentity.hook.js index c63ae5ff..ba90d695 100644 --- a/net/web/src/User/SideBar/Identity/useIdentity.hook.js +++ b/net/web/src/User/SideBar/Identity/useIdentity.hook.js @@ -1,7 +1,9 @@ -import { useContext, useState, useEffect } from 'react'; +import { useContext, useState, useRef, useEffect } from 'react'; import { AppContext } from 'context/AppContext'; import { ProfileContext } from 'context/ProfileContext'; +import { AccountContext } from 'context/AccountContext'; import { useNavigate } from "react-router-dom"; +import { getUsername } from 'api/getUsername'; export function useIdentity() { @@ -11,11 +13,21 @@ export function useIdentity() { domain: '', imageUrl: null, image: null, + + username: null, + usernameStatus: null, + password: null, + passwordStatus: null, + confirm: null, + confirmStatus: null, + }); const navigate = useNavigate(); const profile = useContext(ProfileContext); + const account = useContext(AccountContext); const app = useContext(AppContext); + const debounce = useRef(null); const updateState = (value) => { setState((s) => ({ ...s, ...value })); @@ -31,7 +43,56 @@ export function useIdentity() { }, editProfile: () => { navigate('/user/profile'); - } + }, + setUsername: (value) => { + if (debounce.current) { + clearTimeout(debounce.current); + } + updateState({ username: value }); + if (state.handle.toLowerCase() == value.toLowerCase() || value == null || value == '') { + updateState({ usernameStatus: null }); + return; + } + debounce.current = setTimeout(async () => { + let available = await getUsername(value); + if (available) { + updateState({ usernameStatus: null }); + } + else { + updateState({ usernameStatus: 'not available' }); + } + }, 500); + }, + setPassword: (value) => { + updateState({ password: value }); + }, + setConfirm: (value) => { + updateState({ confirm: value }); + }, + setLogin: async () => { + if (state.username == null || state.username == '') { + updateState({ usernameStatus: 'username required' }); + throw 'username required'; + } + else { + updateState({ usernameStatus: null }); + } + if (state.password == null || state.password == '') { + updateState({ passwordStatus: 'password required' }); + throw 'password required'; + } + else { + updateState({ passwordStatus: null }); + } + if (state.confirm != state.password) { + updateState({ confirmStatus: 'password mismatch' }); + throw 'password mismatch'; + } + else { + updateState({ confirmStatus: null }); + } + await account.actions.setLogin(state.username, state.password); + }, }; useEffect(() => { @@ -47,6 +108,7 @@ export function useIdentity() { updateState({ image: identity.image }); updateState({ name: identity.name }); updateState({ handle: identity.handle }); + updateState({ username: identity.handle }); updateState({ domain: identity.node }); } }, [profile]) diff --git a/net/web/src/api/getUsername.js b/net/web/src/api/getUsername.js new file mode 100644 index 00000000..e7f3b8db --- /dev/null +++ b/net/web/src/api/getUsername.js @@ -0,0 +1,8 @@ +import { checkResponse, fetchWithTimeout } from './fetchUtil'; + +export async function getUsername(name) { + let available = await fetchWithTimeout('/account/username?name=' + encodeURIComponent(name), { method: 'GET' }) + checkResponse(available) + return await available.json() +} + diff --git a/net/web/src/api/setAccountLogin.js b/net/web/src/api/setAccountLogin.js new file mode 100644 index 00000000..cb108f74 --- /dev/null +++ b/net/web/src/api/setAccountLogin.js @@ -0,0 +1,10 @@ +import { checkResponse, fetchWithTimeout } from './fetchUtil'; +var base64 = require('base-64'); + +export async function setAccountLogin(token, username, password) { + let headers = new Headers() + headers.append('Credentials', 'Basic ' + base64.encode(username + ":" + password)); + let res = await fetchWithTimeout(`/account/login?agent=${token}`, { method: 'PUT', headers }) + checkResponse(res); +} + diff --git a/net/web/src/context/useAccountContext.hook.js b/net/web/src/context/useAccountContext.hook.js index 558a6965..c62db36a 100644 --- a/net/web/src/context/useAccountContext.hook.js +++ b/net/web/src/context/useAccountContext.hook.js @@ -1,6 +1,7 @@ import { useEffect, useState, useRef } from 'react'; import { setAccountSearchable } from 'api/setAccountSearchable'; import { getAccountStatus } from 'api/getAccountStatus'; +import { setAccountLogin } from 'api/setAccountLogin'; export function useAccountContext() { const [state, setState] = useState({ @@ -47,6 +48,9 @@ export function useAccountContext() { setSearchable: async (flag) => { await setAccountSearchable(access.current, flag); }, + setLogin: async (username, password) => { + await setAccountLogin(access.current, username, password); + }, } return { state, actions }