diff --git a/controller/token.go b/controller/token.go new file mode 100644 index 00000000..094235c2 --- /dev/null +++ b/controller/token.go @@ -0,0 +1,143 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + "net/http" + "one-api/common" + "one-api/model" + "strconv" +) + +func GetAllTokens(c *gin.Context) { + userId := c.GetInt("id") + p, _ := strconv.Atoi(c.Query("p")) + if p < 0 { + p = 0 + } + tokens, err := model.GetAllUserTokens(userId, p*common.ItemsPerPage, common.ItemsPerPage) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": tokens, + }) + return +} + +func SearchTokens(c *gin.Context) { + userId := c.GetInt("id") + keyword := c.Query("keyword") + tokens, err := model.SearchUserTokens(userId, keyword) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": tokens, + }) + return +} + +func GetToken(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + token, err := model.GetTokenById(id) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": token, + }) + return +} + +func AddToken(c *gin.Context) { + token := model.Token{} + err := c.ShouldBindJSON(&token) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + err = token.Insert() + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + }) + return +} + +func DeleteToken(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + token := model.Token{Id: id} + err := token.Delete() + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + }) + return +} + +func UpdateToken(c *gin.Context) { + token := model.Token{} + err := c.ShouldBindJSON(&token) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + err = token.Update() + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + }) + return +} diff --git a/controller/user.go b/controller/user.go index ce5f2862..014f974f 100644 --- a/controller/user.go +++ b/controller/user.go @@ -4,12 +4,10 @@ import ( "encoding/json" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" - "github.com/google/uuid" "net/http" "one-api/common" "one-api/model" "strconv" - "strings" ) type LoginRequest struct { @@ -245,43 +243,6 @@ func GetUser(c *gin.Context) { return } -func GenerateToken(c *gin.Context) { - id := c.GetInt("id") - user, err := model.GetUserById(id, true) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) - return - } - user.Token = uuid.New().String() - user.Token = strings.Replace(user.Token, "-", "", -1) - - if model.DB.Where("token = ?", user.Token).First(user).RowsAffected != 0 { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "请重试,系统生成的 UUID 竟然重复了!", - }) - return - } - - if err := user.Update(false); err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": user.Token, - }) - return -} - func GetSelf(c *gin.Context) { id := c.GetInt("id") user, err := model.GetUserById(id, false) diff --git a/model/channel.go b/model/channel.go index 4badd53c..3b885adb 100644 --- a/model/channel.go +++ b/model/channel.go @@ -5,28 +5,32 @@ import ( ) type Channel struct { - Id int `json:"id"` - Type int `json:"type" gorm:"default:0"` - Key string `json:"key"` - Status int `json:"status" gorm:"default:1"` + Id int `json:"id"` + Type int `json:"type" gorm:"default:0"` + Key string `json:"key"` + Status int `json:"status" gorm:"default:1"` + Name string `json:"name" gorm:"unique;index"` + Weight int `json:"weight"` + CreatedTime int64 `json:"created_time" gorm:"bigint"` + AccessedTime int64 `json:"accessed_time" gorm:"bigint"` } func GetAllChannels(startIdx int, num int) ([]*Channel, error) { var channels []*Channel var err error - err = DB.Order("id desc").Limit(num).Offset(startIdx).Find(&channels).Error + err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error return channels, err } func SearchChannels(keyword string) (channels []*Channel, err error) { - err = DB.Select([]string{"id", "key"}, keyword, keyword).Find(&channels).Error + err = DB.Omit("key").Where("id = ? or name LIKE ?", keyword, keyword+"%").Find(&channels).Error return channels, err } func GetChannelById(id int) (*Channel, error) { channel := Channel{Id: id} var err error = nil - err = DB.Select([]string{"id", "type"}).First(&channel, "id = ?", id).Error + err = DB.Omit("key").First(&channel, "id = ?", id).Error return &channel, err } @@ -42,7 +46,6 @@ func (channel *Channel) Update() error { return err } -// Delete Make sure link is valid! Because we will use os.Remove to delete it! func (channel *Channel) Delete() error { var err error err = DB.Delete(channel).Error diff --git a/model/token.go b/model/token.go new file mode 100644 index 00000000..0cefd02c --- /dev/null +++ b/model/token.go @@ -0,0 +1,52 @@ +package model + +import ( + _ "gorm.io/driver/sqlite" +) + +type Token struct { + Id int `json:"id"` + UserId int `json:"user_id"` + Key string `json:"key"` + Status int `json:"status" gorm:"default:1"` + Name string `json:"name" gorm:"unique;index"` + CreatedTime int64 `json:"created_time" gorm:"bigint"` + AccessedTime int64 `json:"accessed_time" gorm:"bigint"` +} + +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 + 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 + return tokens, err +} + +func GetTokenById(id int) (*Token, error) { + token := Token{Id: id} + var err error = nil + err = DB.Omit("key").Select([]string{"id", "type"}).First(&token, "id = ?", id).Error + return &token, err +} + +func (token *Token) Insert() error { + var err error + err = DB.Create(token).Error + return err +} + +func (token *Token) Update() error { + var err error + err = DB.Model(token).Updates(token).Error + return err +} + +func (token *Token) Delete() error { + var err error + err = DB.Delete(token).Error + return err +} diff --git a/model/user.go b/model/user.go index 496417b4..d84119bc 100644 --- a/model/user.go +++ b/model/user.go @@ -15,11 +15,11 @@ type User struct { DisplayName string `json:"display_name" gorm:"index" validate:"max=20"` Role int `json:"role" gorm:"type:int;default:1"` // admin, common Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled - Token string `json:"token" gorm:"index"` Email string `json:"email" gorm:"index" validate:"max=50"` GitHubId string `json:"github_id" gorm:"column:github_id;index"` WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database! + Balance int `json:"balance" gorm:"type:int;default:0"` } func GetMaxUserId() int { @@ -29,12 +29,12 @@ func GetMaxUserId() int { } func GetAllUsers(startIdx int, num int) (users []*User, err error) { - err = DB.Order("id desc").Limit(num).Offset(startIdx).Select([]string{"id", "username", "display_name", "role", "status", "email"}).Find(&users).Error + err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("password").Find(&users).Error return users, err } func SearchUsers(keyword string) (users []*User, err error) { - err = DB.Select([]string{"id", "username", "display_name", "role", "status", "email"}).Where("id = ? or username LIKE ? or email LIKE ? or display_name LIKE ?", keyword, keyword+"%", keyword+"%", keyword+"%").Find(&users).Error + err = DB.Omit("password").Where("id = ? or username LIKE ? or email LIKE ? or display_name LIKE ?", keyword, keyword+"%", keyword+"%", keyword+"%").Find(&users).Error return users, err } @@ -47,7 +47,7 @@ func GetUserById(id int, selectAll bool) (*User, error) { if selectAll { err = DB.First(&user, "id = ?", id).Error } else { - err = DB.Select([]string{"id", "username", "display_name", "role", "status", "email", "wechat_id", "github_id"}).First(&user, "id = ?", id).Error + err = DB.Omit("password").First(&user, "id = ?", id).Error } return &user, err } diff --git a/router/api-router.go b/router/api-router.go index 27efe79c..5e34cea6 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -33,7 +33,6 @@ func SetApiRouter(router *gin.Engine) { selfRoute.GET("/self", controller.GetSelf) selfRoute.PUT("/self", controller.UpdateSelf) selfRoute.DELETE("/self", controller.DeleteSelf) - selfRoute.GET("/token", controller.GenerateToken) } adminRoute := userRoute.Group("/") @@ -64,5 +63,15 @@ func SetApiRouter(router *gin.Engine) { channelRoute.PUT("/", controller.UpdateChannel) channelRoute.DELETE("/:id", controller.DeleteChannel) } + tokenRoute := apiRouter.Group("/token") + tokenRoute.Use(middleware.UserAuth()) + { + tokenRoute.GET("/", controller.GetAllTokens) + tokenRoute.GET("/search", controller.SearchTokens) + tokenRoute.GET("/:id", controller.GetToken) + tokenRoute.POST("/", controller.AddToken) + tokenRoute.PUT("/", controller.UpdateToken) + tokenRoute.DELETE("/:id", controller.DeleteToken) + } } } diff --git a/web/src/App.js b/web/src/App.js index a1a8a898..673ec496 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -15,6 +15,7 @@ import GitHubOAuth from './components/GitHubOAuth'; import PasswordResetConfirm from './components/PasswordResetConfirm'; import { UserContext } from './context/User'; import Channel from './pages/Channel'; +import Token from './pages/Token'; const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); @@ -72,6 +73,12 @@ function App() { } /> + + } + /> { setInputs((inputs) => ({ ...inputs, [name]: value })); }; - const generateToken = async () => { - const res = await API.get('/api/user/token'); - const { success, message, data } = res.data; - if (success) { - await copy(data); - showSuccess(`令牌已重置并已复制到剪贴板:${data}`); - } else { - showError(message); - } - }; - const bindWeChat = async () => { if (inputs.wechat_verification_code === '') return; const res = await API.get( @@ -106,7 +95,6 @@ const PersonalSetting = () => { 更新个人信息 - 生成访问令牌 账号绑定 普通用户; + case 10: + return 管理员; + case 100: + return 超级管理员; + default: + return 未知身份; + } +} + +const TokensTable = () => { + const [users, setUsers] = 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 { success, message, data } = res.data; + if (success) { + if (startIdx === 0) { + setUsers(data); + } else { + let newUsers = users; + newUsers.push(...data); + setUsers(newUsers); + } + } else { + showError(message); + } + setLoading(false); + }; + + const onPaginationChange = (e, { activePage }) => { + (async () => { + if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) { + // In this case we have to load more data and then append them. + await loadUsers(activePage - 1); + } + setActivePage(activePage); + })(); + }; + + useEffect(() => { + loadUsers(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); + } else { + showError(message); + } + })(); + }; + + const renderStatus = (status) => { + switch (status) { + case 1: + return 已激活; + case 2: + return ( + + 已封禁 + + ); + default: + return ( + + 未知状态 + + ); + } + }; + + const searchUsers = async () => { + if (searchKeyword === '') { + // if keyword is blank, load files instead. + await loadUsers(0); + setActivePage(1); + return; + } + setSearching(true); + const res = await API.get(`/api/user/search?keyword=${searchKeyword}`); + const { success, message, data } = res.data; + if (success) { + setUsers(data); + setActivePage(1); + } else { + showError(message); + } + setSearching(false); + }; + + const handleKeywordChange = async (e, { value }) => { + setSearchKeyword(value.trim()); + }; + + const sortUser = (key) => { + if (users.length === 0) return; + setLoading(true); + let sortedUsers = [...users]; + sortedUsers.sort((a, b) => { + return ('' + a[key]).localeCompare(b[key]); + }); + if (sortedUsers[0].id === users[0].id) { + sortedUsers.reverse(); + } + setUsers(sortedUsers); + setLoading(false); + }; + + return ( + <> + + + + + + + + { + sortUser('username'); + }} + > + 用户名 + + { + sortUser('display_name'); + }} + > + 显示名称 + + { + sortUser('email'); + }} + > + 邮箱地址 + + { + sortUser('role'); + }} + > + 用户角色 + + { + sortUser('status'); + }} + > + 状态 + + 操作 + + + + + {users + .slice( + (activePage - 1) * ITEMS_PER_PAGE, + activePage * ITEMS_PER_PAGE + ) + .map((user, idx) => { + if (user.deleted) return <>>; + return ( + + {user.username} + {user.display_name} + {user.email ? user.email : '无'} + {renderRole(user.role)} + {renderStatus(user.status)} + + + { + manageUser(user.username, 'promote', idx); + }} + > + 提升 + + { + manageUser(user.username, 'demote', idx); + }} + > + 降级 + + { + manageUser(user.username, 'delete', idx); + }} + > + 删除 + + { + manageUser( + user.username, + user.status === 1 ? 'disable' : 'enable', + idx + ); + }} + > + {user.status === 1 ? '禁用' : '启用'} + + + 编辑 + + + + + ); + })} + + + + + + + 添加新的用户 + + + + + + + > + ); +}; + +export default TokensTable; diff --git a/web/src/pages/Token/index.js b/web/src/pages/Token/index.js new file mode 100644 index 00000000..c995131e --- /dev/null +++ b/web/src/pages/Token/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { Segment, Header } from 'semantic-ui-react'; +import TokensTable from '../../components/TokensTable'; + +const Token = () => ( + <> + + 我的令牌 + + + > +); + +export default Token;