diff --git a/common/utils.go b/common/utils.go index 6eb3a0fe..4892404c 100644 --- a/common/utils.go +++ b/common/utils.go @@ -10,6 +10,7 @@ import ( "runtime" "strconv" "strings" + "time" ) func OpenBrowser(url string) { @@ -132,6 +133,10 @@ func GetUUID() string { return code } +func GetTimestamp() int64 { + return time.Now().Unix() +} + func Max(a int, b int) int { if a >= b { return a diff --git a/controller/token.go b/controller/token.go index 094235c2..e8433b69 100644 --- a/controller/token.go +++ b/controller/token.go @@ -51,6 +51,7 @@ func SearchTokens(c *gin.Context) { func GetToken(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) + userId := c.GetInt("id") if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -58,7 +59,7 @@ func GetToken(c *gin.Context) { }) return } - token, err := model.GetTokenById(id) + token, err := model.GetTokenByIds(id, userId) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -84,7 +85,21 @@ func AddToken(c *gin.Context) { }) return } - err = token.Insert() + if len(token.Name) == 0 || len(token.Name) > 20 { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "令牌名称长度必须在1-20之间", + }) + return + } + cleanToken := model.Token{ + UserId: c.GetInt("id"), + Name: token.Name, + Key: common.GetUUID(), + CreatedTime: common.GetTimestamp(), + AccessedTime: common.GetTimestamp(), + } + err = cleanToken.Insert() if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -101,8 +116,8 @@ func AddToken(c *gin.Context) { func DeleteToken(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) - token := model.Token{Id: id} - err := token.Delete() + userId := c.GetInt("id") + err := model.DeleteTokenById(id, userId) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -118,6 +133,7 @@ func DeleteToken(c *gin.Context) { } func UpdateToken(c *gin.Context) { + userId := c.GetInt("id") token := model.Token{} err := c.ShouldBindJSON(&token) if err != nil { @@ -127,7 +143,17 @@ func UpdateToken(c *gin.Context) { }) return } - err = token.Update() + cleanToken, err := model.GetTokenByIds(token.Id, userId) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + cleanToken.Name = token.Name + cleanToken.Status = token.Status + err = cleanToken.Update() if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -138,6 +164,7 @@ func UpdateToken(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", + "data": cleanToken, }) return } diff --git a/model/main.go b/model/main.go index 563835cb..608cf6f3 100644 --- a/model/main.go +++ b/model/main.go @@ -56,6 +56,10 @@ func InitDB() (err error) { if err != nil { return err } + err = db.AutoMigrate(&Token{}) + if err != nil { + return err + } err = db.AutoMigrate(&User{}) if err != nil { return err diff --git a/model/token.go b/model/token.go index 0cefd02c..cc9e3976 100644 --- a/model/token.go +++ b/model/token.go @@ -1,6 +1,7 @@ package model import ( + "errors" _ "gorm.io/driver/sqlite" ) @@ -9,7 +10,7 @@ type Token struct { UserId int `json:"user_id"` Key string `json:"key"` Status int `json:"status" gorm:"default:1"` - Name string `json:"name" gorm:"unique;index"` + Name string `json:"name" gorm:"index" ` CreatedTime int64 `json:"created_time" gorm:"bigint"` AccessedTime int64 `json:"accessed_time" gorm:"bigint"` } @@ -17,19 +18,22 @@ type Token struct { func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) { var tokens []*Token var err error - err = DB.Where("userId = ?", userId).Order("id desc").Limit(num).Offset(startIdx).Omit("key").Find(&tokens).Error + err = DB.Where("user_id = ?", userId).Order("id desc").Limit(num).Offset(startIdx).Find(&tokens).Error return tokens, err } func SearchUserTokens(userId int, keyword string) (tokens []*Token, err error) { - err = DB.Where("userId = ?", userId).Omit("key").Where("id = ? or name LIKE ?", keyword, keyword+"%").Find(&tokens).Error + err = DB.Where("user_id = ?", userId).Where("id = ? or name LIKE ?", keyword, keyword+"%").Find(&tokens).Error return tokens, err } -func GetTokenById(id int) (*Token, error) { - token := Token{Id: id} +func GetTokenByIds(id int, userId int) (*Token, error) { + if id == 0 || userId == 0 { + return nil, errors.New("id 或 userId 为空!") + } + token := Token{Id: id, UserId: userId} var err error = nil - err = DB.Omit("key").Select([]string{"id", "type"}).First(&token, "id = ?", id).Error + err = DB.First(&token, "id = ? and user_id = ?", id, userId).Error return &token, err } @@ -50,3 +54,16 @@ func (token *Token) Delete() error { err = DB.Delete(token).Error return err } + +func DeleteTokenById(id int, userId int) (err error) { + // Why we need userId here? In case user want to delete other's token. + if id == 0 || userId == 0 { + return errors.New("id 或 userId 为空!") + } + token := Token{Id: id, UserId: userId} + err = DB.Where(token).First(&token).Error + if err != nil { + return err + } + return token.Delete() +} diff --git a/web/src/App.js b/web/src/App.js index 673ec496..89ca42aa 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -16,6 +16,10 @@ import PasswordResetConfirm from './components/PasswordResetConfirm'; import { UserContext } from './context/User'; import Channel from './pages/Channel'; import Token from './pages/Token'; +import EditToken from './pages/Token/EditToken'; +import AddToken from './pages/Token/AddToken'; +import EditChannel from './pages/Channel/EditChannel'; +import AddChannel from './pages/Channel/AddChannel'; const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); @@ -73,12 +77,44 @@ function App() { } /> + }> + + + } + /> + }> + + + } + /> } /> + }> + + + } + /> + }> + + + } + /> 普通用户; - case 10: - return ; - case 100: - return ; - default: - return ; - } +function renderTimestamp(timestamp) { + return ( + <> + {timestamp2string(timestamp)} + + ); } const TokensTable = () => { - const [users, setUsers] = useState([]); + const [tokens, setTokens] = useState([]); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); const [searchKeyword, setSearchKeyword] = useState(''); const [searching, setSearching] = useState(false); - const loadUsers = async (startIdx) => { - const res = await API.get(`/api/user/?p=${startIdx}`); + const loadTokens = async (startIdx) => { + const res = await API.get(`/api/token/?p=${startIdx}`); const { success, message, data } = res.data; if (success) { if (startIdx === 0) { - setUsers(data); + setTokens(data); } else { - let newUsers = users; - newUsers.push(...data); - setUsers(newUsers); + let newTokens = tokens; + newTokens.push(...data); + setTokens(newTokens); } } else { showError(message); @@ -44,55 +39,63 @@ const TokensTable = () => { const onPaginationChange = (e, { activePage }) => { (async () => { - if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) { + if (activePage === Math.ceil(tokens.length / ITEMS_PER_PAGE) + 1) { // In this case we have to load more data and then append them. - await loadUsers(activePage - 1); + await loadTokens(activePage - 1); } setActivePage(activePage); })(); }; useEffect(() => { - loadUsers(0) + loadTokens(0) .then() .catch((reason) => { showError(reason); }); }, []); - const manageUser = (username, action, idx) => { - (async () => { - const res = await API.post('/api/user/manage', { - username, - action, - }); - const { success, message } = res.data; - if (success) { - showSuccess('操作成功完成!'); - let user = res.data.data; - let newUsers = [...users]; - let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; - if (action === 'delete') { - newUsers[realIdx].deleted = true; - } else { - newUsers[realIdx].status = user.status; - newUsers[realIdx].role = user.role; - } - setUsers(newUsers); + const manageToken = async (id, action, idx) => { + let data = { id }; + let res; + switch (action) { + case 'delete': + res = await API.delete(`/api/token/${id}/`); + break; + case 'enable': + data.status = 1; + res = await API.put('/api/token/', data); + break; + case 'disable': + data.status = 2; + res = await API.put('/api/token/', data); + break; + } + const { success, message } = res.data; + if (success) { + showSuccess('操作成功完成!'); + let token = res.data.data; + let newTokens = [...tokens]; + let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; + if (action === 'delete') { + newTokens[realIdx].deleted = true; } else { - showError(message); + newTokens[realIdx].status = token.status; } - })(); + setTokens(newTokens); + } else { + showError(message); + } }; const renderStatus = (status) => { switch (status) { case 1: - return ; + return ; case 2: return ( ); default: @@ -104,18 +107,18 @@ const TokensTable = () => { } }; - const searchUsers = async () => { + const searchTokens = async () => { if (searchKeyword === '') { // if keyword is blank, load files instead. - await loadUsers(0); + await loadTokens(0); setActivePage(1); return; } setSearching(true); - const res = await API.get(`/api/user/search?keyword=${searchKeyword}`); + const res = await API.get(`/api/token/search?keyword=${searchKeyword}/`); const { success, message, data } = res.data; if (success) { - setUsers(data); + setTokens(data); setActivePage(1); } else { showError(message); @@ -127,28 +130,28 @@ const TokensTable = () => { setSearchKeyword(value.trim()); }; - const sortUser = (key) => { - if (users.length === 0) return; + const sortToken = (key) => { + if (tokens.length === 0) return; setLoading(true); - let sortedUsers = [...users]; - sortedUsers.sort((a, b) => { + let sortedTokens = [...tokens]; + sortedTokens.sort((a, b) => { return ('' + a[key]).localeCompare(b[key]); }); - if (sortedUsers[0].id === users[0].id) { - sortedUsers.reverse(); + if (sortedTokens[0].id === tokens[0].id) { + sortedTokens.reverse(); } - setUsers(sortedUsers); + setTokens(sortedTokens); setLoading(false); }; return ( <> -
+ { { - sortUser('username'); + sortToken('id'); }} > - 用户名 + ID { - sortUser('display_name'); + sortToken('name'); }} > - 显示名称 + 名称 { - sortUser('email'); - }} - > - 邮箱地址 - - { - sortUser('role'); - }} - > - 用户角色 - - { - sortUser('status'); + sortToken('status'); }} > 状态 + { + sortToken('created_time'); + }} + > + 创建时间 + + { + sortToken('accessed_time'); + }} + > + 访问时间 + 操作 - {users + {tokens .slice( (activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE ) - .map((user, idx) => { - if (user.deleted) return <>; + .map((token, idx) => { + if (token.deleted) return <>; return ( - - {user.username} - {user.display_name} - {user.email ? user.email : '无'} - {renderRole(user.role)} - {renderStatus(user.status)} + + {token.id} + {token.name ? token.name : '无'} + {renderStatus(token.status)} + {renderTimestamp(token.created_time)} + {renderTimestamp(token.accessed_time)}
- @@ -275,8 +273,8 @@ const TokensTable = () => { - { size='small' siblingRange={1} totalPages={ - Math.ceil(users.length / ITEMS_PER_PAGE) + - (users.length % ITEMS_PER_PAGE === 0 ? 1 : 0) + Math.ceil(tokens.length / ITEMS_PER_PAGE) + + (tokens.length % ITEMS_PER_PAGE === 0 ? 1 : 0) } /> diff --git a/web/src/helpers/utils.js b/web/src/helpers/utils.js index 6203392b..1a555462 100644 --- a/web/src/helpers/utils.js +++ b/web/src/helpers/utils.js @@ -97,3 +97,41 @@ export function removeTrailingSlash(url) { return url; } } + +export function timestamp2string(timestamp) { + let date = new Date(timestamp * 1000); + let year = date.getFullYear().toString(); + let month = (date.getMonth() + 1).toString(); + let day = date.getDate().toString(); + let hour = date.getHours().toString(); + let minute = date.getMinutes().toString(); + let second = date.getSeconds().toString(); + if (month.length === 1) { + month = '0' + month; + } + if (day.length === 1) { + day = '0' + day; + } + if (hour.length === 1) { + hour = '0' + hour; + } + if (minute.length === 1) { + minute = '0' + minute; + } + if (second.length === 1) { + second = '0' + second; + } + return ( + year + + '-' + + month + + '-' + + day + + ' ' + + hour + + ':' + + minute + + ':' + + second + ); +} \ No newline at end of file diff --git a/web/src/pages/Channel/AddChannel.js b/web/src/pages/Channel/AddChannel.js new file mode 100644 index 00000000..1930eb75 --- /dev/null +++ b/web/src/pages/Channel/AddChannel.js @@ -0,0 +1,77 @@ +import React, { useState } from 'react'; +import { Button, Form, Header, Segment } from 'semantic-ui-react'; +import { API, showError, showSuccess } from '../../helpers'; + +const AddChannel = () => { + const originInputs = { + username: '', + display_name: '', + password: '', + }; + const [inputs, setInputs] = useState(originInputs); + const { username, display_name, password } = inputs; + + const handleInputChange = (e, { name, value }) => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + }; + + const submit = async () => { + if (inputs.username === '' || inputs.password === '') return; + const res = await API.post(`/api/user/`, inputs); + const { success, message } = res.data; + if (success) { + showSuccess('用户账户创建成功!'); + setInputs(originInputs); + } else { + showError(message); + } + }; + + return ( + <> + +
创建新用户账户
+ + + + + + + + + + + + +
+ + ); +}; + +export default AddChannel; diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js new file mode 100644 index 00000000..d4faea6b --- /dev/null +++ b/web/src/pages/Channel/EditChannel.js @@ -0,0 +1,132 @@ +import React, { useEffect, useState } from 'react'; +import { Button, Form, Header, Segment } from 'semantic-ui-react'; +import { useParams } from 'react-router-dom'; +import { API, showError, showSuccess } from '../../helpers'; + +const EditChannel = () => { + const params = useParams(); + const userId = params.id; + const [loading, setLoading] = useState(true); + const [inputs, setInputs] = useState({ + username: '', + display_name: '', + password: '', + github_id: '', + wechat_id: '', + email: '', + }); + const { username, display_name, password, github_id, wechat_id, email } = + inputs; + const handleInputChange = (e, { name, value }) => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + }; + + const loadUser = async () => { + let res = undefined; + if (userId) { + res = await API.get(`/api/user/${userId}`); + } else { + res = await API.get(`/api/user/self`); + } + const { success, message, data } = res.data; + if (success) { + data.password = ''; + setInputs(data); + } else { + showError(message); + } + setLoading(false); + }; + useEffect(() => { + loadUser().then(); + }, []); + + const submit = async () => { + let res = undefined; + if (userId) { + res = await API.put(`/api/user/`, { ...inputs, id: parseInt(userId) }); + } else { + res = await API.put(`/api/user/self`, inputs); + } + const { success, message } = res.data; + if (success) { + showSuccess('用户信息更新成功!'); + } else { + showError(message); + } + }; + + return ( + <> + +
更新用户信息
+
+ + + + + + + + + + + + + + + + + + + +
+
+ + ); +}; + +export default EditChannel; diff --git a/web/src/pages/Token/AddToken.js b/web/src/pages/Token/AddToken.js new file mode 100644 index 00000000..c2dd327b --- /dev/null +++ b/web/src/pages/Token/AddToken.js @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import { Button, Form, Header, Segment } from 'semantic-ui-react'; +import { API, showError, showSuccess } from '../../helpers'; + +const AddToken = () => { + const originInputs = { + name: '', + }; + const [inputs, setInputs] = useState(originInputs); + const { name, display_name, password } = inputs; + + const handleInputChange = (e, { name, value }) => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + }; + + const submit = async () => { + if (inputs.name === '') return; + const res = await API.post(`/api/token/`, inputs); + const { success, message } = res.data; + if (success) { + showSuccess('令牌创建成功!'); + setInputs(originInputs); + } else { + showError(message); + } + }; + + return ( + <> + +
创建新的令牌
+
+ + + + +
+
+ + ); +}; + +export default AddToken; diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js new file mode 100644 index 00000000..4d349c73 --- /dev/null +++ b/web/src/pages/Token/EditToken.js @@ -0,0 +1,65 @@ +import React, { useEffect, useState } from 'react'; +import { Button, Form, Header, Segment } from 'semantic-ui-react'; +import { useParams } from 'react-router-dom'; +import { API, showError, showSuccess } from '../../helpers'; + +const EditToken = () => { + const params = useParams(); + const tokenId = params.id; + const [loading, setLoading] = useState(true); + const [inputs, setInputs] = useState({ + name: '' + }); + const { name } = inputs; + const handleInputChange = (e, { name, value }) => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + }; + + const loadToken = async () => { + let res = await API.get(`/api/token/${tokenId}`); + const { success, message, data } = res.data; + if (success) { + data.password = ''; + setInputs(data); + } else { + showError(message); + } + setLoading(false); + }; + useEffect(() => { + loadToken().then(); + }, []); + + const submit = async () => { + let res = await API.put(`/api/token/`, { ...inputs, id: parseInt(tokenId) }); + const { success, message } = res.data; + if (success) { + showSuccess('令牌更新成功!'); + } else { + showError(message); + } + }; + + return ( + <> + +
更新令牌信息
+
+ + + + +
+
+ + ); +}; + +export default EditToken;