diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index e8e3c77..f51386d 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -58,15 +58,19 @@ jobs: with: go-version: '1.27' + - name: Read version + id: version + run: echo "value=$(cat GAHversion)" >> "$GITHUB_OUTPUT" + - name: Build env: CGO_ENABLED: 0 GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} - run: go build -trimpath -ldflags "-s -w" -o GoAccountHub-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }} . + run: go build -trimpath -ldflags "-s -w" -o GoAccountHub-${{ steps.version.outputs.value }}-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }} . - name: Upload Artifact uses: actions/upload-artifact@v4 with: - name: GoAccountHub-${{ matrix.goos }}-${{ matrix.goarch }} - path: GoAccountHub-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }} + name: GoAccountHub-${{ steps.version.outputs.value }}-${{ matrix.goos }}-${{ matrix.goarch }} + path: GoAccountHub-${{ steps.version.outputs.value }}-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }} diff --git a/GAHFrontend/package-lock.json b/GAHFrontend/package-lock.json index 8412348..fc5c4e5 100644 --- a/GAHFrontend/package-lock.json +++ b/GAHFrontend/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "@element-plus/icons-vue": "^2.3.2", "element-plus": "^2.14.5", + "monaco-editor": "^0.52.2", "vue": "^3.5.41", + "vue-cookies": "^1.8.6", "vue-router": "^5.3.1" }, "devDependencies": { @@ -1217,6 +1219,12 @@ "pathe": "^2.0.1" } }, + "node_modules/monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "license": "MIT" + }, "node_modules/muggle-string": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", @@ -1603,6 +1611,12 @@ "integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==", "license": "MIT" }, + "node_modules/vue-cookies": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/vue-cookies/-/vue-cookies-1.8.6.tgz", + "integrity": "sha512-e2kYaHj1Y/zVsBSM3KWlOoVJ5o3l4QZjytNU7xdCgmkw3521CMUerqHekBGZKXXC1oRxYljBeeOK2SCel6cKuw==", + "license": "MIT" + }, "node_modules/vue-router": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.3.1.tgz", diff --git a/GAHFrontend/package.json b/GAHFrontend/package.json index 3d57a4b..de3da5f 100644 --- a/GAHFrontend/package.json +++ b/GAHFrontend/package.json @@ -11,7 +11,9 @@ "dependencies": { "@element-plus/icons-vue": "^2.3.2", "element-plus": "^2.14.5", + "monaco-editor": "^0.52.2", "vue": "^3.5.41", + "vue-cookies": "^1.8.6", "vue-router": "^5.3.1" }, "devDependencies": { diff --git a/GAHFrontend/src/components/component_test.vue b/GAHFrontend/src/components/component_test.vue new file mode 100644 index 0000000..cc340bc --- /dev/null +++ b/GAHFrontend/src/components/component_test.vue @@ -0,0 +1 @@ + diff --git a/GAHFrontend/src/components/customize/AddAdminDialogFrom.vue b/GAHFrontend/src/components/customize/AddAdminDialogFrom.vue new file mode 100644 index 0000000..4694858 --- /dev/null +++ b/GAHFrontend/src/components/customize/AddAdminDialogFrom.vue @@ -0,0 +1,52 @@ + + + diff --git a/GAHFrontend/src/components/customize/AddCharacterDialogFrom.vue b/GAHFrontend/src/components/customize/AddCharacterDialogFrom.vue new file mode 100644 index 0000000..33255c6 --- /dev/null +++ b/GAHFrontend/src/components/customize/AddCharacterDialogFrom.vue @@ -0,0 +1,45 @@ + + + diff --git a/GAHFrontend/src/components/customize/AddUserDialogFrom.vue b/GAHFrontend/src/components/customize/AddUserDialogFrom.vue new file mode 100644 index 0000000..f73c73a --- /dev/null +++ b/GAHFrontend/src/components/customize/AddUserDialogFrom.vue @@ -0,0 +1,41 @@ + + + diff --git a/GAHFrontend/src/components/customize/EditAdminDialogFrom.vue b/GAHFrontend/src/components/customize/EditAdminDialogFrom.vue new file mode 100644 index 0000000..ed15606 --- /dev/null +++ b/GAHFrontend/src/components/customize/EditAdminDialogFrom.vue @@ -0,0 +1,68 @@ + + + diff --git a/GAHFrontend/src/components/customize/EditCharacterDialog.vue b/GAHFrontend/src/components/customize/EditCharacterDialog.vue new file mode 100644 index 0000000..9e9aaab --- /dev/null +++ b/GAHFrontend/src/components/customize/EditCharacterDialog.vue @@ -0,0 +1,62 @@ + + + diff --git a/GAHFrontend/src/components/customize/EditUserDialog.vue b/GAHFrontend/src/components/customize/EditUserDialog.vue new file mode 100644 index 0000000..6860646 --- /dev/null +++ b/GAHFrontend/src/components/customize/EditUserDialog.vue @@ -0,0 +1,60 @@ + + + diff --git a/GAHFrontend/src/components/customize/MetaDataEditor.vue b/GAHFrontend/src/components/customize/MetaDataEditor.vue new file mode 100644 index 0000000..911dbc4 --- /dev/null +++ b/GAHFrontend/src/components/customize/MetaDataEditor.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/GAHFrontend/src/components/login/login.vue b/GAHFrontend/src/components/login/login.vue index e9fec19..5ef402c 100644 --- a/GAHFrontend/src/components/login/login.vue +++ b/GAHFrontend/src/components/login/login.vue @@ -36,7 +36,7 @@ import { reactive, ref, onMounted } from 'vue' import { useRouter } from 'vue-router' import { User, Lock } from '@element-plus/icons-vue' import { ElMessage } from 'element-plus' -import { post, get } from '../../lib/request' +import { post } from '../../lib/request' const router = useRouter() const loginFormRef = ref() @@ -62,7 +62,7 @@ const rules = { // 已登录则直接跳转主页 onMounted(async () => { try { - await get('/admin/check_token', {}, { showError: false, autoRedirect401: false }) + await post('/admin/check_token', {}, { showError: false, autoRedirect401: false }) router.push('/main/home') } catch { // 未登录,留在登录页 diff --git a/GAHFrontend/src/components/main/bar.vue b/GAHFrontend/src/components/main/bar.vue index 3220f95..b3ac187 100644 --- a/GAHFrontend/src/components/main/bar.vue +++ b/GAHFrontend/src/components/main/bar.vue @@ -65,7 +65,8 @@ import { computed, onMounted, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import { Odometer, Fold, Expand, User, SetUp, Grid } from '@element-plus/icons-vue' -import { get, post, put, del } from '../../lib/request' +import { post, put, del } from '../../lib/request' +import VueCookies from 'vue-cookies' const route = useRoute() const router = useRouter() @@ -83,8 +84,8 @@ const handleLogout = async () => { // 即使后端出错也继续清理并跳转 } // 删除本地 cookie - document.cookie = 'admin_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;' - document.cookie = 'admin_name=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;' + VueCookies.remove('admin_token') + VueCookies.remove('admin_name') router.push('/') } @@ -110,13 +111,13 @@ const canGetCharacter = computed(() => { onMounted(()=>{ // Check Admin Token - get('/admin/check_token').then(respond=>{ + post('/admin/check_token').then(respond=>{ if(respond.code === 200){ }else{ } }) // Get Admin Info - get('/admin/info').then(respond=>{ + post('/admin/info').then(respond=>{ if(respond.code === 200){ adminInfo.value = respond.data } diff --git a/GAHFrontend/src/components/main/page/admin.vue b/GAHFrontend/src/components/main/page/admin.vue index 068743e..79760ed 100644 --- a/GAHFrontend/src/components/main/page/admin.vue +++ b/GAHFrontend/src/components/main/page/admin.vue @@ -3,46 +3,338 @@ -

-

+

+

权限筛选:

- - - - - - + + + + + +
+ - - +
+ + + +
+ + + diff --git a/GAHFrontend/src/components/main/page/character.vue b/GAHFrontend/src/components/main/page/character.vue index ae105e0..074ade9 100644 --- a/GAHFrontend/src/components/main/page/character.vue +++ b/GAHFrontend/src/components/main/page/character.vue @@ -1,32 +1,394 @@ \ No newline at end of file + diff --git a/GAHFrontend/src/components/main/page/home.vue b/GAHFrontend/src/components/main/page/home.vue index 687d9e5..0bb6de0 100644 --- a/GAHFrontend/src/components/main/page/home.vue +++ b/GAHFrontend/src/components/main/page/home.vue @@ -40,7 +40,7 @@ \ No newline at end of file + diff --git a/GAHFrontend/src/lib/rangeAdmin.js b/GAHFrontend/src/lib/rangeAdmin.js new file mode 100644 index 0000000..7def547 --- /dev/null +++ b/GAHFrontend/src/lib/rangeAdmin.js @@ -0,0 +1,12 @@ +import { post } from './request.js' + +const rangeAdmin = async (condition, start, length) => { + const res = await post('/admin/range', { + begin_table_id: start, + length: length, + search_condition: condition + }) + return res.data +} + +export default rangeAdmin diff --git a/GAHFrontend/src/lib/rangeCharacter.js b/GAHFrontend/src/lib/rangeCharacter.js new file mode 100644 index 0000000..ba5b4d1 --- /dev/null +++ b/GAHFrontend/src/lib/rangeCharacter.js @@ -0,0 +1,12 @@ +import { post } from './request.js' + +const rangeCharacter = async (condition, start, length) => { + const res = await post('/character/range', { + begin_table_id: start, + length: length, + search_condition: condition + }) + return res.data +} + +export default rangeCharacter diff --git a/GAHFrontend/src/lib/rangeUser.js b/GAHFrontend/src/lib/rangeUser.js new file mode 100644 index 0000000..b1f7a7c --- /dev/null +++ b/GAHFrontend/src/lib/rangeUser.js @@ -0,0 +1,12 @@ +import { post } from './request.js' + +const rangeUser = async (condition, start, length) => { + const res = await post('/user/range', { + begin_table_id: start, + length: length, + search_condition: condition + }) + return res.data +} + +export default rangeUser diff --git a/GAHFrontend/src/router/index.js b/GAHFrontend/src/router/index.js index 5e034c8..1e91219 100644 --- a/GAHFrontend/src/router/index.js +++ b/GAHFrontend/src/router/index.js @@ -5,6 +5,7 @@ import Home from '../components/main/page/home.vue' import Admin from '../components/main/page/admin.vue' import User from '../components/main/page/user.vue' import Character from '../components/main/page/character.vue' +import ComponentTest from '../components/component_test.vue' const routes = [ { @@ -12,6 +13,11 @@ const routes = [ name: 'Login', component: Login, }, + { + path: '/component', + name: 'ComponentTest', + component: () => import('../components/component_test.vue'), + }, { path: '/main', component: Bar, diff --git a/GAHversion b/GAHversion new file mode 100644 index 0000000..2c99d92 --- /dev/null +++ b/GAHversion @@ -0,0 +1 @@ +beta-0.1.0 \ No newline at end of file diff --git a/README.md b/README.md index 85f7de4..8bb8b08 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,92 @@ -# GoAccountHub -A User Hub in Go +

GoAccountHub

+ + +

+ Go + PostgreSQL + Vue + Node.js + npm +

+ +## 🌐Language +[English](README.md) | [简体中文](README_zh-cn.md) + +## 📖 Introduction +Go Account Hub,or GAH,is a user Hub in Go language,which can storage metadata of user and support multiple "sub user"(called "character" in this project), with a vue frontend management web page. + +This project need a PostgreSQL database to store. + +## 🛠️ Installation & Start +**__Now it only can be install compiling.We plan to add a docker-compose file in the future.__** + +- 📦 Install + 1. Clone the repository. + 2. Install PostgreSQL database and Node.js. + 3. Type `go mod tidy` in `./GoAccountHub` to install go dependencies. + 4. Type `npm install` in `./GoAccountHub/GAHFrontend` to install vue dependencies. + 5. Compile the Go project in `./GoAccountHub`.Type `go build` to compile the project. +- ▶️ Start + **__⚠️Attention: Before start the api server,you MUST to launch the Api server.And ALSO make sure there is `.env` file in the frontend root directory.Because when Api server start,it will change the `.env` file according to the config.__** + + + 🖥️ Api Server + 1. Type this command to generate the setting config (default config name is `config.json`) + ```bash + gah generate + ``` + 2. Type this command to set the root admin password: + ```bash + gah password + ``` + **__⚠️Warning: The GAH `root` admin is the top privilege admin,which can do anything in the system.__** + 3. Open the Config File, to set the config: + ```json + { + "port": "8081", + "database_name": "GoAccountHub", + "database_host": "127.0.0.1", + "database_port": "5432", + "database_user": "postgres", + "database_password": "postgres", + "frontend_port": "8082", + "root_admin_password_hash": "a665a45920422f9d417......", + "root_admin_uu_hash": "0fbaf45ee863c0......", + "switch_config": { + "allow_multi_character": true, + "allow_admin_logout": false + } + } + ``` + Description of the config: + - `port`: The port number of the server. + - `database_name`: The name of the database. + - `database_host`: The host of the database. + - `database_port`: The port number of the database. + - `database_user`: The username of the database. + - `database_password`: The password of the database. + - `frontend_port`: The port number of the frontend.It will write into the `.env` file when Api server start. + + - `root_admin_password_hash`: The password hash of the root admin.Generated by Cil, do not need to set it. + - `root_admin_uu_hash`: The uu hash of the root admin.Generated by Cil, do not need to set it. + + - `switch_config`: The switch config of the system. + - `switch_config.allow_multi_character`: Whether to allow multiple characters. + - `switch_config.allow_admin_logout`: Whether to allow admin logout(Development wrong,will be removed). + 4. Type this command to start the api server: + ```bash + gah start + ``` + Now you can access the GAH api in `http://localhost:`.It will return: + ```json + {"code":200,"message":"Welcome to GoAccountHub"} + ``` + + 🎨 Frontend Server + 1. Type this command to start the frontend server: + ```bash + npm run dev + ``` + Now you can access the GAH frontend in `http://localhost:`. + +**__More Information,Please See The [`docs/api.md`](docs/api.md)__.** diff --git a/README_zh-cn.md b/README_zh-cn.md new file mode 100644 index 0000000..8ad18b2 --- /dev/null +++ b/README_zh-cn.md @@ -0,0 +1,92 @@ +

GoAccountHub

+ + +

+ Go + PostgreSQL + Vue + Node.js + npm +

+ +## 🌐Language +[English](README.md) | [简体中文](README_zh-cn.md) + +## 📖 简介 +Go Account Hub(简称 GAH)是一个用 Go 语言编写的用户中心,可以存储用户的元数据,并支持多个「子用户」(在本项目中称为「角色」),同时提供一个 Vue 前端管理页面。 + +本项目需要一个 PostgreSQL 数据库来存储数据。 + +## 🛠️ 安装与启动 +**目前只能通过编译安装。我们计划在未来添加 docker-compose 文件。** + +- 📦 安装 + 1. 克隆本仓库。 + 2. 安装 PostgreSQL 数据库和 Node.js。 + 3. 在 `./GoAccountHub` 目录下执行 `go mod tidy` 以安装 Go 依赖。 + 4. 在 `./GoAccountHub/GAHFrontend` 目录下执行 `npm install` 以安装 Vue 依赖。 + 5. 在 `./GoAccountHub` 目录下编译 Go 项目,执行 `go build` 进行编译。 +- ▶️ 启动 + **⚠️注意:在启动 API 服务之前,你**必须**先启动 API 服务。同时请确保前端根目录下存在 `.env` 文件。因为 API 服务启动时会根据配置修改该 `.env` 文件。** + + + 🖥️ Api 服务 + 1. 执行以下命令生成配置文件(默认配置名为 `config.json`) + ```bash + gah generate + ``` + 2. 执行以下命令设置 root 管理员密码: + ```bash + gah password + ``` + **__⚠️警告:GAH 的 `root` 管理员是最高权限管理员,可以操作系统中的任何内容。__** + 3. 打开配置文件,设置配置项: + ```json + { + "port": "8081", + "database_name": "GoAccountHub", + "database_host": "127.0.0.1", + "database_port": "5432", + "database_user": "postgres", + "database_password": "postgres", + "frontend_port": "8082", + "root_admin_password_hash": "a665a45920422f9d417......", + "root_admin_uu_hash": "0fbaf45ee863c0......", + "switch_config": { + "allow_multi_character": true, + "allow_admin_logout": false + } + } + ``` + 配置项说明: + - `port`:服务的端口号。 + - `database_name`:数据库名称。 + - `database_host`:数据库主机地址。 + - `database_port`:数据库端口号。 + - `database_user`:数据库用户名。 + - `database_password`:数据库密码。 + - `frontend_port`:前端端口号。API 服务启动时会写入 `.env` 文件。 + + - `root_admin_password_hash`:root 管理员的密码哈希。由 CLI 生成,无需手动设置。 + - `root_admin_uu_hash`:root 管理员的 uu 哈希。由 CLI 生成,无需手动设置。 + + - `switch_config`:系统的开关配置。 + - `switch_config.allow_multi_character`:是否允许多角色。 + - `switch_config.allow_admin_logout`:是否允许管理员登出(开发有误,将会移除)。 + 4. 执行以下命令启动 API 服务: + ```bash + gah start + ``` + 现在你可以通过 `http://localhost:` 访问 GAH API。它将返回: + ```json + {"code":200,"message":"Welcome to GoAccountHub"} + ``` + + 🎨 前端服务 + 1. 执行以下命令启动前端服务: + ```bash + npm run dev + ``` + 现在你可以通过 `http://localhost:` 访问 GAH 前端。 + +**__更多信息,请参阅 [`docs/api_zh-cn.md`](docs/api_zh-cn.md)。__** diff --git a/adminControllor/CharacterCount.go b/adminControllor/CharacterCount.go new file mode 100644 index 0000000..66da2f5 --- /dev/null +++ b/adminControllor/CharacterCount.go @@ -0,0 +1,28 @@ +package adminControllor + +import ( + "net/http" + + "github.com/TrafficLight6/GoAccountHub/sqlTable" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func CharacterCount(c *gin.Context) { + //Get Db + db := c.Value("db").(*gorm.DB) + + //Count Characters (soft-deleted rows are excluded automatically) + var characterCount int64 + if err := db.Model(&sqlTable.Character{}).Count(&characterCount).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": http.StatusInternalServerError, "error": "Failed To Count Characters"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": http.StatusOK, + "data": gin.H{ + "character_count": characterCount, + }, + }) +} diff --git a/adminControllor/adminCount.go b/adminControllor/adminCount.go new file mode 100644 index 0000000..35d88c6 --- /dev/null +++ b/adminControllor/adminCount.go @@ -0,0 +1,28 @@ +package adminControllor + +import ( + "net/http" + + "github.com/TrafficLight6/GoAccountHub/sqlTable" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func AdminCount(c *gin.Context) { + //Get Db + db := c.Value("db").(*gorm.DB) + + //Count Admins (soft-deleted rows are excluded automatically) + var adminCount int64 + if err := db.Model(&sqlTable.Admin{}).Count(&adminCount).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": http.StatusInternalServerError, "error": "Failed To Count Admins"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": http.StatusOK, + "data": gin.H{ + "admin_count": adminCount, + }, + }) +} diff --git a/adminControllor/adminLogin.go b/adminControllor/adminLogin.go index f9249a8..1e041ac 100644 --- a/adminControllor/adminLogin.go +++ b/adminControllor/adminLogin.go @@ -44,10 +44,10 @@ func AdminLogin(c *gin.Context) { //Set cookie (must be before c.JSON, otherwise headers are already flushed) //admin_token 保持 httpOnly(JS 不可读,防 XSS);admin_name 仅用于前端展示,允许 JS 读取 if body.IsRemember { - c.SetCookie("admin_token", token, 30*24*60*60, "/", "", false, true) + c.SetCookie("admin_token", token, 30*24*60*60, "/", "", false, false) c.SetCookie("admin_name", "root", 30*24*60*60, "/", "", false, false) } else { - c.SetCookie("admin_token", token, 0, "/", "", false, true) + c.SetCookie("admin_token", token, 0, "/", "", false, false) c.SetCookie("admin_name", "root", 0, "/", "", false, false) } c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "message": "login success", "token": token}) @@ -76,10 +76,10 @@ func AdminLogin(c *gin.Context) { //Set cookie (must be before c.JSON, otherwise headers are already flushed) //admin_token 保持 httpOnly(JS 不可读,防 XSS);admin_name 仅用于前端展示,允许 JS 读取 if body.IsRemember { - c.SetCookie("admin_token", token, 30*24*60*60, "/", "", false, true) + c.SetCookie("admin_token", token, 30*24*60*60, "/", "", false, false) c.SetCookie("admin_name", admin.Username, 30*24*60*60, "/", "", false, false) } else { - c.SetCookie("admin_token", token, 0, "/", "", false, true) + c.SetCookie("admin_token", token, 0, "/", "", false, false) c.SetCookie("admin_name", admin.Username, 0, "/", "", false, false) } diff --git a/adminControllor/adminRange.go b/adminControllor/adminRange.go index 68a601c..7c43d8f 100644 --- a/adminControllor/adminRange.go +++ b/adminControllor/adminRange.go @@ -18,9 +18,16 @@ type AdminRangeRequestBody struct { type AdminSearchCondition struct { //If The Field is Empty, It Means Ignore This Condition - Username string `json:"username"` - PasswordHash string `json:"password_hash"` - UUHash string `json:"uu_hash"` + Username string `json:"username"` + UUHash string `json:"uu_hash"` + + //Permission Filter (Bool, false means ignore) + CanAddAdmin bool `json:"can_add_admin"` + CanDeleteAdmin bool `json:"can_delete_admin"` + CanEditAdmin bool `json:"can_edit_admin"` + CanGetAdmin bool `json:"can_get_admin"` + CanOperateUser bool `json:"can_operate_user"` + CanOperateCharacter bool `json:"can_operate_character"` } func AdminRange(c *gin.Context) { @@ -57,18 +64,35 @@ func AdminRange(c *gin.Context) { conditions = append(conditions, "username LIKE ?") args = append(args, "%"+condition.Username+"%") } - if condition.PasswordHash != "" { - conditions = append(conditions, "password_hash LIKE ?") - args = append(args, "%"+condition.PasswordHash+"%") - } if condition.UUHash != "" { conditions = append(conditions, "uu_hash LIKE ?") args = append(args, "%"+condition.UUHash+"%") } + //Permission Filter: only checked (true) fields are applied, all combined with AND + if condition.CanAddAdmin { + conditions = append(conditions, "permission ->> 'can_add_admin' = 'true'") + } + if condition.CanDeleteAdmin { + conditions = append(conditions, "permission ->> 'can_delete_admin' = 'true'") + } + if condition.CanEditAdmin { + conditions = append(conditions, "permission ->> 'can_edit_admin' = 'true'") + } + if condition.CanGetAdmin { + conditions = append(conditions, "permission ->> 'can_get_admin' = 'true'") + } + if condition.CanOperateUser { + conditions = append(conditions, "permission ->> 'can_operate_user' = 'true'") + } + if condition.CanOperateCharacter { + conditions = append(conditions, "permission ->> 'can_operate_character' = 'true'") + } query := db.Model(&sqlTable.Admin{}) if len(conditions) > 0 { query = query.Where(strings.Join(conditions, " AND "), args...) } + //Must Be Ordered, Otherwise Limit/Offset Follows Physical Row Order And Pages Are Not Stable + query = query.Order("id ASC") //Search var admins []sqlTable.Admin if isReturnAll { diff --git a/adminControllor/characterEdit.go b/adminControllor/characterEdit.go index df9b0b9..6b65a68 100644 --- a/adminControllor/characterEdit.go +++ b/adminControllor/characterEdit.go @@ -48,6 +48,12 @@ func CharacterEdit(c *gin.Context) { c.Abort() return } + //Check is Same Name Character + if user.UUHash == characterOld.UserUUHash { + c.JSON(http.StatusBadRequest, gin.H{"code": http.StatusBadRequest, "error": "Same Name Character Can Not Be Edited, Please Trun to User Edit Api"}) + c.Abort() + return + } //Update Character characterNew := characterOld if body.CharacterName != "" { diff --git a/adminControllor/characterRange.go b/adminControllor/characterRange.go index ad63cff..5ed2a66 100644 --- a/adminControllor/characterRange.go +++ b/adminControllor/characterRange.go @@ -19,7 +19,6 @@ type CharacterRangeRequestBody struct { type CharacterSearchCondition struct { //If The Field is Empty, It Means Ignore This Condition CharacterName string `json:"character_name"` - PasswordHash string `json:"password_hash"` UserUUHash string `json:"user_uu_hash"` UUHash string `json:"uu_hash"` } @@ -52,10 +51,6 @@ func CharacterRange(c *gin.Context) { conditions = append(conditions, "character_name LIKE ?") args = append(args, "%"+condition.CharacterName+"%") } - if condition.PasswordHash != "" { - conditions = append(conditions, "password_hash LIKE ?") - args = append(args, "%"+condition.PasswordHash+"%") - } if condition.UserUUHash != "" { conditions = append(conditions, "user_uu_hash LIKE ?") args = append(args, "%"+condition.UserUUHash+"%") @@ -68,6 +63,8 @@ func CharacterRange(c *gin.Context) { if len(conditions) > 0 { query = query.Where(strings.Join(conditions, " AND "), args...) } + //Must Be Ordered, Otherwise Limit/Offset Follows Physical Row Order And Pages Are Not Stable + query = query.Order("id ASC") //Search var characters []sqlTable.Character if isReturnAll { diff --git a/adminControllor/userCount.go b/adminControllor/userCount.go new file mode 100644 index 0000000..3f33631 --- /dev/null +++ b/adminControllor/userCount.go @@ -0,0 +1,28 @@ +package adminControllor + +import ( + "net/http" + + "github.com/TrafficLight6/GoAccountHub/sqlTable" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func UserCount(c *gin.Context) { + //Get Db + db := c.Value("db").(*gorm.DB) + + //Count Users (soft-deleted rows are excluded automatically) + var userCount int64 + if err := db.Model(&sqlTable.User{}).Count(&userCount).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": http.StatusInternalServerError, "error": "Failed To Count Users"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": http.StatusOK, + "data": gin.H{ + "user_count": userCount, + }, + }) +} diff --git a/adminControllor/userEdit.go b/adminControllor/userEdit.go index d948f97..697b831 100644 --- a/adminControllor/userEdit.go +++ b/adminControllor/userEdit.go @@ -26,12 +26,6 @@ func UserEdit(c *gin.Context) { } //Get Db db := c.Value("db").(*gorm.DB) - //Check Username or Password is Empty - if body.Username == "" || body.Password == "" { - c.JSON(http.StatusBadRequest, gin.H{"code": http.StatusBadRequest, "error": "Username or Password Is Empty"}) - c.Abort() - return - } //Check User Exist var userOld sqlTable.User if err := db.Where("uu_hash = ?", body.UUHash).First(&userOld).Error; err != nil { @@ -39,7 +33,7 @@ func UserEdit(c *gin.Context) { c.Abort() return } - //Edit User + //Edit User (Empty Field Means Keep The Old Value) userNew := userOld if body.Username != "" { userNew.Username = body.Username diff --git a/adminControllor/userRanger.go b/adminControllor/userRanger.go index c18b681..c6545e5 100644 --- a/adminControllor/userRanger.go +++ b/adminControllor/userRanger.go @@ -18,9 +18,8 @@ type UserRangeRequestBody struct { type UserSearchCondition struct { //If The Field is Empty, It Means Ignore This Condition - Username string `json:"username"` - PasswordHash string `json:"password_hash"` - UUHash string `json:"uu_hash"` + Username string `json:"username"` + UUHash string `json:"uu_hash"` } func UserRange(c *gin.Context) { @@ -57,10 +56,6 @@ func UserRange(c *gin.Context) { conditions = append(conditions, "username LIKE ?") args = append(args, "%"+condition.Username+"%") } - if condition.PasswordHash != "" { - conditions = append(conditions, "password_hash LIKE ?") - args = append(args, "%"+condition.PasswordHash+"%") - } if condition.UUHash != "" { conditions = append(conditions, "uu_hash LIKE ?") args = append(args, "%"+condition.UUHash+"%") @@ -69,6 +64,8 @@ func UserRange(c *gin.Context) { if len(conditions) > 0 { query = query.Where(strings.Join(conditions, " AND "), args...) } + //Must Be Ordered, Otherwise Limit/Offset Follows Physical Row Order And Pages Are Not Stable + query = query.Order("id ASC") //Search var users []sqlTable.User if isReturnAll { diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..fd3622f --- /dev/null +++ b/docs/api.md @@ -0,0 +1,782 @@ +# 🚀 GoAccountHub API Reference +**__Generated by Ai,If there is any error,please report it in the issue tracker.__** + +## 🌐Language +[English](api.md) | [简体中文](api_zh-cn.md) + +REST API for **GoAccountHub (GAH)** — a user hub written in Go that stores user metadata and supports multiple sub-users (called **characters**). + +- **Base URL:** `http://:` +- **API version prefix:** `/api/v1` +- **Content type:** `application/json` (request bodies are JSON, including `DELETE` / `PUT`) +- **Auth model:** cookie based (`admin_token` for admins, `app_key` for third-party apps) + a login `token` returned by login for app users + +## 📑 Table of Contents + +- [Conventions](#-conventions) +- [Cookie & Auth Model](#-cookie--auth-model) +- [Permission Model](#-permission-model) +- [Quick Reference](#-quick-reference) +- [🌐 Root](#-root) +- [🛡️ Admin API](#️-admin-api) +- [📱 App API](#-app-api) + +## 🧭 Conventions + +| Topic | Rule | +| --- | --- | +| Success | HTTP `200` with `{"code": 200, ...}` | +| Error | Non-2xx status with `{"code": , "error": ""}` (some endpoints use `"message"` instead of `"error"`) | +| Booleans | Empty / `false` search fields are ignored | +| Pagination | `begin_table_id` starts at **1**; `length = -1` means **return all**; `offset = (begin_table_id - 1) * length` | +| Object fields | `User` / `Character` / `Admin` objects are serialized with **PascalCase** keys (e.g. `Username`, `UUHash`, `MetaData`); `Permission` uses **snake_case** (e.g. `can_add_admin`) | + +## 🍪 Cookie & Auth Model + +| Cookie | Set by | Used by | Meaning | +| --- | --- | --- | --- | +| `admin_token` | `POST /api/v1/admin/login` | All `/api/v1/*` admin endpoints | Admin session token | +| `admin_name` | `POST /api/v1/admin/login` | Frontend display only | Current admin username | +| `app_key` | External (issued out-of-band) | All `/api/v1/app/*` endpoints **except** the two `metadata` endpoints | Third-party application key | + +> `is_remember = true` → cookie/session lasts **30 days**; otherwise **1 hour** (session cookie for the admin token). + +## 🔑 Permission Model + +Normal admins carry a `permission` object: + +```json +{ + "can_add_admin": false, + "can_delete_admin": false, + "can_edit_admin": false, + "can_get_admin": false, + "can_operate_user": false, + "can_operate_character": false +} +``` + +- The built-in `root` admin is stored in the config file (not in the database) and **bypasses all permission checks**. +- Missing or `false` permission → HTTP `403` `Permission Denied`. +- Invalid / expired `admin_token` → HTTP `401`. + +| Permission | Grants access to | +| --- | --- | +| `can_get_admin` | `POST /admin/count`, `POST /admin/get`, `POST /admin/range` | +| `can_add_admin` | `POST /admin/add` | +| `can_delete_admin` | `DELETE /admin/delete` | +| `can_edit_admin` | `PUT /admin/edit` | +| `can_operate_user` | All `/api/v1/user/*` admin endpoints | +| `can_operate_character` | All `/api/v1/character/*` admin endpoints | + +## 📋 Quick Reference + +| Method | Path | 🍪 Cookie | 🔑 Permission | +| --- | --- | --- | --- | +| `GET` | `/` | ❌ | — | +| `POST` | `/api/v1/admin/login` | ❌ | — | +| `DELETE` | `/api/v1/admin/logout` | ✅ `admin_token` | — | +| `POST` | `/api/v1/info` | ✅ `admin_token` | — | +| `POST` | `/api/v1/admin/check_token` | ✅ `admin_token` | — | +| `POST` | `/api/v1/admin/info` | ✅ `admin_token` | — | +| `POST` | `/api/v1/admin/count` | ✅ `admin_token` | `can_get_admin` | +| `POST` | `/api/v1/admin/add` | ✅ `admin_token` | `can_add_admin` | +| `DELETE` | `/api/v1/admin/delete` | ✅ `admin_token` | `can_delete_admin` | +| `PUT` | `/api/v1/admin/edit` | ✅ `admin_token` | `can_edit_admin` | +| `POST` | `/api/v1/admin/get` | ✅ `admin_token` | `can_get_admin` | +| `POST` | `/api/v1/admin/range` | ✅ `admin_token` | `can_get_admin` | +| `POST` | `/api/v1/user/count` | ✅ `admin_token` | `can_operate_user` | +| `POST` | `/api/v1/user/add` | ✅ `admin_token` | `can_operate_user` | +| `DELETE` | `/api/v1/user/delete` | ✅ `admin_token` | `can_operate_user` | +| `PUT` | `/api/v1/user/edit` | ✅ `admin_token` | `can_operate_user` | +| `POST` | `/api/v1/user/get` | ✅ `admin_token` | `can_operate_user` | +| `POST` | `/api/v1/user/range` | ✅ `admin_token` | `can_operate_user` | +| `POST` | `/api/v1/character/count` | ✅ `admin_token` | `can_operate_character` + multi-character switch | +| `POST` | `/api/v1/character/add` | ✅ `admin_token` | `can_operate_character` + multi-character switch | +| `DELETE` | `/api/v1/character/delete` | ✅ `admin_token` | `can_operate_character` + multi-character switch | +| `PUT` | `/api/v1/character/edit` | ✅ `admin_token` | `can_operate_character` + multi-character switch | +| `POST` | `/api/v1/character/get` | ✅ `admin_token` | `can_operate_character` + multi-character switch | +| `POST` | `/api/v1/character/range` | ✅ `admin_token` | `can_operate_character` + multi-character switch | +| `POST` | `/api/v1/app/user/login` | ✅ `app_key` | — | +| `DELETE` | `/api/v1/app/user/logout` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/user/add` | ✅ `app_key` | — | +| `DELETE` | `/api/v1/app/user/delete` | ✅ `app_key` | — | +| `PUT` | `/api/v1/app/user/edit` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/user/get` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/user/range` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/character/add` | ✅ `app_key` | — | +| `DELETE` | `/api/v1/app/character/delete` | ✅ `app_key` | — | +| `PUT` | `/api/v1/app/character/edit` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/character/get` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/character/range` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/user/get/metadata` | ❌ | — | +| `POST` | `/api/v1/app/character/get/metadata` | ❌ | — | + +--- + +## 🌐 Root + +### `GET` `/` + +Health / welcome check. + +- 🍪 **Cookie required:** No +- 🔑 **Permission required:** — + +**Response** + +```json +{ "code": 200, "message": "Welcome to GoAccountHub" } +``` + +--- + +## 🛡️ Admin API + +Service-side management API. After login, the browser holds the `admin_token` cookie. + +### 🔐 Auth + +#### `POST` `/api/v1/admin/login` + +Log in as `root` or a normal admin. Sets the `admin_token` (and `admin_name`) cookie on success. + +- 🍪 **Cookie required:** No +- 🔑 **Permission required:** — + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `username` | string | ✅ | Admin username (`root` for the root admin) | +| `password` | string | ✅ | Plaintext password (server hashes it with SHA-256) | +| `is_remember` | bool | ❌ | `true` = 30-day session, `false`/omitted = 1-hour session | + +```json +{ "username": "root", "password": "secret", "is_remember": true } +``` + +**Response** + +```json +{ "code": 200, "message": "login success", "token": "", "admin": "root" } +``` + +> Invalid credentials → `400` `{"code":400,"message":"invalid credentials"}`. + +#### `DELETE` `/api/v1/admin/logout` + +Invalidate the current `admin_token` and delete it from the database. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** — + +**Request Body:** none + +**Response** + +```json +{ "code": 200, "message": "Logout success" } +``` + +#### `POST` `/api/v1/info` + +Dashboard statistics for any logged-in admin. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** — + +**Request Body:** none + +**Response** + +```json +{ + "code": 200, + "data": { + "user_count": 10, + "character_count": 25, + "total_token_count": 7, + "user_token_count": 5, + "admin_token_count": 2, + "admin_count": 3 + } +} +``` + +> `admin_count` includes the root admin when configured. + +#### `POST` `/api/v1/admin/check_token` + +Validate the current `admin_token`. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** — + +**Request Body:** none + +**Response** + +```json +{ "code": 200, "message": "admin_token valid" } +``` + +> Missing/invalid token → `400`. + +#### `POST` `/api/v1/admin/info` + +Return the current admin's identity and permissions. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** — + +**Request Body:** none + +**Response (normal admin)** + +```json +{ + "code": 200, + "message": "Admin Info", + "data": { + "username": "alice", + "uu_hash": "", + "is_root": false, + "permission": { "can_add_admin": true, "can_operate_user": true } + } +} +``` + +**Response (root admin)** — `is_root: true` with all permissions `true`. + +### 👤 Admin Management + +#### `POST` `/api/v1/admin/count` + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_get_admin` +- **Request Body:** none + +```json +{ "code": 200, "data": { "admin_count": 3 } } +``` + +#### `POST` `/api/v1/admin/add` + +Create a new (non-root) admin. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_add_admin` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `username` | string | ✅ | Must not be `root` and must be unique | +| `password` | string | ✅ | Plaintext password | +| `permission` | object | ✅ | Permission map (see [Permission Model](#-permission-model)) | + +```json +{ + "username": "alice", + "password": "p@ssw0rd", + "permission": { "can_add_admin": true, "can_operate_user": true } +} +``` + +**Response** + +```json +{ "code": 200, "message": "Admin Added", "admin": { "ID": 2, "Username": "alice", "UUHash": "", "Permission": { } } } +``` + +> Username `root` is rejected; duplicate username or empty username/password → `400`. + +#### `DELETE` `/api/v1/admin/delete` + +Delete an admin by `uu_hash`. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_delete_admin` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `uu_hash` | string | ✅ | Target admin `uu_hash` | + +```json +{ "uu_hash": "" } +``` + +**Response** + +```json +{ "code": 200, "message": "Delete Admin Success" } +``` + +> The root admin cannot be deleted, and an admin cannot delete itself → `400`. + +#### `PUT` `/api/v1/admin/edit` + +Edit an admin's password and/or permissions. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_edit_admin` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `uu_hash` | string | ✅ | Target admin `uu_hash` | +| `admin_info.password` | string | ❌ | New password; empty = keep current | +| `admin_info.permission` | object | ❌ | New permission map | + +```json +{ + "uu_hash": "", + "admin_info": { + "password": "new-password", + "permission": { "can_operate_user": true } + } +} +``` + +**Response** + +```json +{ "code": 200, "message": "Success" } +``` + +> The root admin cannot be edited, and an admin cannot edit itself → `400`. + +#### `POST` `/api/v1/admin/get` + +Fetch a single admin by `uu_hash`. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_get_admin` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `uu_hash` | string | ✅ | Target admin `uu_hash` | + +**Response (normal admin)** + +```json +{ "code": 200, "admin": { "ID": 2, "Username": "alice", "UUHash": "", "Permission": { } }, "is_root": false } +``` + +**Response (root admin)** — `{ "code": 200, "admin": null, "is_root": true }` +Not found → `404`. + +#### `POST` `/api/v1/admin/range` + +Paginated / filtered admin list. **The root admin is not included.** + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_get_admin` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `begin_table_id` | int | ✅ | Page index, starts at `1` (must be `>= 0`) | +| `length` | int | ✅ | Page size; `-1` = return all (must be `>= -1`) | +| `search_condition` | object | ❌ | Filters below (empty fields ignored, combined with `AND`) | + +**`search_condition`** + +| Field | Type | Description | +| --- | --- | --- | +| `username` | string | Fuzzy match (`LIKE %value%`) | +| `uu_hash` | string | Fuzzy match (`LIKE %value%`) | +| `can_add_admin` | bool | Only admins **having** this permission when `true` | +| `can_delete_admin` | bool | Same as above | +| `can_edit_admin` | bool | Same as above | +| `can_get_admin` | bool | Same as above | +| `can_operate_user` | bool | Same as above | +| `can_operate_character` | bool | Same as above | + +```json +{ + "begin_table_id": 1, + "length": 20, + "search_condition": { "username": "ali", "can_get_admin": true } +} +``` + +**Response** + +```json +{ "code": 200, "message": "Success(not include root admin)", "data": [ { "ID": 2, "Username": "alice" } ] } +``` + +### 🧑 User Management + +#### `POST` `/api/v1/user/count` + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_operate_user` +- **Request Body:** none + +```json +{ "code": 200, "data": { "user_count": 10 } } +``` + +#### `POST` `/api/v1/user/add` + +Create a user. A **same-name character** is created automatically. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_operate_user` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `username` | string | ✅ | Unique | +| `password` | string | ✅ | Plaintext password | +| `meta_data` | string | ❌ | Arbitrary metadata (JSON / XML / YAML / plain text) | + +**Response** + +```json +{ "code": 200, "message": "User Added Successfully,And Same Name Character Added Successfully", "user": { "ID": 1, "Username": "bob", "UUHash": "", "MetaData": "" } } +``` + +#### `DELETE` `/api/v1/user/delete` + +Delete a user **and all characters belonging to it**. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_operate_user` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `uu_hash` | string | ✅ | Target user `uu_hash` | + +**Response** + +```json +{ "code": 200, "message": "User Deleted" } +``` + +#### `PUT` `/api/v1/user/edit` + +Edit a user. Empty fields keep their current value. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_operate_user` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `uu_hash` | string | ✅ | Target user `uu_hash` | +| `username` | string | ❌ | New username (also renames the same-name character) | +| `password` | string | ❌ | New password | +| `meta_data` | string | ❌ | New metadata | + +**Response** + +```json +{ "code": 200, "message": "Success" } +``` + +#### `POST` `/api/v1/user/get` + +Fetch a single user plus the number of characters it owns. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_operate_user` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `uu_hash` | string | ✅ | Target user `uu_hash` | + +**Response** + +```json +{ "code": 200, "message": "Success", "user": { "ID": 1, "Username": "bob" }, "character_number": 2 } +``` + +#### `POST` `/api/v1/user/range` + +Paginated / filtered user list. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_operate_user` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `begin_table_id` | int | ✅ | Page index, starts at `1` | +| `length` | int | ✅ | Page size; `-1` = return all | +| `search_condition` | object | ❌ | Filters below | + +**`search_condition`** + +| Field | Type | Description | +| --- | --- | --- | +| `username` | string | Fuzzy match (`LIKE %value%`) | +| `uu_hash` | string | Fuzzy match (`LIKE %value%`) | + +**Response** + +```json +{ "code": 200, "message": "Success", "data": [ { "ID": 1, "Username": "bob" } ] } +``` + +### 🎭 Character Management + +> ⚠️ These endpoints (and the whole `/api/v1/app/*` group, see below) are only reachable when the config switch **`allow_multi_character`** is `true`. Otherwise → `403` `Server Not Allow Multi Character`. + +#### `POST` `/api/v1/character/count` + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_operate_character` + +```json +{ "code": 200, "data": { "character_count": 25 } } +``` + +#### `POST` `/api/v1/character/add` + +Add a character under an existing user. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_operate_character` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `character_name` | string | ✅ | Globally unique | +| `password` | string | ✅ | Plaintext password | +| `user_uu_hash` | string | ✅ | Owner user `uu_hash` (must exist) | +| `meta_data` | string | ❌ | Arbitrary metadata | + +**Response** + +```json +{ "code": 200, "message": "Character Added Successfully", "character": { "ID": 5, "CharacterName": "bob_alt", "UserUUHash": "", "UUHash": "" } } +``` + +#### `DELETE` `/api/v1/character/delete` + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_operate_character` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `user_uu_hash` | string | ✅ | Owner user `uu_hash` | +| `character_uu_hash` | string | ✅ | Target character `uu_hash` | + +**Response** + +```json +{ "code": 200, "message": "Character Deleted" } +``` + +> The same-name character (`character_uu_hash == user_uu_hash`) cannot be deleted here; delete the user instead. + +#### `PUT` `/api/v1/character/edit` + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_operate_character` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `user_uu_hash` | string | ✅ | Owner user `uu_hash` | +| `character_uu_hash` | string | ✅ | Target character `uu_hash` | +| `character_name` | string | ❌ | New name (must not clash) | +| `password` | string | ❌ | New password | +| `meta_data` | string | ❌ | New metadata | + +**Response** + +```json +{ "code": 200, "message": "Edit Character Success" } +``` + +> The same-name character must be edited through the user endpoints. + +#### `POST` `/api/v1/character/get` + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_operate_character` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `user_uu_hash` | string | ✅ | Owner user `uu_hash` | +| `character_uu_hash` | string | ✅ | Target character `uu_hash` | + +**Response** + +```json +{ "code": 200, "character": { "ID": 5, "CharacterName": "bob_alt", "UserUUHash": "" } } +``` + +#### `POST` `/api/v1/character/range` + +Paginated / filtered character list. + +- 🍪 **Cookie required:** Yes — `admin_token` +- 🔑 **Permission required:** `can_operate_character` + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `begin_table_id` | int | ✅ | Page index, starts at `1` | +| `length` | int | ✅ | Page size; `-1` = return all | +| `search_condition` | object | ❌ | Filters below | + +**`search_condition`** + +| Field | Type | Description | +| --- | --- | --- | +| `character_name` | string | Fuzzy match (`LIKE %value%`) | +| `user_uu_hash` | string | Fuzzy match (`LIKE %value%`) | +| `uu_hash` | string | Fuzzy match (`LIKE %value%`) | + +**Response** + +```json +{ "code": 200, "message": "Success", "data": [ { "ID": 5, "CharacterName": "bob_alt" } ] } +``` + +--- + +## 📱 App API + +Third-party application API under `/api/v1/app`. All endpoints require the **`app_key`** cookie — except the two `metadata` endpoints which authenticate with the login `token` in the body. + +> ⚠️ Because of group middleware inheritance, **all** `/api/v1/app/*` endpoints are also gated by the **`allow_multi_character`** switch (including user endpoints). + +### 🔐 Auth + +#### `POST` `/api/v1/app/user/login` + +Log in a user with a specific character. Returns a `token` (used for the app user's own operations and the metadata endpoints). + +- 🍪 **Cookie required:** Yes — `app_key` +- 🔑 **Permission required:** — + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `username` | string | ✅ | User name | +| `character_name` | string | ✅ | Character belonging to the user | +| `password` | string | ✅ | User password (plaintext) | +| `is_remember` | bool | ❌ | `true` = 30-day token, otherwise 1 hour | + +```json +{ "username": "bob", "character_name": "bob", "password": "secret", "is_remember": true } +``` + +**Response** + +```json +{ "code": 200, "message": "Login success", "token": "" } +``` + +> Unknown user / wrong password / unknown character name → `400`. + +#### `DELETE` `/api/v1/app/user/logout` + +Invalidate a user `token`. + +- 🍪 **Cookie required:** Yes — `app_key` +- 🔑 **Permission required:** — + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `token` | string | ✅ | Token returned by app login | + +**Response** + +```json +{ "code": 200, "message": "Logout success" } +``` + +### 🧑 User Endpoints (App) + +Identical handlers to their admin counterparts, but guarded by `app_key` instead of `admin_token` + permissions. + +| Method | Path | 🍪 Cookie | Body | +| --- | --- | --- | --- | +| `POST` | `/api/v1/app/user/add` | `app_key` | `username`, `password`, `meta_data` | +| `DELETE` | `/api/v1/app/user/delete` | `app_key` | `uu_hash` | +| `PUT` | `/api/v1/app/user/edit` | `app_key` | `uu_hash`, `username?`, `password?`, `meta_data?` | +| `POST` | `/api/v1/app/user/get` | `app_key` | `uu_hash` | +| `POST` | `/api/v1/app/user/range` | `app_key` | `begin_table_id`, `length`, `search_condition{ username, uu_hash }` | + +### 🎭 Character Endpoints (App) + +| Method | Path | 🍪 Cookie | Body | +| --- | --- | --- | --- | +| `POST` | `/api/v1/app/character/add` | `app_key` | `character_name`, `password`, `user_uu_hash`, `meta_data` | +| `DELETE` | `/api/v1/app/character/delete` | `app_key` | `user_uu_hash`, `character_uu_hash` | +| `PUT` | `/api/v1/app/character/edit` | `app_key` | `user_uu_hash`, `character_uu_hash`, `character_name?`, `password?`, `meta_data?` | +| `POST` | `/api/v1/app/character/get` | `app_key` | `user_uu_hash`, `character_uu_hash` | +| `POST` | `/api/v1/app/character/range` | `app_key` | `begin_table_id`, `length`, `search_condition{ character_name, user_uu_hash, uu_hash }` | + +### 📦 Metadata Endpoints + +> 🍪 These two endpoints do **not** require `app_key`; they authenticate with the app user `token` in the body. + +#### `POST` `/api/v1/app/user/get/metadata` + +Return the metadata of the user owning the token. + +- 🍪 **Cookie required:** No +- 🔑 **Permission required:** — + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `token` | string | ✅ | App user token | + +**Response** + +```json +{ "code": 200, "message": "Success", "meta_data": "" } +``` + +#### `POST` `/api/v1/app/character/get/metadata` + +Return the metadata of the character owning the token. + +- 🍪 **Cookie required:** No +- 🔑 **Permission required:** — + +**Request Body** + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `token` | string | ✅ | App user token | + +**Response** + +```json +{ "code": 200, "message": "success", "meta_data": "" } +``` diff --git a/docs/api_zh-cn.md b/docs/api_zh-cn.md new file mode 100644 index 0000000..15f224b --- /dev/null +++ b/docs/api_zh-cn.md @@ -0,0 +1,782 @@ +# 🚀 GoAccountHub API 参考 +**__由 AI 生成,如有错误,请在 issue 中反馈。__** + +## 🌐Language +[English](api.md) | [简体中文](api_zh-cn.md) + +**GoAccountHub (GAH)** 的 REST API —— 一个用 Go 编写的用户中心,可存储用户元数据,并支持多个子用户(在本项目中称为「角色」)。 + +- **基础 URL:** `http://:` +- **API 版本前缀:** `/api/v1` +- **内容类型:** `application/json`(请求体均为 JSON,包括 `DELETE` / `PUT`) +- **鉴权模型:** 基于 Cookie(管理员使用 `admin_token`,第三方应用使用 `app_key`)+ 应用用户登录后返回的 `token` + +## 📑 目录 + +- [约定](#-约定) +- [Cookie 与鉴权模型](#-cookie-与鉴权模型) +- [权限模型](#-权限模型) +- [快速参考](#-快速参考) +- [🌐 根路由](#-根路由) +- [🛡️ 管理员 API](#️-管理员-api) +- [📱 应用 API](#-应用-api) + +## 🧭 约定 + +| 主题 | 规则 | +| --- | --- | +| 成功 | HTTP `200`,响应体为 `{"code": 200, ...}` | +| 错误 | 非 2xx 状态码,响应体为 `{"code": , "error": ""}`(部分端点使用 `"message"` 而非 `"error"`) | +| 布尔条件 | 搜索条件中为空 / `false` 的字段会被忽略 | +| 分页 | `begin_table_id` 从 **1** 开始;`length = -1` 表示**返回全部**;`offset = (begin_table_id - 1) * length` | +| 对象字段 | `User` / `Character` / `Admin` 对象序列化时使用 **PascalCase** 键名(如 `Username`、`UUHash`、`MetaData`);`Permission` 使用 **snake_case**(如 `can_add_admin`) | + +## 🍪 Cookie 与鉴权模型 + +| Cookie | 设置方 | 使用者 | 含义 | +| --- | --- | --- | --- | +| `admin_token` | `POST /api/v1/admin/login` | 所有 `/api/v1/*` 管理员端点 | 管理员会话令牌 | +| `admin_name` | `POST /api/v1/admin/login` | 仅前端展示 | 当前管理员用户名 | +| `app_key` | 外部(线下发放) | 所有 `/api/v1/app/*` 端点,**两个 `metadata` 端点除外** | 第三方应用密钥 | + +> `is_remember = true` → Cookie / 会话有效期 **30 天**;否则为 **1 小时**(管理员令牌为会话 Cookie)。 + +## 🔑 权限模型 + +普通管理员携带一个 `permission` 对象: + +```json +{ + "can_add_admin": false, + "can_delete_admin": false, + "can_edit_admin": false, + "can_get_admin": false, + "can_operate_user": false, + "can_operate_character": false +} +``` + +- 内置的 `root` 管理员存储于配置文件中(而非数据库),**绕过所有权限检查**。 +- 权限字段缺失或为 `false` → HTTP `403` `Permission Denied`。 +- `admin_token` 无效 / 过期 → HTTP `401`。 + +| 权限 | 可访问的端点 | +| --- | --- | +| `can_get_admin` | `POST /admin/count`、`POST /admin/get`、`POST /admin/range` | +| `can_add_admin` | `POST /admin/add` | +| `can_delete_admin` | `DELETE /admin/delete` | +| `can_edit_admin` | `PUT /admin/edit` | +| `can_operate_user` | 所有 `/api/v1/user/*` 管理员端点 | +| `can_operate_character` | 所有 `/api/v1/character/*` 管理员端点 | + +## 📋 快速参考 + +| 方法 | 路径 | 🍪 Cookie | 🔑 权限 | +| --- | --- | --- | --- | +| `GET` | `/` | ❌ | — | +| `POST` | `/api/v1/admin/login` | ❌ | — | +| `DELETE` | `/api/v1/admin/logout` | ✅ `admin_token` | — | +| `POST` | `/api/v1/info` | ✅ `admin_token` | — | +| `POST` | `/api/v1/admin/check_token` | ✅ `admin_token` | — | +| `POST` | `/api/v1/admin/info` | ✅ `admin_token` | — | +| `POST` | `/api/v1/admin/count` | ✅ `admin_token` | `can_get_admin` | +| `POST` | `/api/v1/admin/add` | ✅ `admin_token` | `can_add_admin` | +| `DELETE` | `/api/v1/admin/delete` | ✅ `admin_token` | `can_delete_admin` | +| `PUT` | `/api/v1/admin/edit` | ✅ `admin_token` | `can_edit_admin` | +| `POST` | `/api/v1/admin/get` | ✅ `admin_token` | `can_get_admin` | +| `POST` | `/api/v1/admin/range` | ✅ `admin_token` | `can_get_admin` | +| `POST` | `/api/v1/user/count` | ✅ `admin_token` | `can_operate_user` | +| `POST` | `/api/v1/user/add` | ✅ `admin_token` | `can_operate_user` | +| `DELETE` | `/api/v1/user/delete` | ✅ `admin_token` | `can_operate_user` | +| `PUT` | `/api/v1/user/edit` | ✅ `admin_token` | `can_operate_user` | +| `POST` | `/api/v1/user/get` | ✅ `admin_token` | `can_operate_user` | +| `POST` | `/api/v1/user/range` | ✅ `admin_token` | `can_operate_user` | +| `POST` | `/api/v1/character/count` | ✅ `admin_token` | `can_operate_character` + 多角色开关 | +| `POST` | `/api/v1/character/add` | ✅ `admin_token` | `can_operate_character` + 多角色开关 | +| `DELETE` | `/api/v1/character/delete` | ✅ `admin_token` | `can_operate_character` + 多角色开关 | +| `PUT` | `/api/v1/character/edit` | ✅ `admin_token` | `can_operate_character` + 多角色开关 | +| `POST` | `/api/v1/character/get` | ✅ `admin_token` | `can_operate_character` + 多角色开关 | +| `POST` | `/api/v1/character/range` | ✅ `admin_token` | `can_operate_character` + 多角色开关 | +| `POST` | `/api/v1/app/user/login` | ✅ `app_key` | — | +| `DELETE` | `/api/v1/app/user/logout` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/user/add` | ✅ `app_key` | — | +| `DELETE` | `/api/v1/app/user/delete` | ✅ `app_key` | — | +| `PUT` | `/api/v1/app/user/edit` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/user/get` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/user/range` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/character/add` | ✅ `app_key` | — | +| `DELETE` | `/api/v1/app/character/delete` | ✅ `app_key` | — | +| `PUT` | `/api/v1/app/character/edit` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/character/get` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/character/range` | ✅ `app_key` | — | +| `POST` | `/api/v1/app/user/get/metadata` | ❌ | — | +| `POST` | `/api/v1/app/character/get/metadata` | ❌ | — | + +--- + +## 🌐 根路由 + +### `GET` `/` + +健康检查 / 欢迎检查。 + +- 🍪 **是否需要 Cookie:** 否 +- 🔑 **所需权限:** — + +**响应** + +```json +{ "code": 200, "message": "Welcome to GoAccountHub" } +``` + +--- + +## 🛡️ 管理员 API + +服务端管理 API。登录后,浏览器持有 `admin_token` Cookie。 + +### 🔐 鉴权 + +#### `POST` `/api/v1/admin/login` + +以 `root` 或普通管理员身份登录。成功后设置 `admin_token`(以及 `admin_name`)Cookie。 + +- 🍪 **是否需要 Cookie:** 否 +- 🔑 **所需权限:** — + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `username` | string | ✅ | 管理员用户名(root 管理员为 `root`) | +| `password` | string | ✅ | 明文密码(服务端使用 SHA-256 哈希) | +| `is_remember` | bool | ❌ | `true` = 30 天会话,`false` / 省略 = 1 小时会话 | + +```json +{ "username": "root", "password": "secret", "is_remember": true } +``` + +**响应** + +```json +{ "code": 200, "message": "login success", "token": "", "admin": "root" } +``` + +> 凭据无效 → `400` `{"code":400,"message":"invalid credentials"}`。 + +#### `DELETE` `/api/v1/admin/logout` + +使当前 `admin_token` 失效,并从数据库中删除。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** — + +**请求体:** 无 + +**响应** + +```json +{ "code": 200, "message": "Logout success" } +``` + +#### `POST` `/api/v1/info` + +任何已登录管理员均可查看的仪表盘统计数据。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** — + +**请求体:** 无 + +**响应** + +```json +{ + "code": 200, + "data": { + "user_count": 10, + "character_count": 25, + "total_token_count": 7, + "user_token_count": 5, + "admin_token_count": 2, + "admin_count": 3 + } +} +``` + +> 若已配置,`admin_count` 会包含 root 管理员。 + +#### `POST` `/api/v1/admin/check_token` + +校验当前 `admin_token`。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** — + +**请求体:** 无 + +**响应** + +```json +{ "code": 200, "message": "admin_token valid" } +``` + +> 令牌缺失 / 无效 → `400`。 + +#### `POST` `/api/v1/admin/info` + +返回当前管理员的身份与权限。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** — + +**请求体:** 无 + +**响应(普通管理员)** + +```json +{ + "code": 200, + "message": "Admin Info", + "data": { + "username": "alice", + "uu_hash": "", + "is_root": false, + "permission": { "can_add_admin": true, "can_operate_user": true } + } +} +``` + +**响应(root 管理员)** —— `is_root: true`,且所有权限均为 `true`。 + +### 👤 管理员管理 + +#### `POST` `/api/v1/admin/count` + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_get_admin` +- **请求体:** 无 + +```json +{ "code": 200, "data": { "admin_count": 3 } } +``` + +#### `POST` `/api/v1/admin/add` + +创建一个新的(非 root)管理员。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_add_admin` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `username` | string | ✅ | 不能为 `root`,且必须唯一 | +| `password` | string | ✅ | 明文密码 | +| `permission` | object | ✅ | 权限映射(见[权限模型](#-权限模型)) | + +```json +{ + "username": "alice", + "password": "p@ssw0rd", + "permission": { "can_add_admin": true, "can_operate_user": true } +} +``` + +**响应** + +```json +{ "code": 200, "message": "Admin Added", "admin": { "ID": 2, "Username": "alice", "UUHash": "", "Permission": { } } } +``` + +> 用户名为 `root` 会被拒绝;用户名重复或用户名 / 密码为空 → `400`。 + +#### `DELETE` `/api/v1/admin/delete` + +根据 `uu_hash` 删除管理员。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_delete_admin` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `uu_hash` | string | ✅ | 目标管理员的 `uu_hash` | + +```json +{ "uu_hash": "" } +``` + +**响应** + +```json +{ "code": 200, "message": "Delete Admin Success" } +``` + +> root 管理员不可删除,管理员也不能删除自己 → `400`。 + +#### `PUT` `/api/v1/admin/edit` + +修改管理员的密码和 / 或权限。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_edit_admin` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `uu_hash` | string | ✅ | 目标管理员的 `uu_hash` | +| `admin_info.password` | string | ❌ | 新密码;为空表示保持不变 | +| `admin_info.permission` | object | ❌ | 新的权限映射 | + +```json +{ + "uu_hash": "", + "admin_info": { + "password": "new-password", + "permission": { "can_operate_user": true } + } +} +``` + +**响应** + +```json +{ "code": 200, "message": "Success" } +``` + +> root 管理员不可修改,管理员也不能修改自己 → `400`。 + +#### `POST` `/api/v1/admin/get` + +根据 `uu_hash` 获取单个管理员。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_get_admin` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `uu_hash` | string | ✅ | 目标管理员的 `uu_hash` | + +**响应(普通管理员)** + +```json +{ "code": 200, "admin": { "ID": 2, "Username": "alice", "UUHash": "", "Permission": { } }, "is_root": false } +``` + +**响应(root 管理员)** —— `{ "code": 200, "admin": null, "is_root": true }` +未找到 → `404`。 + +#### `POST` `/api/v1/admin/range` + +分页 / 过滤的管理员列表。**不包含 root 管理员。** + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_get_admin` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `begin_table_id` | int | ✅ | 页码,从 `1` 开始(必须 `>= 0`) | +| `length` | int | ✅ | 每页数量;`-1` 表示返回全部(必须 `>= -1`) | +| `search_condition` | object | ❌ | 过滤条件(见下,空字段忽略,使用 `AND` 组合) | + +**`search_condition`** + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `username` | string | 模糊匹配(`LIKE %value%`) | +| `uu_hash` | string | 模糊匹配(`LIKE %value%`) | +| `can_add_admin` | bool | 为 `true` 时仅返回**拥有**该权限的管理员 | +| `can_delete_admin` | bool | 同上 | +| `can_edit_admin` | bool | 同上 | +| `can_get_admin` | bool | 同上 | +| `can_operate_user` | bool | 同上 | +| `can_operate_character` | bool | 同上 | + +```json +{ + "begin_table_id": 1, + "length": 20, + "search_condition": { "username": "ali", "can_get_admin": true } +} +``` + +**响应** + +```json +{ "code": 200, "message": "Success(not include root admin)", "data": [ { "ID": 2, "Username": "alice" } ] } +``` + +### 🧑 用户管理 + +#### `POST` `/api/v1/user/count` + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_operate_user` +- **请求体:** 无 + +```json +{ "code": 200, "data": { "user_count": 10 } } +``` + +#### `POST` `/api/v1/user/add` + +创建用户。会自动创建一个**同名角色**。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_operate_user` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `username` | string | ✅ | 唯一 | +| `password` | string | ✅ | 明文密码 | +| `meta_data` | string | ❌ | 任意元数据(JSON / XML / YAML / 纯文本) | + +**响应** + +```json +{ "code": 200, "message": "User Added Successfully,And Same Name Character Added Successfully", "user": { "ID": 1, "Username": "bob", "UUHash": "", "MetaData": "" } } +``` + +#### `DELETE` `/api/v1/user/delete` + +删除用户**及其所属的所有角色**。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_operate_user` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `uu_hash` | string | ✅ | 目标用户的 `uu_hash` | + +**响应** + +```json +{ "code": 200, "message": "User Deleted" } +``` + +#### `PUT` `/api/v1/user/edit` + +修改用户。字段为空时保持原值。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_operate_user` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `uu_hash` | string | ✅ | 目标用户的 `uu_hash` | +| `username` | string | ❌ | 新用户名(会同时重命名同名角色) | +| `password` | string | ❌ | 新密码 | +| `meta_data` | string | ❌ | 新元数据 | + +**响应** + +```json +{ "code": 200, "message": "Success" } +``` + +#### `POST` `/api/v1/user/get` + +获取单个用户及其拥有的角色数量。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_operate_user` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `uu_hash` | string | ✅ | 目标用户的 `uu_hash` | + +**响应** + +```json +{ "code": 200, "message": "Success", "user": { "ID": 1, "Username": "bob" }, "character_number": 2 } +``` + +#### `POST` `/api/v1/user/range` + +分页 / 过滤的用户列表。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_operate_user` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `begin_table_id` | int | ✅ | 页码,从 `1` 开始 | +| `length` | int | ✅ | 每页数量;`-1` 表示返回全部 | +| `search_condition` | object | ❌ | 过滤条件(见下) | + +**`search_condition`** + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `username` | string | 模糊匹配(`LIKE %value%`) | +| `uu_hash` | string | 模糊匹配(`LIKE %value%`) | + +**响应** + +```json +{ "code": 200, "message": "Success", "data": [ { "ID": 1, "Username": "bob" } ] } +``` + +### 🎭 角色管理 + +> ⚠️ 当配置开关 **`allow_multi_character`** 为 `true` 时,这些端点(以及下文整个 `/api/v1/app/*` 分组)才可访问。否则 → `403` `Server Not Allow Multi Character`。 + +#### `POST` `/api/v1/character/count` + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_operate_character` + +```json +{ "code": 200, "data": { "character_count": 25 } } +``` + +#### `POST` `/api/v1/character/add` + +在已存在的用户下新增角色。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_operate_character` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `character_name` | string | ✅ | 全局唯一 | +| `password` | string | ✅ | 明文密码 | +| `user_uu_hash` | string | ✅ | 所属用户的 `uu_hash`(必须存在) | +| `meta_data` | string | ❌ | 任意元数据 | + +**响应** + +```json +{ "code": 200, "message": "Character Added Successfully", "character": { "ID": 5, "CharacterName": "bob_alt", "UserUUHash": "", "UUHash": "" } } +``` + +#### `DELETE` `/api/v1/character/delete` + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_operate_character` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `user_uu_hash` | string | ✅ | 所属用户的 `uu_hash` | +| `character_uu_hash` | string | ✅ | 目标角色的 `uu_hash` | + +**响应** + +```json +{ "code": 200, "message": "Character Deleted" } +``` + +> 同名角色(`character_uu_hash == user_uu_hash`)不能在此删除;请改为删除该用户。 + +#### `PUT` `/api/v1/character/edit` + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_operate_character` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `user_uu_hash` | string | ✅ | 所属用户的 `uu_hash` | +| `character_uu_hash` | string | ✅ | 目标角色的 `uu_hash` | +| `character_name` | string | ❌ | 新名称(不能冲突) | +| `password` | string | ❌ | 新密码 | +| `meta_data` | string | ❌ | 新元数据 | + +**响应** + +```json +{ "code": 200, "message": "Edit Character Success" } +``` + +> 同名角色必须通过用户相关端点进行修改。 + +#### `POST` `/api/v1/character/get` + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_operate_character` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `user_uu_hash` | string | ✅ | 所属用户的 `uu_hash` | +| `character_uu_hash` | string | ✅ | 目标角色的 `uu_hash` | + +**响应** + +```json +{ "code": 200, "character": { "ID": 5, "CharacterName": "bob_alt", "UserUUHash": "" } } +``` + +#### `POST` `/api/v1/character/range` + +分页 / 过滤的角色列表。 + +- 🍪 **是否需要 Cookie:** 是 —— `admin_token` +- 🔑 **所需权限:** `can_operate_character` + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `begin_table_id` | int | ✅ | 页码,从 `1` 开始 | +| `length` | int | ✅ | 每页数量;`-1` 表示返回全部 | +| `search_condition` | object | ❌ | 过滤条件(见下) | + +**`search_condition`** + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `character_name` | string | 模糊匹配(`LIKE %value%`) | +| `user_uu_hash` | string | 模糊匹配(`LIKE %value%`) | +| `uu_hash` | string | 模糊匹配(`LIKE %value%`) | + +**响应** + +```json +{ "code": 200, "message": "Success", "data": [ { "ID": 5, "CharacterName": "bob_alt" } ] } +``` + +--- + +## 📱 应用 API + +位于 `/api/v1/app` 下的第三方应用 API。所有端点都要求 **`app_key`** Cookie —— 两个 `metadata` 端点除外,它们使用请求体中的登录 `token` 进行鉴权。 + +> ⚠️ 由于分组中间件继承机制,**所有** `/api/v1/app/*` 端点也会受 **`allow_multi_character`** 开关限制(包括用户相关端点)。 + +### 🔐 鉴权 + +#### `POST` `/api/v1/app/user/login` + +以指定角色登录用户。返回 `token`(用于应用用户自身的操作以及 metadata 端点)。 + +- 🍪 **是否需要 Cookie:** 是 —— `app_key` +- 🔑 **所需权限:** — + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `username` | string | ✅ | 用户名 | +| `character_name` | string | ✅ | 属于该用户的角色 | +| `password` | string | ✅ | 用户密码(明文) | +| `is_remember` | bool | ❌ | `true` = 30 天令牌,否则 1 小时 | + +```json +{ "username": "bob", "character_name": "bob", "password": "secret", "is_remember": true } +``` + +**响应** + +```json +{ "code": 200, "message": "Login success", "token": "" } +``` + +> 用户不存在 / 密码错误 / 角色名不存在 → `400`。 + +#### `DELETE` `/api/v1/app/user/logout` + +使用户 `token` 失效。 + +- 🍪 **是否需要 Cookie:** 是 —— `app_key` +- 🔑 **所需权限:** — + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `token` | string | ✅ | 应用登录返回的令牌 | + +**响应** + +```json +{ "code": 200, "message": "Logout success" } +``` + +### 🧑 用户端点(应用) + +与对应的管理员端点使用相同的处理函数,但改用 `app_key` 校验,而非 `admin_token` + 权限。 + +| 方法 | 路径 | 🍪 Cookie | 请求体 | +| --- | --- | --- | --- | +| `POST` | `/api/v1/app/user/add` | `app_key` | `username`、`password`、`meta_data` | +| `DELETE` | `/api/v1/app/user/delete` | `app_key` | `uu_hash` | +| `PUT` | `/api/v1/app/user/edit` | `app_key` | `uu_hash`、`username?`、`password?`、`meta_data?` | +| `POST` | `/api/v1/app/user/get` | `app_key` | `uu_hash` | +| `POST` | `/api/v1/app/user/range` | `app_key` | `begin_table_id`、`length`、`search_condition{ username, uu_hash }` | + +### 🎭 角色端点(应用) + +| 方法 | 路径 | 🍪 Cookie | 请求体 | +| --- | --- | --- | --- | +| `POST` | `/api/v1/app/character/add` | `app_key` | `character_name`、`password`、`user_uu_hash`、`meta_data` | +| `DELETE` | `/api/v1/app/character/delete` | `app_key` | `user_uu_hash`、`character_uu_hash` | +| `PUT` | `/api/v1/app/character/edit` | `app_key` | `user_uu_hash`、`character_uu_hash`、`character_name?`、`password?`、`meta_data?` | +| `POST` | `/api/v1/app/character/get` | `app_key` | `user_uu_hash`、`character_uu_hash` | +| `POST` | `/api/v1/app/character/range` | `app_key` | `begin_table_id`、`length`、`search_condition{ character_name, user_uu_hash, uu_hash }` | + +### 📦 Metadata 端点 + +> 🍪 这两个端点**不**要求 `app_key`;它们使用请求体中的应用用户 `token` 进行鉴权。 + +#### `POST` `/api/v1/app/user/get/metadata` + +返回 token 所属用户的元数据。 + +- 🍪 **是否需要 Cookie:** 否 +- 🔑 **所需权限:** — + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `token` | string | ✅ | 应用用户令牌 | + +**响应** + +```json +{ "code": 200, "message": "Success", "meta_data": "" } +``` + +#### `POST` `/api/v1/app/character/get/metadata` + +返回 token 所属角色的元数据。 + +- 🍪 **是否需要 Cookie:** 否 +- 🔑 **所需权限:** — + +**请求体** + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `token` | string | ✅ | 应用用户令牌 | + +**响应** + +```json +{ "code": 200, "message": "success", "meta_data": "" } +``` diff --git a/docs/assets/GAH.png b/docs/assets/GAH.png new file mode 100644 index 0000000..4803ff7 Binary files /dev/null and b/docs/assets/GAH.png differ diff --git a/router/router.go b/router/router.go index 1e43e9c..552a187 100644 --- a/router/router.go +++ b/router/router.go @@ -37,34 +37,40 @@ func ReturnRouter(config config.Config) (*gin.Engine, *gorm.DB) { v1.DELETE("/admin/logout", adminControllor.AdminLogout) //Dashboard Info (any logged-in admin can view) - v1.GET("/info", middleware.AdminCheckMiddleware(), adminControllor.Info) + v1.POST("/info", middleware.AdminCheckMiddleware(), adminControllor.Info) //Check Admin Token - v1.GET("/admin/check_token", middleware.AdminCheckMiddleware(), checkControllor.CheckAdminToken) + v1.POST("/admin/check_token", middleware.AdminCheckMiddleware(), checkControllor.CheckAdminToken) //Admin Info - v1.GET("/admin/info", middleware.AdminCheckMiddleware(), adminControllor.AdminInfo) + v1.POST("/admin/info", middleware.AdminCheckMiddleware(), adminControllor.AdminInfo) + //Admin Count + v1.POST("/admin/count", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_get_admin"), adminControllor.AdminCount) //Admin Operation v1.POST("/admin/add", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_add_admin"), adminControllor.AdminAdd) v1.DELETE("/admin/delete", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_delete_admin"), adminControllor.AdminDelete) v1.PUT("/admin/edit", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_edit_admin"), adminControllor.AdminEdit) - v1.GET("/admin/get", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_get_admin"), adminControllor.AdminGet) - v1.GET("/admin/range", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_get_admin_range"), adminControllor.AdminRange) + v1.POST("/admin/get", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_get_admin"), adminControllor.AdminGet) + v1.POST("/admin/range", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_get_admin"), adminControllor.AdminRange) + //User Count + v1.POST("/user/count", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_user"), adminControllor.UserCount) //User Api v1.POST("/user/add", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_user"), adminControllor.UserAdd) v1.DELETE("/user/delete", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_user"), adminControllor.UserDelete) v1.PUT("/user/edit", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_user"), adminControllor.UserEdit) - v1.GET("/user/get", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_user"), adminControllor.UserGet) - v1.GET("/user/range", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_user_range"), adminControllor.UserRange) + v1.POST("/user/get", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_user"), adminControllor.UserGet) + v1.POST("/user/range", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_user"), adminControllor.UserRange) //Character Api //Check is Allowed to Use Multi Character Functions v1.Use(middleware.ConfigBlocker("allow_multi_character")) + //Character Count + v1.POST("/character/count", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_character"), adminControllor.CharacterCount) //Character Operation v1.POST("/character/add", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_character"), adminControllor.CharacterAdd) v1.DELETE("/character/delete", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_character"), adminControllor.CharacterDelete) v1.PUT("/character/edit", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_character"), adminControllor.CharacterEdit) - v1.GET("/character/get", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_character"), adminControllor.CharacterGet) - v1.GET("/character/range", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_character_range"), adminControllor.CharacterRange) + v1.POST("/character/get", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_character"), adminControllor.CharacterGet) + v1.POST("/character/range", middleware.AdminCheckMiddleware(), middleware.AdminPermissionCheckMiddleware("can_operate_character"), adminControllor.CharacterRange) } { @@ -77,17 +83,17 @@ func ReturnRouter(config config.Config) (*gin.Engine, *gorm.DB) { app.POST("/user/add", middleware.ApplicationKeyCheckMiddleware(), appControllor.UserAdd) app.DELETE("/user/delete", middleware.ApplicationKeyCheckMiddleware(), appControllor.UserDelete) app.PUT("/user/edit", middleware.ApplicationKeyCheckMiddleware(), appControllor.UserEdit) - app.GET("/user/get", middleware.ApplicationKeyCheckMiddleware(), appControllor.UserGet) - app.GET("/user/range", middleware.ApplicationKeyCheckMiddleware(), appControllor.UserRange) + app.POST("/user/get", middleware.ApplicationKeyCheckMiddleware(), appControllor.UserGet) + app.POST("/user/range", middleware.ApplicationKeyCheckMiddleware(), appControllor.UserRange) //Character Operation app.POST("/character/add", middleware.ApplicationKeyCheckMiddleware(), appControllor.CharacterAdd) app.DELETE("/character/delete", middleware.ApplicationKeyCheckMiddleware(), appControllor.CharacterDelete) app.PUT("/character/edit", middleware.ApplicationKeyCheckMiddleware(), appControllor.CharacterEdit) - app.GET("/character/get", middleware.ApplicationKeyCheckMiddleware(), appControllor.CharacterGet) - app.GET("/character/range", middleware.ApplicationKeyCheckMiddleware(), appControllor.CharacterRange) + app.POST("/character/get", middleware.ApplicationKeyCheckMiddleware(), appControllor.CharacterGet) + app.POST("/character/range", middleware.ApplicationKeyCheckMiddleware(), appControllor.CharacterRange) //Get Metadata - app.GET("/user/get/metadata", appControllor.GetUserMetaData) - app.GET("/character/get/metadata", appControllor.GetCharacterMetaData) + app.POST("/user/get/metadata", appControllor.GetUserMetaData) + app.POST("/character/get/metadata", appControllor.GetCharacterMetaData) } return router, db } diff --git a/todo/target.md b/todo/target.md deleted file mode 100644 index 860c6b8..0000000 --- a/todo/target.md +++ /dev/null @@ -1,8 +0,0 @@ -# Frontend -- [ ] Login Page -- [ ] Signup Page -- [ ] Character List Page -- [ ] Character Detail Page -- [ ] User List Page -- [ ] User Detail Page -- [ ] Admin Page \ No newline at end of file