diff --git a/cmd/bookmark/bookmark.go b/cmd/bookmark/main.go similarity index 52% rename from cmd/bookmark/bookmark.go rename to cmd/bookmark/main.go index 1c93add..f45e8ae 100644 --- a/cmd/bookmark/bookmark.go +++ b/cmd/bookmark/main.go @@ -3,8 +3,9 @@ package main import ( "log" - bookmarks "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks" - "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers" + bookmarks_api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks" + user_np_api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/user_np" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/bookmarks" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) @@ -25,12 +26,20 @@ func main() { { // create a type that satisfies the `api.ServerInterface`, // which contains an implementation of every operation from the generated code - if server, err := handlers.NewBookMarks("./data/bookmark.sqlite3"); err != nil { + if server, err := bookmarks.NewBookMarks("./data/bookmark.sqlite3"); err != nil { log.Fatal("Failed to create bookmarks server:", err) - } else if permission, err := handlers.NewBookMarkPermission(); err != nil || permission == nil { + } else if permission, err := bookmarks.NewBookMarkPermission(); err != nil || permission == nil { log.Fatal("Failed to create bookmarks permission:", err) } else { - bookmarks.RegisterHandlersWithOptions(api_router, server, *permission) + bookmarks_api.RegisterHandlersWithOptions(api_router, server, *permission) + } + + // create a type that satisfies the `api.ServerInterface`, + // which contains an implementation of every operation from the generated code + if server, err := bookmarks.NewUserNP("./data/user_np.sqlite3"); err != nil { + log.Fatal("Failed to create user_np server:", err) + } else { + user_np_api.RegisterHandlers(api_router, server) } } diff --git a/cmd/user_np/user_np.go b/cmd/user_np/user_np.go deleted file mode 100644 index 8b42e7b..0000000 --- a/cmd/user_np/user_np.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -import ( - "log" - - user_np "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/user_np" - "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers" - "github.com/gin-gonic/gin" -) - -func main() { - // gin.SetMode(gin.ReleaseMode) - router := gin.Default() - - api_router := router.Group("/api") - { - // create a type that satisfies the `api.ServerInterface`, - // which contains an implementation of every operation from the generated code - if server, err := handlers.NewUserNP("./data/user_np.sqlite3"); err != nil { - log.Fatal("Failed to create user_np server:", err) - } else { - user_np.RegisterHandlers(api_router, server) - } - } - - var listener = "localhost:8082" - log.Printf("Starting server at http://%s", listener) - log.Fatal(router.Run(listener)) -} diff --git a/config/user_np/user_np.yaml b/config/user_np/user_np.yaml index 75a303b..20a3dc3 100644 --- a/config/user_np/user_np.yaml +++ b/config/user_np/user_np.yaml @@ -4,7 +4,7 @@ info: description: 用户节点权限相关操作(user_name and password) version: '1.0' servers: - - url: http://localhost:8082/api + - url: http://localhost:8081/api description: 开发环境 - url: https://api.zzyxyz.com/api description: 生产环境 @@ -90,8 +90,6 @@ components: properties: token: type: string - expires_in: - type: integer user_id: type: integer format: int64 @@ -114,11 +112,14 @@ components: required: - old_password - new_password + - user_name properties: old_password: type: string new_password: type: string + user_name: + type: string securitySchemes: ApiKeyAuth: diff --git a/config/vfs/vfs.yaml b/config/vfs/vfs.yaml index e7c68eb..da7f28d 100644 --- a/config/vfs/vfs.yaml +++ b/config/vfs/vfs.yaml @@ -4,43 +4,47 @@ info: description: 虚拟文件系统API服务 version: '1.0' servers: - - url: http://localhost:8083/api + - url: http://localhost:8080/api description: 开发环境 - url: https://api.zzyxyz.com/api description: 生产环境 tags: - name: vfs description: 虚拟文件系统相关操作 + - name: user + description: 用户相关操作 security: - ApiKeyAuth: [] paths: - /vfs/v1/users: + /vfs/v1/users/{username}: + parameters: + - name: username + in: path + example: user + required: true + schema: + type: string + post: summary: 创建用户 description: 创建一个用户 operationId: createUser - tags: [vfs] - requestBody: - required: true - content: - application/json: - schema: - type: string - description: 用户名 + tags: [user] responses: '201': description: 创建成功 - content: - application/json: + headers: + X-VFS-Token: schema: type: string - description: X-VFS-TOKEN + description: 认证令牌 + delete: summary: 删除用户 description: 删除一个用户 operationId: deleteUser - tags: [vfs] + tags: [user] responses: '204': description: 删除成功 @@ -106,7 +110,7 @@ paths: operationId: createVFSNode tags: [vfs] requestBody: - required: true + required: false content: application/json: schema: @@ -130,19 +134,25 @@ paths: patch: summary: 修改文件或修改目录 - description: 修改/移动/重命名 已存在的 文件/目录 + description: 修改/移动/重命名/复制 已存在的 文件/目录 operationId: updateVFSNode tags: [vfs] + parameters: + - name: op + in: query + description: 操作模式 + required: true + schema: + type: string + enum: [move, rename, change, copy] requestBody: required: true content: application/json: schema: - type: object - properties: - new_name: - type: string - description: 新名称(用于重命名) + type: string + description: 目的文件夹路径 / 文件名路径 + example: "/home/" responses: '200': description: 操作成功 @@ -182,6 +192,7 @@ components: type: apiKey in: header name: X-VFS-Token + responses: ServerInternalError: description: 服务器内部错误 @@ -189,12 +200,14 @@ components: application/json: schema: $ref: '#/components/schemas/Error' + Unauthorized: description: 未授权 content: application/json: schema: $ref: '#/components/schemas/Error' + schemas: VFSNodeType: type: string @@ -222,6 +235,7 @@ components: - type - created_at - updated_at + VFSDirectoryEntry: type: object properties: diff --git a/go.mod b/go.mod index 352ec67..8380d91 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,29 @@ require ( require github.com/gin-contrib/cors v1.7.6 +require ( + github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect + github.com/casbin/casbin/v2 v2.127.0 // indirect + github.com/casbin/govaluate v1.3.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect +) + require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect diff --git a/go.sum b/go.sum index e2bb4c4..ebe1dd3 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,20 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/casbin/casbin/v2 v2.127.0 h1:UGK3uO/8cOslnNqFUJ4xzm/bh+N+o45U7cSolaFk38c= +github.com/casbin/casbin/v2 v2.127.0/go.mod h1:n4uZK8+tCMvcD6EVQZI90zKAok8iHAvEypcMJVKhGF0= +github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -22,6 +29,8 @@ github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5ql github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= @@ -32,6 +41,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= @@ -47,10 +58,13 @@ github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAu github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -64,6 +78,9 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -92,6 +109,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -134,26 +153,53 @@ github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= @@ -161,6 +207,10 @@ github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2W github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw= golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -172,6 +222,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -189,17 +240,22 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -211,6 +267,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= diff --git a/internal/handlers/bookmark.go b/internal/bookmarks/bookmark.go similarity index 98% rename from internal/handlers/bookmark.go rename to internal/bookmarks/bookmark.go index 62b92b9..9bf47d1 100644 --- a/internal/handlers/bookmark.go +++ b/internal/bookmarks/bookmark.go @@ -1,12 +1,12 @@ // internal/handlers/note_link.go -package handlers +package bookmarks import ( "net/http" api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks" - "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/bookmarks/models" "github.com/gin-gonic/gin" _ "github.com/mattn/go-sqlite3" "gorm.io/driver/sqlite" @@ -34,7 +34,6 @@ func AuthMiddleware() api.MiddlewareFunc { // 提取 API Key apiKey := c.GetHeader("X-BookMark-Token") - return // 验证 API Key(您需要实现这个逻辑) if apiKey == "" || !validateApiKey(apiKey) { c.JSON(http.StatusUnauthorized, api.Error{ @@ -55,26 +54,6 @@ func NewBookMarkPermission() (*api.GinServerOptions, error) { }, nil } -func bookmarkReq2Model(req api.BookmarkRequest) models.Bookmark { - return models.Bookmark{ - Name: req.Name, - Link: req.Link, - Detail: req.Detail, - Description: req.Description, - } -} - -func bookmarkModel2Res(bookmark models.Bookmark) api.BookmarkResponse { - return api.BookmarkResponse{ - Id: bookmark.ID, - Name: bookmark.Name, - Link: bookmark.Link, - Detail: bookmark.Detail, - Description: bookmark.Description, - CreatedAt: bookmark.CreatedAt, - } -} - func NewBookMarks(dbPath string) (*BookMarksImpl, error) { var err error var db *gorm.DB @@ -236,5 +215,25 @@ func (b *BookMarksImpl) UpdateBookmark(c *gin.Context, id int64) { c.JSON(http.StatusOK, response) } +func bookmarkReq2Model(req api.BookmarkRequest) models.Bookmark { + return models.Bookmark{ + Name: req.Name, + Link: req.Link, + Detail: req.Detail, + Description: req.Description, + } +} + +func bookmarkModel2Res(bookmark models.Bookmark) api.BookmarkResponse { + return api.BookmarkResponse{ + Id: bookmark.ID, + Name: bookmark.Name, + Link: bookmark.Link, + Detail: bookmark.Detail, + Description: bookmark.Description, + CreatedAt: bookmark.CreatedAt, + } +} + // Make sure we conform to ServerInterface var _ api.ServerInterface = (*BookMarksImpl)(nil) diff --git a/internal/bookmarks/config.go b/internal/bookmarks/config.go new file mode 100644 index 0000000..a31337a --- /dev/null +++ b/internal/bookmarks/config.go @@ -0,0 +1,4 @@ +package bookmarks + +type Config struct { +} diff --git a/internal/models/bookmark.go b/internal/bookmarks/models/bookmark.go similarity index 100% rename from internal/models/bookmark.go rename to internal/bookmarks/models/bookmark.go diff --git a/internal/bookmarks/models/user_np.go b/internal/bookmarks/models/user_np.go new file mode 100644 index 0000000..b149c8a --- /dev/null +++ b/internal/bookmarks/models/user_np.go @@ -0,0 +1,38 @@ +// internal/models/user_np.go + +package models + +import ( + "time" + + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +// UserNP 用户名密码认证模型 +type UserNP struct { + ID int64 `json:"id" gorm:"primaryKey"` + Username string `json:"username" gorm:"not null;index;size:255;unique"` + Password string `json:"password" gorm:"not null;size:255"` + Email *string `json:"email" gorm:"type:text;unique"` + Token *string `json:"token" gorm:"type:text"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` +} + +// HashPassword 对密码进行哈希处理 +func (u *UserNP) HashPassword(password string) error { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + if err != nil { + return err + } + u.Password = string(bytes) + return nil +} + +// CheckPassword 验证密码 +func (u *UserNP) CheckPassword(providedPassword string) bool { + err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(providedPassword)) + return err == nil +} diff --git a/internal/handlers/user_np.go b/internal/bookmarks/user_np.go similarity index 65% rename from internal/handlers/user_np.go rename to internal/bookmarks/user_np.go index 0b09240..732f880 100644 --- a/internal/handlers/user_np.go +++ b/internal/bookmarks/user_np.go @@ -1,19 +1,24 @@ // internal/handlers/user_np.go -package handlers +package bookmarks import ( + "context" + "fmt" "net/http" api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/user_np" - "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models" + client "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/bookmarks/models" "github.com/gin-gonic/gin" "gorm.io/driver/sqlite" "gorm.io/gorm" ) type UserNPImpl struct { - db *gorm.DB + db *gorm.DB + client *client.ClientWithResponses + vfsToken string } func NewUserNP(dbPath string) (*UserNPImpl, error) { @@ -29,8 +34,62 @@ func NewUserNP(dbPath string) (*UserNPImpl, error) { if err != nil { return nil, err } + client, err := client.NewClientWithResponses("http://localhost:8080/api") + if err != nil { + return nil, err + } - return &UserNPImpl{db: db}, nil + return &UserNPImpl{ + db: db, + client: client, + vfsToken: "random_token", + }, nil +} + +func (u *UserNPImpl) RegisterVFSService(username, token string) (*string, error) { + ctx := context.Background() + reqs, err := u.client.CreateUserWithResponse(ctx, username, func(ctx context.Context, req *http.Request) error { + req.Header.Set("X-VFS-Token", token) + return nil + }) + if err != nil { + return nil, err + } + + if reqs.StatusCode() != http.StatusCreated { + return nil, fmt.Errorf("failed to register vfs service: %s", reqs.Status()) + } + + tokenHeader := reqs.HTTPResponse.Header.Get("X-VFS-Token") + if tokenHeader == "" { + return nil, fmt.Errorf("X-VFS-Token header is missing") + } + return &tokenHeader, nil +} + +func (u *UserNPImpl) UnregisterVFSService(username, token string) error { + ctx := context.Background() + reqs, err := u.client.DeleteUserWithResponse(ctx, username, func(ctx context.Context, req *http.Request) error { + req.Header.Set("X-VFS-Token", token) + return nil + }) + if err != nil { + return err + } + + if reqs.StatusCode() == http.StatusNoContent { + return nil + } + + if reqs.JSON404 != nil { + return fmt.Errorf("用户不存在 %s", reqs.JSON404.Message) + } + + if reqs.JSON500 != nil { + return fmt.Errorf("服务器错误 %s", reqs.JSON500.Message) + } + + return fmt.Errorf("未知错误") } // PostAuthLogin 用户登录 @@ -54,19 +113,8 @@ func (u *UserNPImpl) PostAuthLogin(c *gin.Context) { return } - // 生成JWT token - token, err := user.GenerateSimpleJWT() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "无法生成访问令牌"}) - return - } - - // 更新用户token字段(可选) - user.Token = &token - u.db.Save(&user) - c.JSON(http.StatusOK, api.LoginResponse{ - Token: &token, + Token: user.Token, UserId: &user.ID, }) } @@ -101,7 +149,14 @@ func (u *UserNPImpl) PostAuthRegister(c *gin.Context) { // 保存到数据库 if err := u.db.Create(&user).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "用户创建失败"}) - return + } + + if token, err := u.RegisterVFSService(req.Username, u.vfsToken); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "无法生成访问令牌"}) + u.db.Delete(&user) + } else { + user.Token = token + u.db.Save(&user) } c.JSON(http.StatusCreated, nil) @@ -116,13 +171,6 @@ func (u *UserNPImpl) PutAuthPassword(c *gin.Context) { return } - // 验证token并获取用户名 - username, err := models.CheckSimpleJWT(authHeader) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) - return - } - var req api.PutAuthPasswordJSONRequestBody if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -131,7 +179,7 @@ func (u *UserNPImpl) PutAuthPassword(c *gin.Context) { // 查找用户 var user models.UserNP - if err := u.db.Where("username = ?", username).First(&user).Error; err != nil { + if err := u.db.Where("username = ?", req.UserName).First(&user).Error; err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"}) return } @@ -154,17 +202,6 @@ func (u *UserNPImpl) PutAuthPassword(c *gin.Context) { return } - // 生成新的JWT token - token, err := user.GenerateSimpleJWT() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "无法生成新的访问令牌"}) - return - } - - // 更新用户token字段 - user.Token = &token - u.db.Save(&user) - c.JSON(http.StatusOK, nil) } diff --git a/internal/handlers/vfs.go b/internal/handlers/vfs.go deleted file mode 100644 index 8bb8afb..0000000 --- a/internal/handlers/vfs.go +++ /dev/null @@ -1,354 +0,0 @@ -package handlers - -import ( - "log" - "net/http" - "strings" - "sync" - - api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs" - "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models" - "github.com/gin-gonic/gin" -) - -// // 一行代码生成安全的随机token -// token := make([]byte, 16) -// rand.Read(token) // 忽略错误处理以简化代码(生产环境建议处理) -// tokenStr := hex.EncodeToString(token) -// adminToken = &tokenStr -// log.Printf("Admin API Token (for Swagger testing): %s", *adminToken) - -// if e, err := casbin.NewEnforcer("./config/model.conf", ".data/policy.csv"); err == nil { -// log.Fatalf("Failed to create casbin enforcer: %v", err) -// } else { -// enforcer = e -// } - -// ServiceProxy 服务代理接口 -type ServiceProxy interface { - // Get 从后端服务获取数据 - Get(c *gin.Context, servicePath string, node *models.VfsNode) (any, error) - - // Create 在后端服务创建资源 - Create(c *gin.Context, servicePath string, node *models.VfsNode, data []byte) (string, error) // 返回创建的资源ID - - // Update 更新后端服务资源 - Update(c *gin.Context, servicePath string, node *models.VfsNode, data []byte) error - - // Delete 删除后端服务资源 - Delete(c *gin.Context, servicePath string, node *models.VfsNode) error - - // GetName 获取代理名称 - GetName() string -} - -// ProxyEntry 代理表条目 -type ProxyEntry struct { - Name string - Proxy ServiceProxy // 对应的代理实现 -} - -type VfsImpl struct { - vfs *models.Vfs - proxyTable []*ProxyEntry // 动态代理表 - proxyMutex sync.RWMutex // 保护代理表的读写锁 -} - -func NewVfsHandler(vfs models.Vfs) (*VfsImpl, error) { - return &VfsImpl{ - vfs: &vfs, - proxyTable: make([]*ProxyEntry, 0), - proxyMutex: sync.RWMutex{}, - }, nil -} - -// CreateUser implements api.ServerInterface. -func (v *VfsImpl) CreateUser(c *gin.Context) { - panic("unimplemented") -} - -// DeleteUser implements api.ServerInterface. -func (v *VfsImpl) DeleteUser(c *gin.Context) { - panic("unimplemented") -} - -// CreateVFSNode implements api.ServerInterface. -func (v *VfsImpl) CreateVFSNode(c *gin.Context, params api.CreateVFSNodeParams) { - // 解析路径组件 - parentPath, nodeName, nodeType, err := models.ParsePathComponents(params.Path) - if err != nil { - c.JSON(http.StatusBadRequest, api.Error{ - Errtype: "error", - Message: err.Error(), - }) - return - } - - // 创建节点 - node, err := v.vfs.CreateNodeByComponents(parentPath, nodeName, nodeType) - if err != nil { - c.JSON(http.StatusInternalServerError, api.Error{ - Errtype: "CreateNodeByComponents", - Message: err.Error(), - }) - return - } - if nodeType == models.VfsNodeTypeService { - if !v.Proxy2Service(c, node) { - v.vfs.DeleteVFSNode(node) - return - } - } - - // 返回创建成功的节点 - c.JSON(http.StatusCreated, api.VFSNodeResponse{ - Name: node.Name, - Type: ModelType2ResponseType(node.Type), - CreatedAt: node.CreatedAt, - UpdatedAt: node.UpdatedAt, - }) -} - -// DeleteVFSNode implements api.ServerInterface. -func (v *VfsImpl) DeleteVFSNode(c *gin.Context, params api.DeleteVFSNodeParams) { - node, err := v.vfs.GetNodeByPath(params.Path) - if err != nil { - c.JSON(http.StatusNotFound, api.Error{ - Errtype: "error", - Message: err.Error(), - }) - return - } - - if node.Type == models.VfsNodeTypeService { - if !v.Proxy2Service(c, node) { - return - } - } - - if err := v.vfs.DeleteVFSNode(node); err != nil { - c.JSON(http.StatusInternalServerError, api.Error{ - Errtype: "error", - Message: err.Error(), - }) - return - } - c.JSON(http.StatusNoContent, nil) -} - -// GetVFSNode implements api.ServerInterface. -func (v *VfsImpl) GetVFSNode(c *gin.Context, params api.GetVFSNodeParams) { - node, err := v.vfs.GetNodeByPath(params.Path) - if err != nil { - c.JSON(http.StatusNotFound, api.Error{ - Errtype: "error", - Message: err.Error(), - }) - return - } - - switch node.Type { - case models.VfsNodeTypeDirectory: - if entries, err := v.vfs.GetChildren(node.ID); err != nil { - c.JSON(http.StatusInternalServerError, api.Error{ - Errtype: "error", - Message: err.Error(), - }) - return - } else { - var responseEntries []api.VFSDirectoryEntry - for _, entry := range entries { - responseEntries = append(responseEntries, api.VFSDirectoryEntry{ - Name: entry.Name, - Type: ModelType2ResponseType(entry.Type), - }) - } - c.JSON(http.StatusOK, responseEntries) - return - } - case models.VfsNodeTypeService: - v.Proxy2Service(c, node) - default: - c.JSON(http.StatusBadRequest, api.Error{ - Errtype: "error", - Message: "Not a directory", - }) - } - -} - -// UpdateVFSNode implements api.ServerInterface. -func (v *VfsImpl) UpdateVFSNode(c *gin.Context, params api.UpdateVFSNodeParams) { - var req api.UpdateVFSNodeJSONRequestBody - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, api.Error{ - Errtype: "ParameterError", - Message: "Invalid request parameters", - }) - return - } - - node, err := v.vfs.GetNodeByPath(params.Path) - if err != nil { - c.JSON(http.StatusNotFound, api.Error{ - Errtype: "error", - Message: err.Error(), - }) - return - } - if req.NewName != nil { - node.Name = *req.NewName - } - - // FIXME - if node.Type == models.VfsNodeTypeService { - if !v.Proxy2Service(c, node) { - return - } - } - - // TODO - if err := v.vfs.UpdateVFSNode(node); err != nil { - c.JSON(http.StatusInternalServerError, api.Error{ - Errtype: "error", - Message: err.Error(), - }) - return - } -} - -// Make sure we conform to ServerInterface -var _ api.ServerInterface = (*VfsImpl)(nil) - -func ModelType2ResponseType(nodeType models.VfsNodeType) api.VFSNodeType { - var reponseType api.VFSNodeType - switch nodeType { - case models.VfsNodeTypeFile: - reponseType = api.File - case models.VfsNodeTypeDirectory: - reponseType = api.Directory - case models.VfsNodeTypeService: - reponseType = api.Service - } - return reponseType -} - -// FindProxyByServiceName 根据服务节点名称查找对应的代理 -func (v *VfsImpl) FindProxyByServiceName(serviceName string) ServiceProxy { - v.proxyMutex.RLock() - defer v.proxyMutex.RUnlock() - - if serviceName == "" { - return nil - } - - // 根据服务名称匹配前缀 - for _, entry := range v.proxyTable { - if entry.Name == serviceName { - return entry.Proxy - } - } - - return nil -} - -func (v *VfsImpl) RegisterProxy(entry *ProxyEntry) { - v.proxyMutex.Lock() - defer v.proxyMutex.Unlock() - - v.proxyTable = append(v.proxyTable, entry) -} - -// Proxy2Service 通用服务代理处理函数 -func (v *VfsImpl) Proxy2Service(c *gin.Context, node *models.VfsNode) bool { - exts := strings.Split(node.Name, ".") - var serviceName = exts[1] - log.Println("Proxy2Service: ", serviceName) - // 查找对应的代理 - proxy := v.FindProxyByServiceName(serviceName) - if proxy == nil { - c.JSON(http.StatusNotImplemented, api.Error{ - Errtype: "error", - Message: "Service proxy not found for: " + serviceName, - }) - return false - } - - // 根据HTTP方法调用相应的代理方法 - switch c.Request.Method { - case http.MethodGet: - result, err := proxy.Get(c, serviceName, node) - if err != nil { - c.JSON(http.StatusInternalServerError, api.Error{ - Errtype: "error", - Message: "Failed to get service data: " + err.Error(), - }) - return false - } - - c.JSON(http.StatusOK, result) - return true - case http.MethodPost: - // 读取请求体数据 - data, err := c.GetRawData() - if err != nil { - c.JSON(http.StatusBadRequest, api.Error{ - Errtype: "error", - Message: "Failed to read request data: " + err.Error(), - }) - return false - } - - resourceID, err := proxy.Create(c, serviceName, node, data) - if err != nil { - c.JSON(http.StatusInternalServerError, api.Error{ - Errtype: "error", - Message: "Failed to create service resource: " + err.Error(), - }) - return false - } - - c.JSON(http.StatusCreated, gin.H{"resource_id": resourceID}) - return true - case http.MethodPut, http.MethodPatch: - // 读取请求体数据 - data, err := c.GetRawData() - if err != nil { - c.JSON(http.StatusBadRequest, api.Error{ - Errtype: "error", - Message: "Failed to read request data: " + err.Error(), - }) - return false - } - - err = proxy.Update(c, serviceName, node, data) - if err != nil { - c.JSON(http.StatusInternalServerError, api.Error{ - Errtype: "error", - Message: "Failed to update service resource: " + err.Error(), - }) - return false - } - - c.JSON(http.StatusOK, gin.H{"message": "Updated successfully"}) - return true - case http.MethodDelete: - err := proxy.Delete(c, serviceName, node) - if err != nil { - c.JSON(http.StatusInternalServerError, api.Error{ - Errtype: "error", - Message: "Failed to delete service resource: " + err.Error(), - }) - return false - } - - c.JSON(http.StatusNoContent, nil) - return true - default: - c.JSON(http.StatusMethodNotAllowed, api.Error{ - Errtype: "error", - Message: "Method not allowed", - }) - return false - } -} diff --git a/internal/models/user_np.go b/internal/models/user_np.go deleted file mode 100644 index 9d06579..0000000 --- a/internal/models/user_np.go +++ /dev/null @@ -1,100 +0,0 @@ -// internal/models/user_np.go - -package models - -import ( - "errors" - "time" - - "github.com/golang-jwt/jwt/v5" - "golang.org/x/crypto/bcrypt" - "gorm.io/gorm" -) - -// UserNP 用户名密码认证模型 -type UserNP struct { - ID int64 `json:"id" gorm:"primaryKey"` - Username string `json:"username" gorm:"not null;index;size:255;unique"` - Password string `json:"password" gorm:"not null;size:255"` - Email *string `json:"email" gorm:"type:text;unique"` - Token *string `json:"token" gorm:"type:text"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` -} - -// JWTSecret JWT签名密钥(在实际应用中应该从环境变量或配置文件中读取) -var JWTSecret = []byte("your-secret-key-change-in-production") - -// SimpleClaims 简单的JWT声明结构体 -type SimpleClaims struct { - Username string `json:"username"` - jwt.RegisteredClaims -} - -// HashPassword 对密码进行哈希处理 -func (u *UserNP) HashPassword(password string) error { - bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) - if err != nil { - return err - } - u.Password = string(bytes) - return nil -} - -// CheckPassword 验证密码 -func (u *UserNP) CheckPassword(providedPassword string) bool { - err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(providedPassword)) - return err == nil -} - -// GenerateSimpleJWT 生成简单的JWT Token -func (u *UserNP) GenerateSimpleJWT() (string, error) { - expirationTime := time.Now().Add(24 * time.Hour) - - claims := &SimpleClaims{ - Username: u.Username, - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(expirationTime), - IssuedAt: jwt.NewNumericDate(time.Now()), - NotBefore: jwt.NewNumericDate(time.Now()), - Issuer: "zzyxyz_user_np_api", - Subject: u.Username, - ID: string(rune(u.ID)), // 将用户ID作为JWT ID - }, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString(JWTSecret) - if err != nil { - return "", err - } - - return tokenString, nil -} - -// CheckSimpleJWT 验证JWT Token -func CheckSimpleJWT(tokenString string) (string, error) { - claims := &SimpleClaims{} - - // 解析token - token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { - return JWTSecret, nil - }) - - if err != nil { - // 检查是否是token过期错误 - if errors.Is(err, jwt.ErrTokenExpired) { - return "", errors.New("token已过期") - } - return "", errors.New("无效的token") - } - - // 验证token有效性 - if !token.Valid { - return "", errors.New("无效的token") - } - - // 返回用户名 - return claims.Username, nil -} diff --git a/internal/models/vfs.go b/internal/vfs/models/vfs.go similarity index 58% rename from internal/models/vfs.go rename to internal/vfs/models/vfs.go index 6c6a0ff..d80b98e 100644 --- a/internal/models/vfs.go +++ b/internal/vfs/models/vfs.go @@ -2,8 +2,10 @@ package models import ( + "crypto/rand" "database/sql" "errors" + "fmt" "path" "time" @@ -32,6 +34,11 @@ type VfsNode struct { UpdatedAt time.Time } +type VfsUser struct { + Name string + Token string +} + type VfsDirEntry struct { ID uint64 Name string @@ -45,7 +52,6 @@ func NewVfs(dbPath string) (*Vfs, error) { return nil, err } - // 创建表 _, err = db.Exec(` CREATE TABLE IF NOT EXISTS vfs_nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -60,9 +66,146 @@ func NewVfs(dbPath string) (*Vfs, error) { return nil, err } + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS vfs_users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + token TEXT NOT NULL UNIQUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )`) + if err != nil { + return nil, err + } + + err = createInitialDirectories(db) + if err != nil { + return nil, err + } + return &Vfs{DB: db}, nil } +// 创建初始目录结构 +func createInitialDirectories(db *sql.DB) error { + var err error + // 创建 /home 目录 + _, err = db.Exec(` + INSERT OR IGNORE INTO vfs_nodes (name, parent_id, type, created_at, updated_at) + VALUES ('home', 0, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`, + VfsNodeTypeDirectory) + if err != nil { + return err + } + + // 创建 /public 目录 + _, err = db.Exec(` + INSERT OR IGNORE INTO vfs_nodes (name, parent_id, type, created_at, updated_at) + VALUES ('public', 0, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`, + VfsNodeTypeDirectory) + if err != nil { + return err + } + + return nil +} + +func generateToken() string { + bytes := make([]byte, 16) + if _, err := rand.Read(bytes); err != nil { + // fallback to time-based token + return fmt.Sprintf("%x", time.Now().UnixNano()) + } + return fmt.Sprintf("%x", bytes) +} + +// 添加用户相关操作 +func (v *Vfs) CreateUser(username string) (string, error) { + // 生成随机token + token := generateToken() + + // 检查用户是否已存在 + _, err := v.GetUserByName(username) + if err == nil { + return "", errors.New("user already exists") + } + + // 插入用户(不使用事务) + _, err = v.DB.Exec("INSERT INTO vfs_users (name, token) VALUES (?, ?)", username, token) + if err != nil { + return "", err + } + + // 使用 defer 确保出错时能清理已创建的用户 + defer func() { + if err != nil { + v.DB.Exec("DELETE FROM vfs_users WHERE name = ? AND token = ?", username, token) + } + }() + + // 确保 /home 目录存在 + homeDir, getHomeErr := v.GetNodeByPath("/home/") + if getHomeErr != nil { + // 如果 /home 不存在,创建它 + homeDir = &VfsNode{ + Name: "home", + ParentID: 0, + Type: VfsNodeTypeDirectory, + } + if createHomeErr := v.CreateVFSNode(homeDir); createHomeErr != nil { + err = createHomeErr + return "", err + } + } + + // 创建用户目录 /home/username + userDir := &VfsNode{ + Name: username, + ParentID: homeDir.ID, + Type: VfsNodeTypeDirectory, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + if createDirErr := v.CreateVFSNode(userDir); createDirErr != nil { + err = createDirErr + return "", err + } + + return token, nil +} + +func (v *Vfs) DeleteUser(username string) error { + // TODO: 递归删除用户相关文件 + // 这里暂时只删除用户记录 + _, err := v.DB.Exec("DELETE FROM vfs_users WHERE name = ?", username) + return err +} + +func (v *Vfs) GetUserByToken(token string) (*VfsUser, error) { + user := &VfsUser{} + err := v.DB.QueryRow("SELECT name, token FROM vfs_users WHERE token = ?", token). + Scan(&user.Name, &user.Token) + if err != nil { + if err == sql.ErrNoRows { + return nil, errors.New("user not found") + } + return nil, err + } + return user, nil +} + +func (v *Vfs) GetUserByName(username string) (*VfsUser, error) { + user := &VfsUser{} + err := v.DB.QueryRow("SELECT name, token FROM vfs_users WHERE name = ?", username). + Scan(&user.Name, &user.Token) + if err != nil { + if err == sql.ErrNoRows { + return nil, errors.New("user not found") + } + return nil, err + } + return user, nil +} + // GetChildrenID 获取目录下所有子项的 ID func (v *Vfs) GetChildren(parentID uint64) ([]VfsDirEntry, error) { rows, err := v.DB.Query("SELECT id, name, type FROM vfs_nodes WHERE parent_id = ?", parentID) @@ -253,6 +396,47 @@ func ParsePathComponents(pathStr string) (parentPath, nodeName string, nodeType return parentPath, nodeName, nodeType, nil } +// MoveToPath 将节点移动到指定路径 +func (v *Vfs) MoveToPath(node *VfsNode, destPath string) error { + // 1. 解析目标路径 + parentPath, nodeName, _, err := ParsePathComponents(destPath) + if err != nil { + return fmt.Errorf("invalid destination path: %w", err) + } + + // 2. 查找目标父节点 + parentID, err := v.GetParentID(parentPath) + if err != nil { + return fmt.Errorf("failed to find parent directory '%s': %w", parentPath, err) + } + + // 3. 检查目标位置是否已存在同名节点 + _, err = v.GetNodeByParentIDAndName(parentID, nodeName) + if err == nil { + return fmt.Errorf("destination path '%s' already exists", destPath) + } + if err.Error() != "node not found" { + return fmt.Errorf("error checking destination path: %w", err) + } + + // 4. 更新节点的父节点ID和名称 + node.ParentID = parentID + node.Name = nodeName + node.UpdatedAt = time.Now() + + // 5. 更新数据库中的节点信息 + _, err = v.DB.Exec(` + UPDATE vfs_nodes + SET name = ?, parent_id = ?, updated_at = ? + WHERE id = ?`, + node.Name, node.ParentID, node.UpdatedAt, node.ID) + if err != nil { + return fmt.Errorf("failed to update node: %w", err) + } + + return nil +} + func (v *Vfs) CreateVFSNode(p *VfsNode) error { _, err := v.DB.Exec(` INSERT INTO vfs_nodes (name, parent_id, type, created_at, updated_at) diff --git a/internal/models/vfs_test.go b/internal/vfs/models/vfs_test.go similarity index 98% rename from internal/models/vfs_test.go rename to internal/vfs/models/vfs_test.go index 3e97613..4714023 100644 --- a/internal/models/vfs_test.go +++ b/internal/vfs/models/vfs_test.go @@ -3,7 +3,7 @@ package models_test import ( "testing" - "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs/models" ) func TestParsePathComponents(t *testing.T) { diff --git a/internal/vfs/vfs.go b/internal/vfs/vfs.go new file mode 100644 index 0000000..890af00 --- /dev/null +++ b/internal/vfs/vfs.go @@ -0,0 +1,360 @@ +package vfs + +import ( + "log" + "net/http" + "os" + "path/filepath" + "sync" + + api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs/models" + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + fileadapter "github.com/casbin/casbin/v2/persist/file-adapter" + "github.com/gin-gonic/gin" +) + +type VfsImpl struct { + vfs *models.Vfs + enfocer *casbin.Enforcer + config VFSConfig + proxyTable []*ProxyEntry // 动态代理表 + proxyMutex sync.RWMutex // 保护代理表的读写锁 +} + +func NewVfsHandler(config *Config) (*VfsImpl, error) { + var err error + + vfs, err := models.NewVfs(config.VFS.DbPath) + if err != nil { + return nil, err + } + + var policyPath = config.VFS.PolicyPath + // 检查策略文件是否存在,如果不存在则创建 + if _, err := os.Stat(policyPath); os.IsNotExist(err) { + // 确保目录存在 + dir := filepath.Dir(policyPath) + if err := os.MkdirAll(dir, 0755); err != nil { + log.Fatalf("error: failed to create policy directory: %s", err) + } + + // 创建空的策略文件 + file, err := os.Create(policyPath) + if err != nil { + log.Fatalf("error: failed to create policy file: %s", err) + } + file.Close() + + log.Printf("Created policy file: %s", policyPath) + } + + a := fileadapter.NewAdapter(policyPath) + if a == nil { + log.Fatalf("error: adapter: %s", err) + } + + m, err := model.NewModelFromString(CasbinModel) + if err != nil { + log.Fatalf("error: model: %s", err) + } + + e, err := casbin.NewEnforcer(m, a) + if err != nil { + return nil, err + } + + log.Printf("Admin Token: %s", config.VFS.AdminToken) + log.Printf("Register Token: %s", config.VFS.RegisterToken) + + return &VfsImpl{ + vfs: vfs, + enfocer: e, + config: config.VFS, + proxyTable: make([]*ProxyEntry, 0), + proxyMutex: sync.RWMutex{}, + }, nil +} + +func ModelType2ResponseType(nodeType models.VfsNodeType) api.VFSNodeType { + var reponseType api.VFSNodeType + switch nodeType { + case models.VfsNodeTypeFile: + reponseType = api.File + case models.VfsNodeTypeDirectory: + reponseType = api.Directory + case models.VfsNodeTypeService: + reponseType = api.Service + } + return reponseType +} + +// CreateVFSNode implements api.ServerInterface. +func (v *VfsImpl) CreateVFSNode(c *gin.Context, params api.CreateVFSNodeParams) { + if !v.CheckPermissionMiddleware(c, params.Path) { + return + } + + // 解析路径组件 + parentPath, nodeName, nodeType, err := models.ParsePathComponents(params.Path) + if err != nil { + c.JSON(http.StatusBadRequest, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } + + // 创建节点 + node, err := v.vfs.CreateNodeByComponents(parentPath, nodeName, nodeType) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "CreateNodeByComponents", + Message: err.Error(), + }) + return + } + + if nodeType == models.VfsNodeTypeService { + if !v.Proxy2Service(c, node) { + // Rollback + err := v.vfs.DeleteVFSNode(node) + if err != nil { + // FIXME: 需要解决这种原子性 + panic("Maybe Consistency Error") + } + return + } + } + + // 返回创建成功的节点 + c.JSON(http.StatusCreated, api.VFSNodeResponse{ + Name: node.Name, + Type: ModelType2ResponseType(node.Type), + CreatedAt: node.CreatedAt, + UpdatedAt: node.UpdatedAt, + }) +} + +// GetVFSNode implements api.ServerInterface. +func (v *VfsImpl) GetVFSNode(c *gin.Context, params api.GetVFSNodeParams) { + if !v.CheckPermissionMiddleware(c, params.Path) { + return + } + + node, err := v.vfs.GetNodeByPath(params.Path) + if err != nil { + c.JSON(http.StatusNotFound, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } + + switch node.Type { + case models.VfsNodeTypeDirectory: + if entries, err := v.vfs.GetChildren(node.ID); err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } else { + var responseEntries []api.VFSDirectoryEntry + for _, entry := range entries { + responseEntries = append(responseEntries, api.VFSDirectoryEntry{ + Name: entry.Name, + Type: ModelType2ResponseType(entry.Type), + }) + } + c.JSON(http.StatusOK, responseEntries) + return + } + case models.VfsNodeTypeService: + v.Proxy2Service(c, node) + default: + c.JSON(http.StatusBadRequest, api.Error{ + Errtype: "error", + Message: "Not a valid node type", + }) + } +} + +// DeleteVFSNode implements api.ServerInterface. +func (v *VfsImpl) DeleteVFSNode(c *gin.Context, params api.DeleteVFSNodeParams) { + if !v.CheckPermissionMiddleware(c, params.Path) { + return + } + + node, err := v.vfs.GetNodeByPath(params.Path) + if err != nil { + c.JSON(http.StatusNotFound, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } + + switch node.Type { + case models.VfsNodeTypeService: + if !v.Proxy2Service(c, node) { + return + } + case models.VfsNodeTypeDirectory: + if children, err := v.vfs.GetChildren(node.ID); err != nil || len(children) != 0 { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: "the folder is not empty", + }) + return + } + default: + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: "node type not supported", + }) + return + } + if err := v.vfs.DeleteVFSNode(node); err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } + c.JSON(http.StatusNoContent, nil) +} + +// UpdateVFSNode implements api.ServerInterface. +func (v *VfsImpl) UpdateVFSNode(c *gin.Context, params api.UpdateVFSNodeParams) { + if !v.CheckPermissionMiddleware(c, params.Path) { + return + } + + var req api.UpdateVFSNodeJSONRequestBody + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, api.Error{ + Errtype: "ParameterError", + Message: "Invalid request parameters", + }) + return + } + + node, err := v.vfs.GetNodeByPath(params.Path) + if err != nil { + c.JSON(http.StatusNotFound, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } + + switch params.Op { + case api.Rename: + if req == "" { + c.JSON(http.StatusBadRequest, api.Error{ + Errtype: "ParameterError", + Message: "Invalid request parameters", + }) + return + } + // FIXME: 对于service,后缀属性需要强制保留 + if err := v.vfs.UpdateVFSNode(node); err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } + case api.Change: + if node.Type != models.VfsNodeTypeFile { + c.JSON(http.StatusBadRequest, api.Error{ + Errtype: "ParameterError", + Message: "Invalid request parameters, node type must be a service", + }) + return + } + if !v.Proxy2Service(c, node) { + return + } + case api.Move: + // FIXME: 需要添加权限控制 + v.vfs.MoveToPath(node, req) + + case api.Copy: + fallthrough + default: + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: "op type not supported", + }) + return + } +} + +// CreateUser implements api.ServerInterface. +func (v *VfsImpl) CreateUser(c *gin.Context, username string) { + token := c.GetHeader("X-VFS-Token") + if token != v.config.RegisterToken || token != v.config.AdminToken { + c.JSON(http.StatusForbidden, api.Error{ + Errtype: "AccessDenied", + Message: "Access denied", + }) + return + } + + token, err := v.vfs.CreateUser(username) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "CreateUserError", + Message: err.Error(), + }) + return + } + + _, err = v.enfocer.AddRoleForUser(username, "user") + if err != nil { + log.Printf("Failed to add role for user %s: %v", username, err) + } + v.enfocer.SavePolicy() + + // 根据API文档,token应该通过响应头返回 + c.Header("X-VFS-Token", token) + c.Status(http.StatusCreated) +} + +// DeleteUser implements api.ServerInterface. +func (v *VfsImpl) DeleteUser(c *gin.Context, username string) { + token := c.GetHeader("X-VFS-Token") + user, err := v.vfs.GetUserByToken(token) + if err != nil || user.Name != username { + c.JSON(http.StatusForbidden, api.Error{ + Errtype: "AccessDenied", + Message: "Access denied", + }) + return + } + + err = v.vfs.DeleteUser(username) + if err != nil { + if err.Error() == "user not found" { + c.JSON(http.StatusNotFound, api.Error{ + Errtype: "UserNotFoundError", + Message: "User not found", + }) + return + } + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "DeleteUserError", + Message: err.Error(), + }) + return + } + + // 根据API文档,删除成功返回204状态码 + c.Status(http.StatusNoContent) +} + +// Make sure we conform to ServerInterface +var _ api.ServerInterface = (*VfsImpl)(nil) diff --git a/internal/vfs/vfs_auth.go b/internal/vfs/vfs_auth.go new file mode 100644 index 0000000..07665e3 --- /dev/null +++ b/internal/vfs/vfs_auth.go @@ -0,0 +1,89 @@ +package vfs + +import ( + _ "embed" + "net/http" + "strings" + + api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs/models" + "github.com/gin-gonic/gin" +) + +//go:embed vfs_model.conf +var CasbinModel string + +func NewVfsPermission() (*api.GinServerOptions, error) { + return &api.GinServerOptions{ + Middlewares: []api.MiddlewareFunc{VfsMiddleware()}, + }, nil +} + +func (v *VfsImpl) CheckPermission(token, path, action string) (bool, error) { + // 根据 token 获取用户信息 + user, err := v.vfs.GetUserByToken(token) + if err != nil { + // 匿名用户 + user = &models.VfsUser{Name: "", Token: ""} + } + + // 特殊处理:admin 用户拥有所有权限 + if token == v.config.AdminToken && len(token) != 0 { // admin 用户拥有所有权限 + return true, nil + } + + // 允许任何人读取 public 目录 + if strings.HasPrefix(path, "/public") && action == "GET" { + return true, nil + } + + // 如果是普通用户访问自己的主目录,则允许 + if user.Name != "" && strings.HasPrefix(path, "/home/"+user.Name) { + return true, nil + } + + // 构造 Casbin 请求 + // 对于普通用户,需要将策略中的 {{username}} 替换为实际用户名 + obj := path + sub := user.Name + // 使用 Casbin 检查权限 + allowed, err := v.enfocer.Enforce(sub, obj, action) + if err != nil { + return false, err + } + + return allowed, nil +} + +func (v *VfsImpl) CheckPermissionMiddleware(c *gin.Context, path string) bool { + token := c.GetHeader("X-VFS-Token") + allowed, err := v.CheckPermission(token, path, c.Request.Method) + // log.Println("CheckPermission:", allowed, err) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "PermissionCheckError", + Message: "Failed to check permission: " + err.Error(), + }) + return false + } + if !allowed { + c.JSON(http.StatusForbidden, api.Error{ + Errtype: "AccessDenied", + Message: "Access denied", + }) + return false + } + return true +} + +func VfsMiddleware() api.MiddlewareFunc { + return func(c *gin.Context) { + // 检查当前请求是否需要认证 + if _, exists := c.Get(api.ApiKeyAuthScopes); exists { + // 提取 API Key + apiKey := c.GetHeader("X-VFS-Token") + c.Set(api.ApiKeyAuthScopes, apiKey) + } + c.Next() + } +} diff --git a/internal/vfs/vfs_config.go b/internal/vfs/vfs_config.go new file mode 100644 index 0000000..32f061d --- /dev/null +++ b/internal/vfs/vfs_config.go @@ -0,0 +1,75 @@ +// internal/handlers/vfs/vfs_config.go +package vfs + +import ( + "fmt" + "log" + + "github.com/spf13/viper" +) + +type Config struct { + Server ServerConfig `mapstructure:"server"` + VFS VFSConfig `mapstructure:"vfs"` + Services ServicesConfig `mapstructure:"services"` +} + +type ServerConfig struct { + Address string `mapstructure:"address"` + Mode string `mapstructure:"mode"` +} + +type VFSConfig struct { + DbPath string `mapstructure:"db_path"` + PolicyPath string `mapstructure:"policy_path"` + AdminToken string `mapstructure:"admin_token"` + RegisterToken string `mapstructure:"regiser_token"` + Debug bool `mapstructure:"debug"` +} + +type ServicesConfig struct { + Bookmark BookmarkServiceConfig `mapstructure:"bookmark"` +} + +type BookmarkServiceConfig struct { + URL string `mapstructure:"url"` +} + +func genrateRandomToken(length int) string { + // 生成随机字符串 + return "random_token" +} + +// LoadConfig 加载配置 +func LoadConfig() (*Config, error) { + viper.SetConfigName("vfs_config") + viper.SetConfigType("yaml") + viper.AddConfigPath(".") + viper.AddConfigPath("./config") + viper.AddConfigPath("config") + + // 设置默认值 + viper.SetDefault("server.address", "localhost:8080") + viper.SetDefault("server.mode", "release") + viper.SetDefault("vfs.db_path", "./data/vfs.sqlite3") + viper.SetDefault("vfs.policy_path", "./data/policy.csv") + viper.SetDefault("vfs.debug", false) + viper.SetDefault("services.bookmark.url", "http://localhost:8081/api") + + viper.SetDefault("vfs.admin_token", genrateRandomToken(64)) + viper.SetDefault("vfs.regiser_token", genrateRandomToken(64)) + // 自动读取环境变量 + viper.AutomaticEnv() + + // 读取配置文件 + if err := viper.ReadInConfig(); err != nil { + log.Printf("Warning: unable to read config file: %v. Using defaults and environment variables.", err) + } + + var config Config + if err := viper.Unmarshal(&config); err != nil { + return nil, fmt.Errorf("unable to decode into struct: %v", err) + } + + return &config, nil +} diff --git a/config/model.conf b/internal/vfs/vfs_model.conf similarity index 100% rename from config/model.conf rename to internal/vfs/vfs_model.conf diff --git a/internal/vfs/vfs_service.go b/internal/vfs/vfs_service.go new file mode 100644 index 0000000..bf56310 --- /dev/null +++ b/internal/vfs/vfs_service.go @@ -0,0 +1,155 @@ +package vfs + +import ( + "net/http" + "strings" + + api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs/models" + "github.com/gin-gonic/gin" +) + +// ServiceProxy 服务代理接口 +type ServiceProxy interface { + // Get 从后端服务获取数据 + Get(c *gin.Context, servicePath string, node *models.VfsNode) (any, error) + + // Create 在后端服务创建资源 + Create(c *gin.Context, servicePath string, node *models.VfsNode, data []byte) (string, error) // 返回创建的资源ID + + // Update 更新后端服务资源 + Update(c *gin.Context, servicePath string, node *models.VfsNode, data []byte) error + + // Delete 删除后端服务资源 + Delete(c *gin.Context, servicePath string, node *models.VfsNode) error + + // GetName 获取代理名称 + GetName() string +} + +// ProxyEntry 代理表条目 +type ProxyEntry struct { + Name string + MatchExt string + Proxy ServiceProxy // 对应的代理实现 +} + +// FindProxyByServiceName 根据服务节点名称查找对应的代理 +func (v *VfsImpl) FindProxyByServiceName(serviceName string) ServiceProxy { + v.proxyMutex.RLock() + defer v.proxyMutex.RUnlock() + + if serviceName == "" { + return nil + } + + // 根据服务名称匹配前缀 + for _, entry := range v.proxyTable { + if entry.MatchExt == serviceName { + return entry.Proxy + } + } + + return nil +} + +func (v *VfsImpl) RegisterProxy(entry *ProxyEntry) { + v.proxyMutex.Lock() + defer v.proxyMutex.Unlock() + + v.proxyTable = append(v.proxyTable, entry) +} + +// Proxy2Service 通用服务代理处理函数 +func (v *VfsImpl) Proxy2Service(c *gin.Context, node *models.VfsNode) bool { + exts := strings.Split(node.Name, ".") + var serviceName = exts[1] + // log.Println("Proxy2Service: ", serviceName) + // 查找对应的代理 + proxy := v.FindProxyByServiceName(serviceName) + if proxy == nil { + c.JSON(http.StatusNotImplemented, api.Error{ + Errtype: "error", + Message: "Service proxy not found for: " + serviceName, + }) + return false + } + + // 根据HTTP方法调用相应的代理方法 + switch c.Request.Method { + case http.MethodGet: + result, err := proxy.Get(c, serviceName, node) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: "Failed to get service data: " + err.Error(), + }) + return false + } + + c.JSON(http.StatusOK, result) + return true + case http.MethodPost: + // 读取请求体数据 + data, err := c.GetRawData() + if err != nil { + c.JSON(http.StatusBadRequest, api.Error{ + Errtype: "error", + Message: "Failed to read request data: " + err.Error(), + }) + return false + } + + resourceID, err := proxy.Create(c, serviceName, node, data) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: "Failed to create service resource: " + err.Error(), + }) + return false + } + + c.JSON(http.StatusCreated, gin.H{"resource_id": resourceID}) + return true + case http.MethodPut, http.MethodPatch: + // 读取请求体数据 + data, err := c.GetRawData() + if err != nil { + c.JSON(http.StatusBadRequest, api.Error{ + Errtype: "error", + Message: "Failed to read request data: " + err.Error(), + }) + return false + } + + err = proxy.Update(c, serviceName, node, data) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: "Failed to update service resource: " + err.Error(), + }) + return false + } + + c.JSON(http.StatusOK, gin.H{"message": "Updated successfully"}) + return true + case http.MethodDelete: + err := proxy.Delete(c, serviceName, node) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: "Failed to delete service resource: " + err.Error(), + }) + return false + } + + c.JSON(http.StatusNoContent, nil) + return true + default: + c.JSON(http.StatusMethodNotAllowed, api.Error{ + Errtype: "error", + Message: "Method not allowed", + }) + return false + } +} diff --git a/internal/handlers/vfs_driver/vfs_bookmark.go b/internal/vfs/vfsdriver/vfs_bookmark.go similarity index 90% rename from internal/handlers/vfs_driver/vfs_bookmark.go rename to internal/vfs/vfsdriver/vfs_bookmark.go index d9ae729..3930030 100644 --- a/internal/handlers/vfs_driver/vfs_bookmark.go +++ b/internal/vfs/vfsdriver/vfs_bookmark.go @@ -8,8 +8,8 @@ import ( "fmt" api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks" - "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers" - "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs/models" "github.com/gin-gonic/gin" ) @@ -17,15 +17,18 @@ type VfsBookMarkService struct { client *api.ClientWithResponses } -func NewVfsBookMarkService(serverURL string) (*VfsBookMarkService, error) { +func NewVfsBookMarkService(serverURL string) (*vfs.ProxyEntry, error) { client, err := api.NewClientWithResponses(serverURL) if err != nil { return nil, err } - return &VfsBookMarkService{ - client: client, - }, nil + ret := vfs.ProxyEntry{ + Name: "bookmark", + MatchExt: "bk", + Proxy: &VfsBookMarkService{client: client}, + } + return &ret, nil } // Create implements ServiceProxy. @@ -157,4 +160,4 @@ func (v *VfsBookMarkService) Update(c *gin.Context, servicePath string, node *mo return fmt.Errorf("unknown error") } -var _ handlers.ServiceProxy = (*VfsBookMarkService)(nil) +var _ vfs.ServiceProxy = (*VfsBookMarkService)(nil) diff --git a/magefile.go b/magefile.go index 8521973..7536a9c 100644 --- a/magefile.go +++ b/magefile.go @@ -10,18 +10,23 @@ import ( // mg contains helpful utility functions, like Deps ) +//go:generate go tool oapi-codegen -config config/bookmark/client.yaml config/bookmark/bookmark.yaml +//go:generate go tool oapi-codegen -config config/bookmark/server.yaml config/bookmark/bookmark.yaml +//go:generate go tool oapi-codegen -config config/vfs/server.yaml config/vfs/vfs.yaml +//go:generate go tool oapi-codegen -config config/vfs/client.yaml config/vfs/vfs.yaml +//go:generate go tool oapi-codegen -config config/user_np/server.yaml config/user_np/user_np.yaml + // Default target to run when none is specified // If not set, running mage will list available targets // var Default = Build -func BuildAll() error { +func Build_All() error { services := []struct { Name string Path string }{ - {"openapi", "."}, + {"vfs_api", "."}, {"bookmark", "./cmd/bookmark"}, - {"user_np", "./cmd/user_np"}, } platforms := []struct { @@ -53,9 +58,7 @@ func BuildAll() error { // 使用 release 模式构建并显示链接信息 cmd := exec.Command("go", "build", - "-x", "-ldflags", "-s -w -extldflags -static", // 去除调试信息,减小体积 - "-v", // 显示编译过程中的包信息 "-o", outputName, service.Path) cmd.Env = env diff --git a/main.go b/main.go index 7f3ded1..aa302a8 100644 --- a/main.go +++ b/main.go @@ -6,25 +6,31 @@ import ( "log" "net/http" - vfs "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs" + api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs" "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers" - vfsdriver "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers/vfs_driver" - "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs/vfsdriver" "github.com/gin-gonic/gin" + "github.com/spf13/viper" ) -//go:generate go tool oapi-codegen -config config/bookmark/client.yaml config/bookmark/bookmark.yaml -//go:generate go tool oapi-codegen -config config/bookmark/server.yaml config/bookmark/bookmark.yaml -//go:generate go tool oapi-codegen -config config/vfs/server.yaml config/vfs/vfs.yaml -//go:generate go tool oapi-codegen -config config/vfs/server.yaml config/vfs/vfs.yaml -//go:generate go tool oapi-codegen -config config/user_np/server.yaml config/user_np/user_np.yaml - //go:embed config/* dist/* var staticFiles embed.FS func main() { - gin.SetMode(gin.ReleaseMode) + config, err := vfs.LoadConfig() + if err != nil { + log.Fatal("Failed to load config:", err) + } + + viper.SafeWriteConfigAs("vfs_config.yaml") + + if config.Server.Mode == "debug" { + gin.SetMode(gin.DebugMode) + } else { + gin.SetMode(gin.ReleaseMode) + } router := gin.Default() router.GET("/ping", func(c *gin.Context) { @@ -35,18 +41,16 @@ func main() { api_router := router.Group("/api") { - if vfsImpl, err := models.NewVfs("./data/vfs.sqlite3"); err != nil { - log.Fatal("Failed to create vfs server:", err) - } else if server, err := handlers.NewVfsHandler(*vfsImpl); err != nil { + if server, err := vfs.NewVfsHandler(config); err != nil { log.Fatal("Failed to create bookmarks server:", err) } else { - vfs.RegisterHandlers(api_router, server) + api.RegisterHandlers(api_router, server) // 示例:在你的服务初始化代码中 bookmarkService, err := vfsdriver.NewVfsBookMarkService("http://localhost:8081/api") // 替换为实际的 bookmark 服务地址 if err != nil { log.Fatal("Failed to create bookmark service client:", err) } - server.RegisterProxy(&handlers.ProxyEntry{Name: "bk", Proxy: bookmarkService}) + server.RegisterProxy(bookmarkService) } handlers.TodoHandler(api_router) } @@ -82,7 +86,7 @@ func main() { r.URL.Path = originalPath }) - var listener = "localhost:8080" + var listener = config.Server.Address log.Printf("Starting server at http://%s", listener) log.Printf("Swagger UI: http://%s/swagger/index.html", listener) log.Fatal(router.Run(listener))