diff --git a/data/bindata.go b/data/bindata.go index 92ae71ed..af78e2fd 100644 --- a/data/bindata.go +++ b/data/bindata.go @@ -90,6 +90,7 @@ // .tmp/data/yao/models/dsl.mod.yao // .tmp/data/yao/models/history.mod.yao // .tmp/data/yao/models/kb.mod.yao +// .tmp/data/yao/models/user.mod.yao // .tmp/data/yao/release/app.yaz package data @@ -269,7 +270,7 @@ func cuiSetupIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/setup/index.html", size: 10, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "cui/setup/index.html", size: 10, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -289,7 +290,7 @@ func cuiV09IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v0.9/index.html", size: 13, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "cui/v0.9/index.html", size: 13, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -309,7 +310,7 @@ func cuiV10IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/index.html", size: 49, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "cui/v1.0/index.html", size: 49, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -329,7 +330,7 @@ func cuiV10Layouts__indexAsyncJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "cui/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -349,7 +350,7 @@ func cuiV10UmiJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "cui/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -369,7 +370,7 @@ func initEnv() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.env", size: 219, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.env", size: 219, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -389,7 +390,7 @@ func initVscodeSettingsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/settings.json", size: 4666, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/settings.json", size: 4666, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -409,7 +410,7 @@ func initVscodeTypesRuntimeConsoleDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/console.d.ts", size: 221, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/console.d.ts", size: 221, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -429,7 +430,7 @@ func initVscodeTypesRuntimeExceptionDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/exception.d.ts", size: 738, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/exception.d.ts", size: 738, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -449,7 +450,7 @@ func initVscodeTypesRuntimeFsDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/fs.d.ts", size: 8554, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/fs.d.ts", size: 8554, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -469,7 +470,7 @@ func initVscodeTypesRuntimeGlobalDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/global.d.ts", size: 1759, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/global.d.ts", size: 1759, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -489,7 +490,7 @@ func initVscodeTypesRuntimeHttpDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/http.d.ts", size: 6179, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/http.d.ts", size: 6179, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -509,7 +510,7 @@ func initVscodeTypesRuntimeIoDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/io.d.ts", size: 587, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/io.d.ts", size: 587, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -529,7 +530,7 @@ func initVscodeTypesRuntimeLogDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/log.d.ts", size: 1692, mode: os.FileMode(493), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/log.d.ts", size: 1692, mode: os.FileMode(493), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -549,7 +550,7 @@ func initVscodeTypesRuntimeNeoDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/neo.d.ts", size: 3750, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/neo.d.ts", size: 3750, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -569,7 +570,7 @@ func initVscodeTypesRuntimeProcessFsDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/fs.d.ts", size: 11133, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/fs.d.ts", size: 11133, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -589,7 +590,7 @@ func initVscodeTypesRuntimeProcessHttpDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/http.d.ts", size: 5653, mode: os.FileMode(493), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/http.d.ts", size: 5653, mode: os.FileMode(493), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -609,7 +610,7 @@ func initVscodeTypesRuntimeProcessModelDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/model.d.ts", size: 6656, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/model.d.ts", size: 6656, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -629,7 +630,7 @@ func initVscodeTypesRuntimeProcessDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process.d.ts", size: 23165, mode: os.FileMode(493), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process.d.ts", size: 23165, mode: os.FileMode(493), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -649,7 +650,7 @@ func initVscodeTypesRuntimeQueryDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/query.d.ts", size: 6124, mode: os.FileMode(493), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/query.d.ts", size: 6124, mode: os.FileMode(493), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -669,7 +670,7 @@ func initVscodeTypesRuntimeStoreDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/store.d.ts", size: 2251, mode: os.FileMode(493), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/store.d.ts", size: 2251, mode: os.FileMode(493), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -689,7 +690,7 @@ func initVscodeTypesRuntimeSuiDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/sui.d.ts", size: 1713, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/sui.d.ts", size: 1713, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -709,7 +710,7 @@ func initVscodeTypesRuntimeTimeDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/time.d.ts", size: 711, mode: os.FileMode(493), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/time.d.ts", size: 711, mode: os.FileMode(493), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -729,7 +730,7 @@ func initVscodeTypesRuntimeDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime.d.ts", size: 424, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime.d.ts", size: 424, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -749,7 +750,7 @@ func initVscodeTypesSuiDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/sui.d.ts", size: 8931, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/.vscode/types/sui.d.ts", size: 8931, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -769,7 +770,7 @@ func initAppYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/app.yao", size: 3115, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/app.yao", size: 3115, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -789,7 +790,7 @@ func initDataReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/README.md", size: 41, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/data/README.md", size: 41, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -809,7 +810,7 @@ func initDataTemplatesDefault__assetsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -829,7 +830,7 @@ func initDataTemplatesDefault__assetsImagesIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -849,7 +850,7 @@ func initDataTemplatesDefault__assetsImagesLogosLogo_colorSvg() (*asset, error) return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -869,7 +870,7 @@ func initDataTemplatesDefault__assetsImagesLogosWordmarkSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -889,7 +890,7 @@ func initDataTemplatesDefault__dataJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__data.json", size: 30, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__data.json", size: 30, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -909,7 +910,7 @@ func initDataTemplatesDefault__documentHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__document.html", size: 492, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__document.html", size: 492, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -929,7 +930,7 @@ func initDataTemplatesDefaultIndexIndexCss() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.css", size: 2896, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.css", size: 2896, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -949,7 +950,7 @@ func initDataTemplatesDefaultIndexIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.html", size: 2361, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.html", size: 2361, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -969,7 +970,7 @@ func initDataTemplatesDefaultIndexIndexJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.json", size: 31, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.json", size: 31, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -989,7 +990,7 @@ func initDbReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/db/README.md", size: 84, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/db/README.md", size: 84, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1009,7 +1010,7 @@ func initFlowsMenuFlowYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/flows/menu.flow.yao", size: 813, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/flows/menu.flow.yao", size: 813, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1029,7 +1030,7 @@ func initFormsAccountFormYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/forms/account.form.yao", size: 1194, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/forms/account.form.yao", size: 1194, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1049,7 +1050,7 @@ func initIconsAppIcns() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/icons/app.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1069,7 +1070,7 @@ func initIconsAppIco() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/icons/app.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1089,7 +1090,7 @@ func initIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1109,7 +1110,7 @@ func initLoginsAdminLoginYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/logins/admin.login.yao", size: 302, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/logins/admin.login.yao", size: 302, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1129,7 +1130,7 @@ func initLogsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/logs/README.md", size: 28, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/logs/README.md", size: 28, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1149,7 +1150,7 @@ func initModelsAdminUserModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/models/admin/user.mod.yao", size: 6416, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/models/admin/user.mod.yao", size: 6416, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1169,7 +1170,7 @@ func initModelsTestsPetModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/models/tests/pet.mod.yao", size: 525, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/models/tests/pet.mod.yao", size: 525, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1189,7 +1190,7 @@ func initNeoNeoYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/neo/neo.yml", size: 724, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/neo/neo.yml", size: 724, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1209,7 +1210,7 @@ func initPublicReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/README.md", size: 108, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/public/README.md", size: 108, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1229,7 +1230,7 @@ func initPublicAssetsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/public/assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1249,7 +1250,7 @@ func initPublicAssetsImagesIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/public/assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1269,7 +1270,7 @@ func initPublicAssetsImagesLogosLogo_colorSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/public/assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1289,7 +1290,7 @@ func initPublicAssetsImagesLogosWordmarkSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/public/assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1309,7 +1310,7 @@ func initPublicAssetsLibsuiMinJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/libsui.min.js", size: 12569, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/public/assets/libsui.min.js", size: 12569, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1329,7 +1330,7 @@ func initPublicAssetsLibsuiMinJsMap() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/libsui.min.js.map", size: 38553, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/public/assets/libsui.min.js.map", size: 38553, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1349,7 +1350,7 @@ func initPublicIndexCfg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/index.cfg", size: 85, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/public/index.cfg", size: 85, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1369,7 +1370,7 @@ func initPublicIndexSui() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/index.sui", size: 5682, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/public/index.sui", size: 5682, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1389,7 +1390,7 @@ func initScriptsAccountTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/account.ts", size: 2521, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/scripts/account.ts", size: 2521, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1409,7 +1410,7 @@ func initScriptsAiNeoTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/ai/neo.ts", size: 375, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/scripts/ai/neo.ts", size: 375, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1429,7 +1430,7 @@ func initScriptsTestsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/tests.ts", size: 1044, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/scripts/tests.ts", size: 1044, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1449,7 +1450,7 @@ func initScriptsUtilsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/utils.ts", size: 1230, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/scripts/utils.ts", size: 1230, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1469,7 +1470,7 @@ func initSuisWebSuiYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/suis/web.sui.yao", size: 675, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/suis/web.sui.yao", size: 675, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1489,7 +1490,7 @@ func initTablesAccountTabYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/tables/account.tab.yao", size: 5597, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/tables/account.tab.yao", size: 5597, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1509,7 +1510,7 @@ func initTsconfigJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/tsconfig.json", size: 178, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "init/tsconfig.json", size: 178, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1529,7 +1530,7 @@ func libsuiAgentTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/agent.ts", size: 15267, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "libsui/agent.ts", size: 15267, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1549,7 +1550,7 @@ func libsuiIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/index.ts", size: 13049, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "libsui/index.ts", size: 13049, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1569,7 +1570,7 @@ func libsuiUtilsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/utils.ts", size: 5959, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "libsui/utils.ts", size: 5959, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1589,7 +1590,7 @@ func libsuiYaoTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/yao.ts", size: 4338, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "libsui/yao.ts", size: 4338, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1609,7 +1610,7 @@ func publicIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "public/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "public/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1629,7 +1630,7 @@ func uiIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1649,7 +1650,7 @@ func yaoDataIcons404Png() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1669,7 +1670,7 @@ func yaoDataIconsIconIcns() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1689,7 +1690,7 @@ func yaoDataIconsIconIco() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1709,7 +1710,7 @@ func yaoDataIconsIconPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1729,7 +1730,7 @@ func yaoDataIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1749,7 +1750,7 @@ func yaoFieldsModelTransJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14938, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14938, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1769,7 +1770,7 @@ func yaoLangsEnUsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1789,7 +1790,7 @@ func yaoLangsZhCnGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1809,7 +1810,7 @@ func yaoLangsZhCnLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1829,7 +1830,7 @@ func yaoLangsZhCnLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1849,7 +1850,7 @@ func yaoLangsZhHkGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1869,7 +1870,7 @@ func yaoLangsZhHkLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1889,7 +1890,7 @@ func yaoLangsZhHkLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1909,7 +1910,7 @@ func yaoModelsAssistantModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/assistant.mod.yao", size: 4114, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/models/assistant.mod.yao", size: 4114, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1929,7 +1930,7 @@ func yaoModelsAttachmentModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/attachment.mod.yao", size: 3264, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/models/attachment.mod.yao", size: 3264, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1949,7 +1950,7 @@ func yaoModelsAuditModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/audit.mod.yao", size: 5588, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/models/audit.mod.yao", size: 5588, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1969,7 +1970,7 @@ func yaoModelsChatModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/chat.mod.yao", size: 1444, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/models/chat.mod.yao", size: 1444, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1989,7 +1990,7 @@ func yaoModelsConfigModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/config.mod.yao", size: 1649, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/models/config.mod.yao", size: 1649, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2009,7 +2010,7 @@ func yaoModelsDslModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/dsl.mod.yao", size: 3806, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/models/dsl.mod.yao", size: 3806, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2029,7 +2030,7 @@ func yaoModelsHistoryModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/history.mod.yao", size: 2902, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/models/history.mod.yao", size: 2902, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2049,7 +2050,27 @@ func yaoModelsKbModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/kb.mod.yao", size: 2881, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/models/kb.mod.yao", size: 2881, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _yaoModelsUserModYao = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xc4\x59\x5f\x6f\xd4\x38\x10\x7f\xef\xa7\x18\xe5\xa9\x48\x3d\x89\x16\x15\x9d\xfa\xb6\xfc\x13\x48\x85\x56\x5d\x38\x74\x42\x68\xe5\x8d\x27\x1b\x83\x63\xe7\xfc\xa7\x65\x0f\xf5\xbb\x9f\x6c\x27\xa9\x37\x71\xb7\x49\x0a\xba\xa7\x4d\xed\x99\xf1\xef\x37\x1e\xcf\x8c\xdd\x9f\x07\x00\x99\x20\x15\x66\x67\x90\x5d\x2c\xac\x29\xe1\x93\x46\x95\x1d\xb9\x71\x4e\xd6\xc8\x53\x13\x14\x75\xae\x58\x6d\x98\x14\x77\xd3\x56\xa3\x82\x4a\x52\xe4\x50\x48\x05\xc4\x9a\x12\x85\x61\x39\x71\x62\x40\x04\xf5\x43\x52\xb1\x7f\xfd\x48\xb0\x64\xc8\x46\x67\x67\xf0\x25\x93\x6e\x32\x3b\x82\xac\xfd\x75\xe6\xb2\xaf\x8d\xd0\x9a\x3b\x80\x0e\x6c\x04\xd7\xab\xac\x9c\x9c\xf6\xc6\x00\xb2\x5c\x56\x15\x0a\xb3\x0b\x4a\x83\x37\x30\x0e\xd5\x01\xc0\xad\x5f\x34\x97\xdc\x56\xc2\x83\xf3\xb6\xc3\xe2\xd1\xf2\x8c\x36\xab\x3a\x84\xdb\xda\x8f\xbd\x7b\x75\x37\xd6\x79\x2f\x1e\x8c\x00\x5e\x2a\x56\x11\xb5\x85\xef\xb8\x05\x46\x1d\xa8\x82\x35\x0e\xf6\xa2\x75\x98\xcf\xce\xc0\x28\x8b\x7e\xf4\xf6\x28\x0d\x45\xdb\xf5\x37\xcc\xcd\x10\x8f\x36\x8a\x89\x4d\x02\xd3\xb2\xaf\x31\xf0\x5c\x63\x33\x82\x06\x87\xda\xae\x21\xe7\x84\x55\x4f\x22\x93\x28\x36\xa6\xcc\xce\xe0\xe4\xf4\xb4\x1b\x14\x96\xf3\x66\xd3\x1c\xf8\x6e\xdc\x0a\xf6\x8f\x1d\x8c\x32\x41\xf1\xc7\x18\x9e\x6e\x37\xfd\xf7\x78\xa2\x9f\x06\x2a\x11\x53\x37\x09\x5c\x6e\x98\x80\xa1\xe9\x8e\xd8\xf1\xd3\xa7\xbf\x9b\x18\x56\x84\xf1\x09\xac\x5e\xef\xca\xf7\x29\x79\x73\x40\x28\x55\xa8\xf5\xff\xb3\x55\x35\xd1\xfa\x46\x2a\xba\x2a\x89\x2e\x27\x30\xbb\x6c\xf4\xe0\xed\x8e\x5e\xc4\xd0\x4d\x20\x85\x76\x81\xc4\xb1\x9e\xc9\x38\x57\xdb\x3a\x9c\xcc\xc5\x72\xf9\xf9\xe2\xea\x55\xb6\x97\x61\xc1\x94\x36\xab\x89\xe1\xf8\xc6\x29\xc1\x87\xbd\x01\xe9\x0d\xc3\xf4\x60\xdc\x8b\x96\x93\xe9\x60\xcf\xc9\x83\x58\x9d\xd9\x5f\x0d\xb5\xb0\x9c\x4f\xf6\xab\xe5\xfc\x21\xb7\x3a\x11\xca\x74\xcd\xc9\xf6\x3e\xc8\x27\x33\x21\x93\x6b\x62\x88\x5a\x59\x35\xe5\x10\x2f\xbc\x12\x7c\xba\x3a\x4f\x83\xbe\x3a\x07\x23\x43\x59\xad\x95\x2c\x18\x47\xa8\x59\x6e\xac\x4a\x21\x3f\x9d\x89\xbc\x92\x6b\xc6\xa7\x78\xfa\x7d\x4f\xa1\xef\xe6\x60\x10\xea\x52\x0a\x04\x61\xab\x75\x5c\xd3\x22\xb8\x0f\x1e\xc7\xd1\xa9\x66\x90\xe7\x5a\x0e\x06\x7f\x98\x94\xdf\xfb\xf2\x7d\x0a\x8d\x41\x60\xa2\x90\xaa\xea\xa5\x94\x29\xce\xd5\xb9\xac\x31\x01\xec\x9b\xde\x49\x52\x5d\x51\xee\x89\x47\xb8\x16\xd7\x84\xf9\x65\xa1\xa9\xce\x5e\xd4\xe7\x3e\x53\x32\xed\xa3\x64\x26\x46\x43\x8c\x4d\x60\x44\x61\xab\x14\xc6\x9e\xf8\xc0\x77\x79\x2e\xad\x30\xd0\x37\x2b\xdb\x6e\xf1\x4b\x46\x72\xc3\xae\xd1\xf5\x78\x4c\xdc\x7d\x6b\xab\x6b\x14\x14\xa9\xfb\xc3\x7d\xb9\xf0\xfb\xda\xe9\x53\x2c\x88\xe5\x7e\x9d\x76\x32\x19\x29\x29\x1f\x14\x84\xeb\x11\xf5\x77\x75\x8d\xca\x35\x3a\x89\xbe\x6e\x2d\x25\x47\x92\xda\x33\x5f\x89\xe1\xaf\x81\x66\xe4\x97\xcf\x25\x9a\x12\x55\x38\xc9\xa1\x34\x33\x0d\xc3\xc5\xee\x28\x7a\xc0\xd3\x0f\x42\x38\x7a\xb3\x68\x84\x53\x3d\x81\xc7\xce\x31\xff\x3d\x74\xcc\x8d\x5c\x15\x24\x37\x52\xad\x50\xb8\xad\x9c\xc4\xe8\xe3\x8d\x84\x37\x5e\x1b\x5e\xf7\xb5\x13\xa4\xcc\x8d\xfc\x23\x2c\xd6\xbf\x23\x30\x0d\x83\xe5\x7f\x25\x37\x8d\xb9\xc2\x29\xbd\x7b\xc4\x6c\xd9\xd3\x8d\x88\x7d\xbc\xf8\x78\x09\xba\x24\x0a\x29\x84\x25\xfc\x4d\xe3\xf0\x05\xd1\xf8\xec\x04\x50\xe4\x92\x22\x9d\xdb\xc8\x77\xbd\xd2\xe2\xf5\x72\x7f\x9b\x14\x31\x65\x5a\xdb\x38\x4d\x4d\x61\xfa\xae\xa7\x1b\x31\x0d\x53\xbe\xa4\xb7\xf5\x1d\x29\x30\x11\x6f\xa4\xdb\xd6\xba\x9e\xd9\xdc\x47\xb9\xe7\xef\xc5\x45\xc8\xc0\xa3\x49\x13\xbe\x91\x8a\x99\xb2\x1a\x9d\x62\x23\xd6\x8b\xa1\x72\x7f\x8b\x3b\xfb\x70\xb8\x7c\xbb\x38\x3e\x82\xe5\xdb\xc5\xc9\xe9\x73\xff\x7b\x7a\x7c\xf2\x24\x99\x82\x9d\xa4\xcb\xb3\x41\xb6\xf9\x3a\x3d\x3e\x49\x27\xdc\x20\x3d\xa7\xb8\x44\x7e\xa0\x6c\xc3\x4c\xa2\xce\x30\x61\x70\xb3\xd3\x23\x24\xfc\xf0\xaa\xa7\x1c\x39\xe1\x83\x6f\x31\x40\x16\x10\x56\x70\x1b\xef\x1d\xe3\xe2\x1b\x0e\x9f\x83\x54\xf0\xe7\x93\xd4\xd1\x7d\xfe\x58\x4a\x35\x2a\x26\x13\x49\x69\x14\xa5\xcb\x9e\x72\x7f\x5f\x0d\xab\x10\xc2\x0a\x8e\x92\xc6\x5c\x0a\xaa\xe1\xd0\x6a\x4b\x38\xdf\xc2\xb3\xa7\x49\x52\xcf\xe6\xb5\x81\x71\xc0\x86\x0a\x3e\xb5\x03\x8f\xa3\xb6\xe9\x01\xee\xed\xc7\x5b\x81\x11\x47\xf6\x8e\x70\x7b\x43\x77\x1b\xea\xab\xe8\xc4\xdc\x35\x96\xbf\xc2\x5c\x5e\xa3\xda\xae\x5c\x00\x8d\x6f\xde\x22\xfe\x57\x8d\x05\x78\xb9\x6b\x21\xf2\xc0\x0b\x92\x7f\xb7\x35\xb4\x6b\xf9\x60\x6d\xda\xb9\xfb\x0a\xd1\xa3\x4f\x60\x53\xc5\x56\x24\x51\x6c\x5c\xb4\x69\x43\xaa\x7a\x54\x25\x85\x45\xba\xe6\x7c\x2e\x51\xec\xa9\xa4\x37\x24\x51\x4a\x1f\xdd\xfe\x47\x0c\xfd\x25\xb7\xed\x44\x1e\xc3\xd3\xdf\x7c\xdb\x66\xe8\x3e\xb6\x5e\xc8\x1f\xd3\xfd\x94\x87\xad\xd1\xa3\x39\x7b\xa2\xfe\xe1\x6a\x32\x4b\x8f\xfa\xdc\xbf\x79\xed\xe3\x15\x5e\xc5\x12\x86\x1e\x8d\xbd\x7b\x19\xca\x4b\x22\x36\x33\xf6\xa9\x7b\x22\x7a\x19\x0c\xec\x8d\xc6\xee\x99\xc8\xed\x84\x7f\xad\x68\x96\x9d\x77\x9c\x2a\x34\x84\x12\x43\x46\xa7\x85\xf7\x03\x85\x38\x0f\x52\xca\x5c\x98\x10\xde\x34\xd5\x8d\xb0\x7f\x97\xce\xad\x36\xb2\x82\x82\x21\xa7\x7a\x1e\xda\x5a\x61\x81\x0a\x45\x3e\x21\x8f\x5d\xa6\x74\xfa\xb7\xbc\xc8\xb0\xc7\xaa\xd1\x18\x26\x36\x0f\xc1\x3c\x00\x08\x2f\xfa\x3e\x58\x70\xef\xe3\xfa\x0f\xff\xb2\xbf\xba\x3b\xdd\x31\x9a\xee\x69\x3e\x79\x49\xb8\x2f\xeb\x7d\x4d\x54\x6a\x17\xb4\xc9\xa6\xd2\xcd\x80\xdc\x97\xce\xc2\x15\xd7\xf3\x77\xe1\xba\xbf\x23\xec\x08\x85\x6c\xd0\x4f\xe9\x31\xa5\xfe\x65\x74\x78\xb1\x9b\x45\x24\x5e\xb8\xc5\xee\x0a\x4e\xc1\xb8\x41\x5f\xd4\x7b\x9b\xa4\x90\x7b\x61\x87\xea\x67\xf8\xa7\xc8\x35\xe1\x36\x6c\x5b\x10\xe9\x5a\xca\x9f\xd1\x91\xd5\x6d\x6a\x80\x4c\xcb\xc2\xac\x28\x72\x34\xd8\x8e\xc2\xed\xc1\xed\xc1\x7f\x01\x00\x00\xff\xff\xb2\xe9\xef\x31\x77\x1a\x00\x00") + +func yaoModelsUserModYaoBytes() ([]byte, error) { + return bindataRead( + _yaoModelsUserModYao, + "yao/models/user.mod.yao", + ) +} + +func yaoModelsUserModYao() (*asset, error) { + bytes, err := yaoModelsUserModYaoBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "yao/models/user.mod.yao", size: 6775, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2069,7 +2090,7 @@ func yaoReleaseAppYaz() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/release/app.yaz", size: 181682, mode: os.FileMode(420), modTime: time.Unix(1752575260, 0)} + info := bindataFileInfo{name: "yao/release/app.yaz", size: 181682, mode: os.FileMode(420), modTime: time.Unix(1752809404, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2216,6 +2237,7 @@ var _bindata = map[string]func() (*asset, error){ "yao/models/dsl.mod.yao": yaoModelsDslModYao, "yao/models/history.mod.yao": yaoModelsHistoryModYao, "yao/models/kb.mod.yao": yaoModelsKbModYao, + "yao/models/user.mod.yao": yaoModelsUserModYao, "yao/release/app.yaz": yaoReleaseAppYaz, } @@ -2448,6 +2470,7 @@ var _bintree = &bintree{nil, map[string]*bintree{ "dsl.mod.yao": {yaoModelsDslModYao, map[string]*bintree{}}, "history.mod.yao": {yaoModelsHistoryModYao, map[string]*bintree{}}, "kb.mod.yao": {yaoModelsKbModYao, map[string]*bintree{}}, + "user.mod.yao": {yaoModelsUserModYao, map[string]*bintree{}}, }}, "release": {nil, map[string]*bintree{ "app.yaz": {yaoReleaseAppYaz, map[string]*bintree{}}, diff --git a/model/model.go b/model/model.go index bffbff8f..fd0d9aa5 100644 --- a/model/model.go +++ b/model/model.go @@ -28,6 +28,7 @@ var systemModels = map[string]string{ "__yao.dsl": "yao/models/dsl.mod.yao", "__yao.history": "yao/models/history.mod.yao", "__yao.kb": "yao/models/kb.mod.yao", + "__yao.user": "yao/models/user.mod.yao", } // Load load models diff --git a/openapi/oauth/oauth.go b/openapi/oauth/oauth.go index e00d8833..e148aa6c 100644 --- a/openapi/oauth/oauth.go +++ b/openapi/oauth/oauth.go @@ -1,12 +1,14 @@ package oauth import ( + "fmt" "time" "github.com/yaoapp/gou/store" "github.com/yaoapp/yao/openapi/oauth/providers/client" "github.com/yaoapp/yao/openapi/oauth/providers/user" "github.com/yaoapp/yao/openapi/oauth/types" + "github.com/yaoapp/yao/share" ) // Service OAuth service @@ -98,7 +100,12 @@ func NewService(config *Config) (*Service, error) { // Use UserProvider from config, or create a default one if not provided userProvider := config.UserProvider if userProvider == nil { - userProvider = user.NewDefaultUserProvider(nil, nil, nil) + userProvider = user.NewDefaultUser(&user.DefaultUserOptions{ + Prefix: fmt.Sprintf("%s:", share.App.Prefix), + Model: "__yao.user", + Cache: config.Cache, + TokenStore: config.Store, + }) } // Use ClientProvider from config, or create a default one if not provided @@ -106,7 +113,7 @@ func NewService(config *Config) (*Service, error) { if clientProvider == nil { var err error clientProvider, err = client.NewDefaultClient(&client.DefaultClientOptions{ - Prefix: "__yao:", + Prefix: fmt.Sprintf("%s:", share.App.Prefix), Store: config.Store, Cache: config.Cache, }) diff --git a/openapi/oauth/providers/user/default.go b/openapi/oauth/providers/user/default.go index 48005d7d..90c9c346 100644 --- a/openapi/oauth/providers/user/default.go +++ b/openapi/oauth/providers/user/default.go @@ -2,51 +2,863 @@ package user import ( "context" + "crypto/rand" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base32" + "encoding/binary" + "fmt" + "hash" + "math" + "net/url" + "reflect" + "strings" + "time" - "github.com/yaoapp/yao/openapi/oauth/types" + "github.com/yaoapp/gou/model" + "github.com/yaoapp/gou/store" ) -// DefaultUserProvider provides a default implementation of UserProvider -type DefaultUserProvider struct { - getUserByAccessTokenFunc func(ctx context.Context, accessToken string) (interface{}, error) - getUserBySubjectFunc func(ctx context.Context, subject string) (interface{}, error) - validateUserScopeFunc func(ctx context.Context, userID string, scopes []string) (bool, error) +// Safe user fields that can be displayed to users +var ( + // PublicUserFields contains fields that can be safely returned to users + PublicUserFields = []interface{}{ + "id", "subject", "username", "email", "first_name", "last_name", + "full_name", "avatar_url", "mobile", "address", "scopes", "status", + "email_verified", "mobile_verified", "two_factor_enabled", + "last_login_at", "metadata", "preferences", "created_at", "updated_at", + } + + // BasicUserFields contains minimal fields for basic user info + BasicUserFields = []interface{}{ + "id", "subject", "username", "email", "first_name", "last_name", + "full_name", "avatar_url", "status", "email_verified", "mobile_verified", + } + + // AuthUserFields contains fields needed for authentication + AuthUserFields = []interface{}{ + "id", "subject", "username", "email", "password_hash", "scopes", "status", + "email_verified", "mobile_verified", "two_factor_enabled", "last_login_at", + } + + // TwoFactorUserFields contains fields needed for two-factor authentication + TwoFactorUserFields = []interface{}{ + "id", "two_factor_enabled", "two_factor_secret", "two_factor_algorithm", + "two_factor_digits", "two_factor_period", "two_factor_recovery_codes", + } +) + +// DefaultUser provides a default implementation of UserProvider +type DefaultUser struct { + prefix string + model string + cache store.Store + tokenStore store.Store } -// NewDefaultUserProvider creates a new DefaultUserProvider with the given functions -func NewDefaultUserProvider( - getUserByAccessTokenFunc func(ctx context.Context, accessToken string) (interface{}, error), - getUserBySubjectFunc func(ctx context.Context, subject string) (interface{}, error), - validateUserScopeFunc func(ctx context.Context, userID string, scopes []string) (bool, error), -) *DefaultUserProvider { - return &DefaultUserProvider{ - getUserByAccessTokenFunc: getUserByAccessTokenFunc, - getUserBySubjectFunc: getUserBySubjectFunc, - validateUserScopeFunc: validateUserScopeFunc, +// DefaultUserOptions provides options for the DefaultUser +type DefaultUserOptions struct { + Prefix string + Model string // bind to a specific user model + Cache store.Store + TokenStore store.Store // store for OAuth tokens +} + +// NewDefaultUser creates a new DefaultUser +func NewDefaultUser(options *DefaultUserOptions) *DefaultUser { + // Set default model name if not specified + modelName := options.Model + if modelName == "" { + modelName = "__yao.user" } + + return &DefaultUser{ + prefix: options.Prefix, + model: modelName, + cache: options.Cache, + tokenStore: options.TokenStore, + } +} + +// Key generation methods + +func (u *DefaultUser) tokenKey(accessToken string) string { + return fmt.Sprintf("%s:token:%s", u.prefix, accessToken) +} + +func (u *DefaultUser) cacheKey(userID string) string { + return fmt.Sprintf("%s:user:%s", u.prefix, userID) +} + +func (u *DefaultUser) subjectCacheKey(subject string) string { + return fmt.Sprintf("%s:user:subject:%s", u.prefix, subject) +} + +func (u *DefaultUser) usernameCacheKey(username string) string { + return fmt.Sprintf("%s:user:username:%s", u.prefix, username) +} + +func (u *DefaultUser) emailCacheKey(email string) string { + return fmt.Sprintf("%s:user:email:%s", u.prefix, email) } // GetUserByAccessToken retrieves user information using an access token -func (p *DefaultUserProvider) GetUserByAccessToken(ctx context.Context, accessToken string) (interface{}, error) { - if p.getUserByAccessTokenFunc == nil { - return nil, &types.ErrorResponse{Code: "not_implemented", ErrorDescription: "GetUserByAccessToken is not implemented"} +func (u *DefaultUser) GetUserByAccessToken(ctx context.Context, accessToken string) (interface{}, error) { + // Get token information from tokenStore + tokenData, exists := u.tokenStore.Get(u.tokenKey(accessToken)) + if !exists { + return nil, fmt.Errorf("token not found") } - return p.getUserByAccessTokenFunc(ctx, accessToken) + + // Parse token data to get user subject + var tokenInfo map[string]interface{} + var ok bool + + // Try to convert to map[string]interface{} directly + if tokenInfo, ok = tokenData.(map[string]interface{}); !ok { + // If direct conversion fails, try to handle other possible types + switch v := tokenData.(type) { + case map[interface{}]interface{}: + // Convert map[interface{}]interface{} to map[string]interface{} + tokenInfo = make(map[string]interface{}) + for key, val := range v { + if keyStr, ok := key.(string); ok { + tokenInfo[keyStr] = val + } + } + default: + // Try to convert using map[string]interface{} casting + // This handles primitive.M and other MongoDB types + if reflect.TypeOf(v).Kind() == reflect.Map { + tokenInfo = make(map[string]interface{}) + rv := reflect.ValueOf(v) + for _, key := range rv.MapKeys() { + if keyStr, ok := key.Interface().(string); ok { + tokenInfo[keyStr] = rv.MapIndex(key).Interface() + } + } + if len(tokenInfo) == 0 { + return nil, fmt.Errorf("invalid token data format: %T", tokenData) + } + } else { + return nil, fmt.Errorf("invalid token data format: %T", tokenData) + } + } + } + + subject, ok := tokenInfo["subject"].(string) + if !ok { + return nil, fmt.Errorf("invalid subject in token") + } + + // Get user by subject + return u.GetUserBySubject(ctx, subject) } // GetUserBySubject retrieves user information using a subject identifier -func (p *DefaultUserProvider) GetUserBySubject(ctx context.Context, subject string) (interface{}, error) { - if p.getUserBySubjectFunc == nil { - return nil, &types.ErrorResponse{Code: "not_implemented", ErrorDescription: "GetUserBySubject is not implemented"} +func (u *DefaultUser) GetUserBySubject(ctx context.Context, subject string) (interface{}, error) { + // Try cache first if available + if u.cache != nil { + if cached, ok := u.cache.Get(u.subjectCacheKey(subject)); ok { + return cached, nil + } } - return p.getUserBySubjectFunc(ctx, subject) + + // Get user from database using the model + m := model.Select(u.model) + + user, err := m.Get(model.QueryParam{ + Select: PublicUserFields, + Wheres: []model.QueryWhere{ + {Column: "subject", Value: subject}, + }, + }) + + if err != nil { + return nil, fmt.Errorf("failed to get user by subject: %w", err) + } + + if len(user) == 0 { + return nil, fmt.Errorf("user not found") + } + + userData := user[0] + + // Cache the result if cache is available + if u.cache != nil { + u.cache.Set(u.subjectCacheKey(subject), userData, 5*time.Minute) + } + + return userData, nil } // ValidateUserScope validates if a user has access to requested scopes -func (p *DefaultUserProvider) ValidateUserScope(ctx context.Context, userID string, scopes []string) (bool, error) { - if p.validateUserScopeFunc == nil { - // Default implementation: allow all scopes - return true, nil +func (u *DefaultUser) ValidateUserScope(ctx context.Context, userID string, scopes []string) (bool, error) { + var user interface{} + var err error + + // Try cache first if available + if u.cache != nil { + if cached, ok := u.cache.Get(u.cacheKey(userID)); ok { + user = cached + } } - return p.validateUserScopeFunc(ctx, userID, scopes) + + // If not in cache, get from database + if user == nil { + m := model.Select(u.model) + user, err = m.Find(userID, model.QueryParam{ + Select: []interface{}{"scopes", "status"}, + }) + + if err != nil { + return false, fmt.Errorf("failed to get user: %w", err) + } + + // Cache the result if cache is available + if u.cache != nil { + u.cache.Set(u.cacheKey(userID), user, 5*time.Minute) + } + } + + // Check if user data is valid + if user == nil { + return false, fmt.Errorf("user not found") + } + + // Convert user to map for indexing + var userMap map[string]interface{} + switch v := user.(type) { + case map[string]interface{}: + userMap = v + default: + // Try to convert using reflection if it's a map-like type + if reflect.TypeOf(v).Kind() == reflect.Map { + userMap = make(map[string]interface{}) + rv := reflect.ValueOf(v) + for _, key := range rv.MapKeys() { + if keyStr, ok := key.Interface().(string); ok { + userMap[keyStr] = rv.MapIndex(key).Interface() + } + } + } else { + return false, fmt.Errorf("invalid user data format") + } + } + + // Check if user is active + if status, ok := userMap["status"].(string); ok && status != "active" { + return false, fmt.Errorf("user is not active") + } + + // Get user scopes + userScopes, ok := userMap["scopes"].([]interface{}) + if !ok { + // If no scopes defined, deny access + return false, nil + } + + // Convert user scopes to string slice + userScopeStrings := make([]string, len(userScopes)) + for i, scope := range userScopes { + if scopeStr, ok := scope.(string); ok { + userScopeStrings[i] = scopeStr + } + } + + // Check if user has all requested scopes + for _, requestedScope := range scopes { + hasScope := false + for _, userScope := range userScopeStrings { + if userScope == requestedScope { + hasScope = true + break + } + } + if !hasScope { + return false, nil + } + } + + return true, nil +} + +// StoreToken stores a token in the token store with expiration time +func (u *DefaultUser) StoreToken(accessToken string, tokenData map[string]interface{}, expiration time.Duration) error { + return u.tokenStore.Set(u.tokenKey(accessToken), tokenData, expiration) +} + +// RevokeToken revokes a token by removing it from the token store +func (u *DefaultUser) RevokeToken(accessToken string) error { + u.tokenStore.Del(u.tokenKey(accessToken)) + return nil +} + +// TokenExists checks if a token exists in the token store +func (u *DefaultUser) TokenExists(accessToken string) bool { + _, exists := u.tokenStore.Get(u.tokenKey(accessToken)) + return exists +} + +// GetTokenData retrieves token data from the token store +func (u *DefaultUser) GetTokenData(accessToken string) (map[string]interface{}, error) { + tokenData, exists := u.tokenStore.Get(u.tokenKey(accessToken)) + if !exists { + return nil, fmt.Errorf("token not found") + } + + // Try to convert to map[string]interface{} directly + if tokenInfo, ok := tokenData.(map[string]interface{}); ok { + return tokenInfo, nil + } + + // If direct conversion fails, try to handle other possible types + // This handles cases where MongoDB might return different types + switch v := tokenData.(type) { + case map[string]interface{}: + return v, nil + case map[interface{}]interface{}: + // Convert map[interface{}]interface{} to map[string]interface{} + result := make(map[string]interface{}) + for key, val := range v { + if keyStr, ok := key.(string); ok { + result[keyStr] = val + } + } + return result, nil + default: + // Try to convert using map[string]interface{} casting + // This handles primitive.M and other MongoDB types + if reflect.TypeOf(v).Kind() == reflect.Map { + result := make(map[string]interface{}) + rv := reflect.ValueOf(v) + for _, key := range rv.MapKeys() { + if keyStr, ok := key.Interface().(string); ok { + result[keyStr] = rv.MapIndex(key).Interface() + } + } + if len(result) > 0 { + return result, nil + } + } + return nil, fmt.Errorf("invalid token data format: %T", tokenData) + } +} + +// CreateUser creates a new user in the database +func (u *DefaultUser) CreateUser(userData map[string]interface{}) (interface{}, error) { + m := model.Select(u.model) + userID, err := m.Create(userData) + if err != nil { + return nil, err + } + + // Note: No need to cache newly created user data since it will be cached + // when accessed for the first time through other methods + + return userID, nil +} + +// UpdateUserLastLogin updates the user's last login timestamp +func (u *DefaultUser) UpdateUserLastLogin(userID interface{}) error { + m := model.Select(u.model) + err := m.Update(userID, map[string]interface{}{ + "last_login_at": time.Now(), + }) + + if err != nil { + return err + } + + // Clear cache for this user since data has changed + if u.cache != nil { + userIDStr := fmt.Sprintf("%v", userID) + u.cache.Del(u.cacheKey(userIDStr)) + } + + return nil +} + +// GetUserByUsername retrieves user by username +func (u *DefaultUser) GetUserByUsername(username string) (interface{}, error) { + // Try cache first if available + if u.cache != nil { + if cached, ok := u.cache.Get(u.usernameCacheKey(username)); ok { + return cached, nil + } + } + + m := model.Select(u.model) + + users, err := m.Get(model.QueryParam{ + Select: PublicUserFields, + Wheres: []model.QueryWhere{ + {Column: "username", Value: username}, + }, + }) + + if err != nil { + return nil, fmt.Errorf("failed to get user by username: %w", err) + } + + if len(users) == 0 { + return nil, fmt.Errorf("user not found") + } + + userData := users[0] + + // Cache the result if cache is available + if u.cache != nil { + u.cache.Set(u.usernameCacheKey(username), userData, 5*time.Minute) + } + + return userData, nil +} + +// GetUserByEmail retrieves user by email +func (u *DefaultUser) GetUserByEmail(email string) (interface{}, error) { + // Try cache first if available + if u.cache != nil { + if cached, ok := u.cache.Get(u.emailCacheKey(email)); ok { + return cached, nil + } + } + + m := model.Select(u.model) + + users, err := m.Get(model.QueryParam{ + Select: PublicUserFields, + Wheres: []model.QueryWhere{ + {Column: "email", Value: email}, + }, + }) + + if err != nil { + return nil, fmt.Errorf("failed to get user by email: %w", err) + } + + if len(users) == 0 { + return nil, fmt.Errorf("user not found") + } + + userData := users[0] + + // Cache the result if cache is available + if u.cache != nil { + u.cache.Set(u.emailCacheKey(email), userData, 5*time.Minute) + } + + return userData, nil +} + +// GenerateTOTPSecret generates a new TOTP secret for user +func (u *DefaultUser) GenerateTOTPSecret(ctx context.Context, userID string, issuer string, accountName string) (string, string, error) { + // Generate a random 20-byte secret + secret := make([]byte, 20) + if _, err := rand.Read(secret); err != nil { + return "", "", fmt.Errorf("failed to generate secret: %w", err) + } + + // Encode secret as Base32 + secretBase32 := base32.StdEncoding.EncodeToString(secret) + secretBase32 = strings.TrimRight(secretBase32, "=") // Remove padding + + // Set default values + if issuer == "" { + issuer = "YAO OAuth" + } + if accountName == "" { + accountName = userID + } + + // Generate QR code URL + qrURL := u.generateQRCodeURL(secretBase32, issuer, accountName) + + return secretBase32, qrURL, nil +} + +// EnableTwoFactor enables two-factor authentication for user +func (u *DefaultUser) EnableTwoFactor(ctx context.Context, userID string, secret string, code string) error { + // Verify the provided code with the secret + if !u.verifyTOTPWithSecret(secret, code, "SHA1", 6, 30) { + return fmt.Errorf("invalid verification code") + } + + // Generate recovery codes + recoveryCodes, err := u.generateRecoveryCodesList() + if err != nil { + return fmt.Errorf("failed to generate recovery codes: %w", err) + } + + // Update user record + m := model.Select(u.model) + now := time.Now() + err = m.Update(userID, map[string]interface{}{ + "two_factor_enabled": true, + "two_factor_secret": secret, + "two_factor_recovery_codes": recoveryCodes, + "two_factor_enabled_at": now, + "two_factor_last_verified_at": now, + }) + + if err != nil { + return fmt.Errorf("failed to enable two-factor authentication: %w", err) + } + + // Clear user cache + if u.cache != nil { + u.cache.Del(u.cacheKey(userID)) + } + + return nil +} + +// DisableTwoFactor disables two-factor authentication for user +func (u *DefaultUser) DisableTwoFactor(ctx context.Context, userID string, code string) error { + // Get current user data + m := model.Select(u.model) + user, err := m.Find(userID, model.QueryParam{ + Select: []interface{}{"two_factor_secret", "two_factor_recovery_codes"}, + }) + if err != nil { + return fmt.Errorf("failed to get user: %w", err) + } + + if user == nil { + return fmt.Errorf("user not found") + } + + // Verify code (either TOTP or recovery code) + verified := false + if secret, ok := user["two_factor_secret"].(string); ok && secret != "" { + verified = u.verifyTOTPWithSecret(secret, code, "SHA1", 6, 30) + } + + if !verified { + // Try recovery code + if recoveryCodes, ok := user["two_factor_recovery_codes"].([]interface{}); ok { + for _, rc := range recoveryCodes { + if rcStr, ok := rc.(string); ok && rcStr == code { + verified = true + break + } + } + } + } + + if !verified { + return fmt.Errorf("invalid verification code") + } + + // Disable two-factor authentication + err = m.Update(userID, map[string]interface{}{ + "two_factor_enabled": false, + "two_factor_secret": nil, + "two_factor_recovery_codes": nil, + "two_factor_enabled_at": nil, + "two_factor_last_verified_at": nil, + }) + + if err != nil { + return fmt.Errorf("failed to disable two-factor authentication: %w", err) + } + + // Clear user cache + if u.cache != nil { + u.cache.Del(u.cacheKey(userID)) + } + + return nil +} + +// VerifyTOTPCode verifies a TOTP code for user +func (u *DefaultUser) VerifyTOTPCode(ctx context.Context, userID string, code string) (bool, error) { + // Get user data + m := model.Select(u.model) + user, err := m.Find(userID, model.QueryParam{ + Select: []interface{}{"two_factor_enabled", "two_factor_secret", "two_factor_algorithm", "two_factor_digits", "two_factor_period"}, + }) + if err != nil { + return false, fmt.Errorf("failed to get user: %w", err) + } + + if user == nil { + return false, fmt.Errorf("user not found") + } + + // Check if two-factor is enabled + if enabled, ok := user["two_factor_enabled"].(bool); !ok || !enabled { + return false, fmt.Errorf("two-factor authentication is not enabled") + } + + // Get TOTP parameters + secret, _ := user["two_factor_secret"].(string) + algorithm, _ := user["two_factor_algorithm"].(string) + digits, _ := user["two_factor_digits"].(int) + period, _ := user["two_factor_period"].(int) + + // Set defaults + if algorithm == "" { + algorithm = "SHA1" + } + if digits == 0 { + digits = 6 + } + if period == 0 { + period = 30 + } + + // Verify code + verified := u.verifyTOTPWithSecret(secret, code, algorithm, digits, period) + + if verified { + // Update last verified time + m.Update(userID, map[string]interface{}{ + "two_factor_last_verified_at": time.Now(), + }) + + // Clear user cache + if u.cache != nil { + u.cache.Del(u.cacheKey(userID)) + } + } + + return verified, nil +} + +// GenerateRecoveryCodes generates new recovery codes for user +func (u *DefaultUser) GenerateRecoveryCodes(ctx context.Context, userID string) ([]string, error) { + // Generate new recovery codes + recoveryCodes, err := u.generateRecoveryCodesList() + if err != nil { + return nil, fmt.Errorf("failed to generate recovery codes: %w", err) + } + + // Update user record + m := model.Select(u.model) + err = m.Update(userID, map[string]interface{}{ + "two_factor_recovery_codes": recoveryCodes, + }) + + if err != nil { + return nil, fmt.Errorf("failed to update recovery codes: %w", err) + } + + // Clear user cache + if u.cache != nil { + u.cache.Del(u.cacheKey(userID)) + } + + // Convert to string slice for return + result := make([]string, len(recoveryCodes)) + for i, code := range recoveryCodes { + result[i] = code.(string) + } + + return result, nil +} + +// VerifyRecoveryCode verifies and consumes a recovery code +func (u *DefaultUser) VerifyRecoveryCode(ctx context.Context, userID string, code string) (bool, error) { + // Get user data + m := model.Select(u.model) + user, err := m.Find(userID, model.QueryParam{ + Select: []interface{}{"two_factor_enabled", "two_factor_recovery_codes"}, + }) + if err != nil { + return false, fmt.Errorf("failed to get user: %w", err) + } + + if user == nil { + return false, fmt.Errorf("user not found") + } + + // Check if two-factor is enabled + if enabled, ok := user["two_factor_enabled"].(bool); !ok || !enabled { + return false, fmt.Errorf("two-factor authentication is not enabled") + } + + // Get recovery codes + recoveryCodes, ok := user["two_factor_recovery_codes"].([]interface{}) + if !ok { + return false, fmt.Errorf("no recovery codes found") + } + + // Find and remove the used code + var newRecoveryCodes []interface{} + found := false + for _, rc := range recoveryCodes { + if rcStr, ok := rc.(string); ok && rcStr == code { + found = true + // Don't add this code to the new list (consume it) + } else { + newRecoveryCodes = append(newRecoveryCodes, rc) + } + } + + if !found { + return false, nil + } + + // Update user record with remaining codes + err = m.Update(userID, map[string]interface{}{ + "two_factor_recovery_codes": newRecoveryCodes, + "two_factor_last_verified_at": time.Now(), + }) + + if err != nil { + return false, fmt.Errorf("failed to update recovery codes: %w", err) + } + + // Clear user cache + if u.cache != nil { + u.cache.Del(u.cacheKey(userID)) + } + + return true, nil +} + +// Helper methods for TOTP + +// generateQRCodeURL generates a QR code URL for TOTP setup +func (u *DefaultUser) generateQRCodeURL(secret, issuer, accountName string) string { + // Build the otpauth URL + params := url.Values{} + params.Set("secret", secret) + params.Set("issuer", issuer) + params.Set("algorithm", "SHA1") + params.Set("digits", "6") + params.Set("period", "30") + + label := fmt.Sprintf("%s:%s", issuer, accountName) + qrURL := fmt.Sprintf("otpauth://totp/%s?%s", url.QueryEscape(label), params.Encode()) + + return qrURL +} + +// generateRecoveryCodesList generates a list of recovery codes +func (u *DefaultUser) generateRecoveryCodesList() ([]interface{}, error) { + codes := make([]interface{}, 10) // Generate 10 recovery codes + + for i := 0; i < 10; i++ { + // Generate 8-character recovery code + code := make([]byte, 8) + if _, err := rand.Read(code); err != nil { + return nil, err + } + + // Convert to hex string + codeStr := fmt.Sprintf("%x", code) + codes[i] = codeStr + } + + return codes, nil +} + +// verifyTOTPWithSecret verifies a TOTP code with given parameters +func (u *DefaultUser) verifyTOTPWithSecret(secret, code, algorithm string, digits, period int) bool { + // Decode secret + secretBytes, err := base32.StdEncoding.DecodeString(secret) + if err != nil { + return false + } + + // Get current time + now := time.Now().Unix() + + // Check current time window and previous/next windows for clock skew + for i := -1; i <= 1; i++ { + timeCounter := (now + int64(i*period)) / int64(period) + expectedCode := u.generateTOTPCode(secretBytes, timeCounter, algorithm, digits) + + if expectedCode == code { + return true + } + } + + return false +} + +// generateTOTPCode generates a TOTP code +func (u *DefaultUser) generateTOTPCode(secret []byte, timeCounter int64, algorithm string, digits int) string { + // Convert time counter to byte array + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, uint64(timeCounter)) + + // Choose hash algorithm + var h hash.Hash + switch algorithm { + case "SHA256": + h = sha256.New() + case "SHA512": + h = sha512.New() + default: + h = sha1.New() + } + + // HMAC + for i := 0; i < len(secret); i++ { + h.Write([]byte{secret[i] ^ 0x36}) + } + for i := len(secret); i < h.BlockSize(); i++ { + h.Write([]byte{0x36}) + } + h.Write(buf) + innerHash := h.Sum(nil) + + h.Reset() + for i := 0; i < len(secret); i++ { + h.Write([]byte{secret[i] ^ 0x5c}) + } + for i := len(secret); i < h.BlockSize(); i++ { + h.Write([]byte{0x5c}) + } + h.Write(innerHash) + hmacHash := h.Sum(nil) + + // Dynamic truncation + offset := hmacHash[len(hmacHash)-1] & 0x0f + binCode := binary.BigEndian.Uint32(hmacHash[offset:offset+4]) & 0x7fffffff + + // Generate digits + code := binCode % uint32(math.Pow10(digits)) + + return fmt.Sprintf("%0*d", digits, code) +} + +// GetUserForAuth retrieves user information for authentication purposes (internal use only) +// This method includes sensitive fields like password_hash and should not be exposed to external APIs +func (u *DefaultUser) GetUserForAuth(ctx context.Context, identifier string, identifierType string) (interface{}, error) { + // Get user from database using the model + m := model.Select(u.model) + + var column string + switch identifierType { + case "username": + column = "username" + case "email": + column = "email" + case "subject": + column = "subject" + default: + return nil, fmt.Errorf("invalid identifier type: %s", identifierType) + } + + user, err := m.Get(model.QueryParam{ + Select: AuthUserFields, + Wheres: []model.QueryWhere{ + {Column: column, Value: identifier}, + }, + }) + + if err != nil { + return nil, fmt.Errorf("failed to get user for auth: %w", err) + } + + if len(user) == 0 { + return nil, fmt.Errorf("user not found") + } + + return user[0], nil } diff --git a/openapi/oauth/providers/user/default_test.go b/openapi/oauth/providers/user/default_test.go new file mode 100644 index 00000000..a9f1c62e --- /dev/null +++ b/openapi/oauth/providers/user/default_test.go @@ -0,0 +1,1131 @@ +package user + +import ( + "context" + "fmt" + "os" + "path/filepath" + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yaoapp/gou/connector" + "github.com/yaoapp/gou/model" + "github.com/yaoapp/gou/store" + "github.com/yaoapp/gou/store/badger" + "github.com/yaoapp/gou/store/lru" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/test" +) + +// Store configuration for parameterized tests +type StoreConfig struct { + Name string + GetFunc func(*testing.T) store.Store +} + +// Test user data +type TestUserData struct { + ID int64 `json:"id"` + Subject string `json:"subject"` + Username string `json:"username"` + Email string `json:"email"` + PasswordHash string `json:"password_hash"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + FullName string `json:"full_name"` + AvatarURL string `json:"avatar_url"` + Mobile string `json:"mobile"` + Address string `json:"address"` + Scopes []string `json:"scopes"` + Status string `json:"status"` + EmailVerified bool `json:"email_verified"` + MobileVerified bool `json:"mobile_verified"` + TwoFactorEnabled bool `json:"two_factor_enabled"` + TwoFactorSecret string `json:"two_factor_secret"` + Metadata map[string]interface{} `json:"metadata"` + Preferences map[string]interface{} `json:"preferences"` +} + +var testUserData = &TestUserData{ + Subject: "test-subject-123", + Username: "testuser123", + Email: "test@example.com", + PasswordHash: "hashed_password_123", + FirstName: "Test", + LastName: "User", + FullName: "Test User", + AvatarURL: "https://example.com/avatar.jpg", + Mobile: "+1234567890", + Address: "123 Test Street", + Scopes: []string{"openid", "profile", "email"}, + Status: "active", + EmailVerified: true, + MobileVerified: false, + TwoFactorEnabled: false, + // TwoFactorSecret: "", + Metadata: map[string]interface{}{"test": "data"}, + Preferences: map[string]interface{}{"theme": "dark"}, +} + +// Helper function to convert various map types to map[string]interface{} +func convertToStringMap(t *testing.T, data interface{}) map[string]interface{} { + switch v := data.(type) { + case map[string]interface{}: + return v + default: + // Try to convert using reflection if it's a map-like type + if reflect.TypeOf(v).Kind() == reflect.Map { + result := make(map[string]interface{}) + rv := reflect.ValueOf(v) + for _, key := range rv.MapKeys() { + if keyStr, ok := key.Interface().(string); ok { + result[keyStr] = rv.MapIndex(key).Interface() + } + } + return result + } + t.Fatalf("Unexpected data type: %T", v) + return nil + } +} + +func TestMain(m *testing.M) { + // Setup + test.Prepare(&testing.T{}, config.Conf) + defer test.Clean() + + // Run tests + code := m.Run() + os.Exit(code) +} + +// Test helpers +func getMongoStore(t *testing.T) store.Store { + // Skip test if MongoDB is not available + host := os.Getenv("MONGO_TEST_HOST") + if host == "" { + t.Skip("MongoDB not available - set MONGO_TEST_HOST environment variable") + } + + // Create MongoDB store using connector + mongoConnector, err := connector.New("mongo", "oauth_user_test", []byte(`{ + "name": "OAuth User Test MongoDB", + "type": "mongo", + "options": { + "db": "oauth_user_test", + "hosts": [{ + "host": "`+host+`", + "port": "`+os.Getenv("MONGO_TEST_PORT")+`", + "user": "`+os.Getenv("MONGO_TEST_USER")+`", + "pass": "`+os.Getenv("MONGO_TEST_PASS")+`" + }] + } + }`)) + require.NoError(t, err) + + mongoStore, err := store.New(mongoConnector, nil) + require.NoError(t, err) + + return mongoStore +} + +func getBadgerStore(t *testing.T) store.Store { + // Create temporary directory for test database + tempDir := t.TempDir() + dbPath := filepath.Join(tempDir, "test_oauth_user_badger") + + badgerStore, err := badger.New(dbPath) + require.NoError(t, err) + + // Clean up on test completion + t.Cleanup(func() { + badgerStore.Close() + }) + + return badgerStore +} + +func getLRUCache(t *testing.T) store.Store { + cache, err := lru.New(1000) + require.NoError(t, err) + return cache +} + +// Get all available store configurations +func getStoreConfigs() []StoreConfig { + return []StoreConfig{ + {Name: "MongoDB", GetFunc: getMongoStore}, + {Name: "Badger", GetFunc: getBadgerStore}, + } +} + +// Create test user data with unique identifier +func createTestUser(id string) *TestUserData { + timestamp := time.Now().UnixNano() + uniqueID := fmt.Sprintf("%s-%d", id, timestamp) + + return &TestUserData{ + Subject: "test-subject-" + uniqueID, + Username: "testuser" + uniqueID, + Email: "test" + uniqueID + "@example.com", + PasswordHash: "hashed-password-" + uniqueID, + FirstName: "Test", + LastName: "User " + uniqueID, + FullName: "Test User " + uniqueID, + AvatarURL: "https://example.com/avatar" + uniqueID + ".jpg", + Mobile: "1234567890", + Address: "Test Address " + uniqueID, + Scopes: []string{"openid", "profile", "email"}, + Status: "active", + EmailVerified: true, + MobileVerified: true, + TwoFactorEnabled: false, + Metadata: map[string]interface{}{"test": "data"}, + Preferences: map[string]interface{}{"theme": "dark"}, + } +} + +// Create test token data +func createTestToken(subject string) map[string]interface{} { + return map[string]interface{}{ + "subject": subject, + "client_id": "test-client", + "scopes": []string{"openid", "profile", "email"}, + "expires_at": time.Now().Add(1 * time.Hour).Unix(), + "issued_at": time.Now().Unix(), + } +} + +// Setup test user in database +func setupTestUser(t *testing.T, userData *TestUserData) { + m := model.Select("__yao.user") + + // Create user + userMap := map[string]interface{}{ + "subject": userData.Subject, + "username": userData.Username, + "email": userData.Email, + "password_hash": userData.PasswordHash, + "first_name": userData.FirstName, + "last_name": userData.LastName, + "full_name": userData.FullName, + "avatar_url": userData.AvatarURL, + "mobile": userData.Mobile, + "address": userData.Address, + "scopes": userData.Scopes, + "status": userData.Status, + "email_verified": userData.EmailVerified, + "mobile_verified": userData.MobileVerified, + "two_factor_enabled": userData.TwoFactorEnabled, + "two_factor_secret": userData.TwoFactorSecret, + "metadata": userData.Metadata, + "preferences": userData.Preferences, + } + + id, err := m.Create(userMap) + require.NoError(t, err) + userData.ID = int64(id) +} + +// Clean up test data +func cleanupTestData(t *testing.T) { + m := model.Select("__yao.user") + + // Delete all test users (be more aggressive in cleanup) + _, err := m.DeleteWhere(model.QueryParam{ + Wheres: []model.QueryWhere{ + {Column: "subject", OP: "like", Value: "test-subject-%"}, + }, + }) + if err != nil { + t.Logf("Warning: Failed to clean up test users by subject: %v", err) + } + + // Also clean up by username pattern + _, err = m.DeleteWhere(model.QueryParam{ + Wheres: []model.QueryWhere{ + {Column: "username", OP: "like", Value: "testuser%"}, + }, + }) + if err != nil { + t.Logf("Warning: Failed to clean up test users by username: %v", err) + } +} + +func TestNewDefaultUser(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + t.Run("valid options", func(t *testing.T) { + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + assert.NotNil(t, user) + assert.Equal(t, "test:", user.prefix) + assert.Equal(t, "__yao.user", user.model) + assert.Equal(t, cache, user.cache) + assert.Equal(t, tokenStore, user.tokenStore) + }) + + t.Run("without cache", func(t *testing.T) { + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + TokenStore: tokenStore, + }) + + assert.NotNil(t, user) + assert.Nil(t, user.cache) + }) + + t.Run("without token store", func(t *testing.T) { + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + Cache: cache, + }) + + assert.NotNil(t, user) + assert.Nil(t, user.tokenStore) + }) + }) + } +} + +func TestKeyGeneration(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + t.Run("token key", func(t *testing.T) { + key := user.tokenKey("test-token") + expected := "test::token:test-token" + assert.Equal(t, expected, key) + }) + + t.Run("cache key", func(t *testing.T) { + key := user.cacheKey("123") + expected := "test::user:123" + assert.Equal(t, expected, key) + }) + + t.Run("subject cache key", func(t *testing.T) { + key := user.subjectCacheKey("test-subject") + expected := "test::user:subject:test-subject" + assert.Equal(t, expected, key) + }) + + t.Run("username cache key", func(t *testing.T) { + key := user.usernameCacheKey("testuser") + expected := "test::user:username:testuser" + assert.Equal(t, expected, key) + }) + + t.Run("email cache key", func(t *testing.T) { + key := user.emailCacheKey("test@example.com") + expected := "test::user:email:test@example.com" + assert.Equal(t, expected, key) + }) + }) + } +} + +func TestTokenOperations(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + // Clean up + tokenStore.Clear() + + t.Run("store and get token", func(t *testing.T) { + tokenData := createTestToken("test-subject") + err := user.StoreToken("test-token", tokenData, 1*time.Hour) + assert.NoError(t, err) + + exists := user.TokenExists("test-token") + assert.True(t, exists) + + retrievedData, err := user.GetTokenData("test-token") + assert.NoError(t, err) + assert.Equal(t, tokenData["subject"], retrievedData["subject"]) + }) + + t.Run("revoke token", func(t *testing.T) { + tokenData := createTestToken("test-subject") + err := user.StoreToken("test-token-revoke", tokenData, 1*time.Hour) + assert.NoError(t, err) + + exists := user.TokenExists("test-token-revoke") + assert.True(t, exists) + + err = user.RevokeToken("test-token-revoke") + assert.NoError(t, err) + + exists = user.TokenExists("test-token-revoke") + assert.False(t, exists) + }) + + t.Run("non-existent token", func(t *testing.T) { + exists := user.TokenExists("non-existent") + assert.False(t, exists) + + _, err := user.GetTokenData("non-existent") + assert.Error(t, err) + assert.Contains(t, err.Error(), "token not found") + }) + }) + } +} + +func TestGetUserBySubject(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + cleanupTestData(t) + defer cleanupTestData(t) + + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + // Create test user + testUser := createTestUser("subject1") + setupTestUser(t, testUser) + + ctx := context.Background() + + t.Run("get user by subject", func(t *testing.T) { + retrievedUser, err := user.GetUserBySubject(ctx, testUser.Subject) + assert.NoError(t, err) + assert.NotNil(t, retrievedUser) + + userMap := convertToStringMap(t, retrievedUser) + + assert.Equal(t, testUser.Subject, userMap["subject"]) + assert.Equal(t, testUser.Username, userMap["username"]) + assert.Equal(t, testUser.Email, userMap["email"]) + }) + + t.Run("get user by subject with cache", func(t *testing.T) { + // Clear cache first + cache.Clear() + + // First call should hit database + retrievedUser, err := user.GetUserBySubject(ctx, testUser.Subject) + assert.NoError(t, err) + assert.NotNil(t, retrievedUser) + + // Second call should hit cache + retrievedUser2, err := user.GetUserBySubject(ctx, testUser.Subject) + assert.NoError(t, err) + assert.NotNil(t, retrievedUser2) + + userMap := convertToStringMap(t, retrievedUser2) + + assert.Equal(t, testUser.Subject, userMap["subject"]) + }) + + t.Run("non-existent subject", func(t *testing.T) { + retrievedUser, err := user.GetUserBySubject(ctx, "non-existent-subject") + assert.Error(t, err) + assert.Nil(t, retrievedUser) + assert.Contains(t, err.Error(), "user not found") + }) + }) + } +} + +func TestGetUserByUsername(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + cleanupTestData(t) + defer cleanupTestData(t) + + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + // Create test user + testUser := createTestUser("username1") + setupTestUser(t, testUser) + + t.Run("get user by username", func(t *testing.T) { + retrievedUser, err := user.GetUserByUsername(testUser.Username) + assert.NoError(t, err) + assert.NotNil(t, retrievedUser) + + userMap := convertToStringMap(t, retrievedUser) + assert.Equal(t, testUser.Username, userMap["username"]) + assert.Equal(t, testUser.Email, userMap["email"]) + }) + + t.Run("non-existent username", func(t *testing.T) { + retrievedUser, err := user.GetUserByUsername("non-existent-user") + assert.Error(t, err) + assert.Nil(t, retrievedUser) + assert.Contains(t, err.Error(), "user not found") + }) + }) + } +} + +func TestGetUserByEmail(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + cleanupTestData(t) + defer cleanupTestData(t) + + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + // Create test user + testUser := createTestUser("email1") + setupTestUser(t, testUser) + + t.Run("get user by email", func(t *testing.T) { + retrievedUser, err := user.GetUserByEmail(testUser.Email) + assert.NoError(t, err) + assert.NotNil(t, retrievedUser) + + userMap := convertToStringMap(t, retrievedUser) + assert.Equal(t, testUser.Email, userMap["email"]) + assert.Equal(t, testUser.Username, userMap["username"]) + }) + + t.Run("non-existent email", func(t *testing.T) { + retrievedUser, err := user.GetUserByEmail("non-existent@example.com") + assert.Error(t, err) + assert.Nil(t, retrievedUser) + assert.Contains(t, err.Error(), "user not found") + }) + }) + } +} + +func TestGetUserByAccessToken(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + cleanupTestData(t) + defer cleanupTestData(t) + + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + // Clean up + tokenStore.Clear() + + // Create test user + testUser := createTestUser("token1") + setupTestUser(t, testUser) + + ctx := context.Background() + + t.Run("get user by access token", func(t *testing.T) { + // Store token + tokenData := createTestToken(testUser.Subject) + err := user.StoreToken("test-access-token", tokenData, 1*time.Hour) + require.NoError(t, err) + + // Get user by token + retrievedUser, err := user.GetUserByAccessToken(ctx, "test-access-token") + assert.NoError(t, err) + assert.NotNil(t, retrievedUser) + + userMap := convertToStringMap(t, retrievedUser) + assert.Equal(t, testUser.Subject, userMap["subject"]) + assert.Equal(t, testUser.Username, userMap["username"]) + }) + + t.Run("non-existent token", func(t *testing.T) { + retrievedUser, err := user.GetUserByAccessToken(ctx, "non-existent-token") + assert.Error(t, err) + assert.Nil(t, retrievedUser) + assert.Contains(t, err.Error(), "token not found") + }) + + t.Run("invalid token format", func(t *testing.T) { + // Store invalid token data + invalidTokenData := "invalid-token-data" + tokenStore.Set(user.tokenKey("invalid-token"), invalidTokenData, 1*time.Hour) + + retrievedUser, err := user.GetUserByAccessToken(ctx, "invalid-token") + assert.Error(t, err) + assert.Nil(t, retrievedUser) + assert.Contains(t, err.Error(), "invalid token data format") + }) + }) + } +} + +func TestValidateUserScope(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + cleanupTestData(t) + defer cleanupTestData(t) + + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + // Create test user + testUser := createTestUser("scope1") + setupTestUser(t, testUser) + + ctx := context.Background() + + t.Run("validate user scope - valid", func(t *testing.T) { + valid, err := user.ValidateUserScope(ctx, fmt.Sprintf("%d", testUser.ID), []string{"openid", "profile"}) + assert.NoError(t, err) + assert.True(t, valid) + }) + + t.Run("validate user scope - invalid", func(t *testing.T) { + valid, err := user.ValidateUserScope(ctx, fmt.Sprintf("%d", testUser.ID), []string{"admin"}) + assert.NoError(t, err) + assert.False(t, valid) + }) + + t.Run("validate user scope - inactive user", func(t *testing.T) { + // Create inactive user + inactiveUser := createTestUser("inactive") + inactiveUser.Status = "inactive" + setupTestUser(t, inactiveUser) + + valid, err := user.ValidateUserScope(ctx, fmt.Sprintf("%d", inactiveUser.ID), []string{"openid"}) + assert.Error(t, err) + assert.False(t, valid) + assert.Contains(t, err.Error(), "user is not active") + }) + + t.Run("validate user scope - non-existent user", func(t *testing.T) { + valid, err := user.ValidateUserScope(ctx, "999999", []string{"openid"}) + assert.Error(t, err) + assert.False(t, valid) + assert.Contains(t, err.Error(), "数据不存在") + }) + }) + } +} + +func TestGetUserForAuth(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + cleanupTestData(t) + defer cleanupTestData(t) + + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + // Create test user + testUser := createTestUser("auth1") + setupTestUser(t, testUser) + + ctx := context.Background() + + t.Run("get user for auth by username", func(t *testing.T) { + retrievedUser, err := user.GetUserForAuth(ctx, testUser.Username, "username") + assert.NoError(t, err) + assert.NotNil(t, retrievedUser) + + userMap := convertToStringMap(t, retrievedUser) + assert.Equal(t, testUser.Username, userMap["username"]) + // Password should be encrypted, not equal to original + assert.NotEmpty(t, userMap["password_hash"]) + assert.NotEqual(t, testUser.PasswordHash, userMap["password_hash"]) + }) + + t.Run("get user for auth by email", func(t *testing.T) { + retrievedUser, err := user.GetUserForAuth(ctx, testUser.Email, "email") + assert.NoError(t, err) + assert.NotNil(t, retrievedUser) + + userMap := convertToStringMap(t, retrievedUser) + assert.Equal(t, testUser.Email, userMap["email"]) + // Password should be encrypted, not equal to original + assert.NotEmpty(t, userMap["password_hash"]) + assert.NotEqual(t, testUser.PasswordHash, userMap["password_hash"]) + }) + + t.Run("get user for auth by subject", func(t *testing.T) { + retrievedUser, err := user.GetUserForAuth(ctx, testUser.Subject, "subject") + assert.NoError(t, err) + assert.NotNil(t, retrievedUser) + + userMap := convertToStringMap(t, retrievedUser) + assert.Equal(t, testUser.Subject, userMap["subject"]) + // Password should be encrypted, not equal to original + assert.NotEmpty(t, userMap["password_hash"]) + assert.NotEqual(t, testUser.PasswordHash, userMap["password_hash"]) + }) + + t.Run("get user for auth - invalid identifier type", func(t *testing.T) { + retrievedUser, err := user.GetUserForAuth(ctx, testUser.Username, "invalid") + assert.Error(t, err) + assert.Nil(t, retrievedUser) + assert.Contains(t, err.Error(), "invalid identifier type") + }) + + t.Run("get user for auth - non-existent user", func(t *testing.T) { + retrievedUser, err := user.GetUserForAuth(ctx, "non-existent", "username") + assert.Error(t, err) + assert.Nil(t, retrievedUser) + assert.Contains(t, err.Error(), "user not found") + }) + }) + } +} + +func TestCreateUser(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + cleanupTestData(t) + defer cleanupTestData(t) + + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + t.Run("create user", func(t *testing.T) { + testUser := createTestUser("create") + + userData := map[string]interface{}{ + "subject": testUser.Subject, + "username": testUser.Username, + "email": testUser.Email, + "password_hash": testUser.PasswordHash, + "first_name": testUser.FirstName, + "last_name": testUser.LastName, + "full_name": testUser.FullName, + "status": testUser.Status, + "email_verified": testUser.EmailVerified, + "mobile_verified": testUser.MobileVerified, + "scopes": testUser.Scopes, + } + + // Create user + userID, err := user.CreateUser(userData) + assert.NoError(t, err) + assert.NotNil(t, userID) + + // Verify user was created + m := model.Select("__yao.user") + createdUser, err := m.Find(userID, model.QueryParam{}) + assert.NoError(t, err) + assert.Equal(t, userData["username"], createdUser["username"]) + assert.Equal(t, userData["email"], createdUser["email"]) + + // Verify user was created with correct default model name + user2 := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + assert.Equal(t, "__yao.user", user2.model) + }) + }) + } +} + +func TestUpdateUserLastLogin(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + cleanupTestData(t) + defer cleanupTestData(t) + + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + // Create test user + testUser := createTestUser("login1") + setupTestUser(t, testUser) + + t.Run("update user last login", func(t *testing.T) { + err := user.UpdateUserLastLogin(testUser.ID) + assert.NoError(t, err) + + // Verify last login was updated + m := model.Select("__yao.user") + updatedUser, err := m.Find(testUser.ID, model.QueryParam{}) + assert.NoError(t, err) + assert.NotNil(t, updatedUser) + assert.NotNil(t, updatedUser["last_login_at"]) + }) + }) + } +} + +func TestTOTPGeneration(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + Model: "__yao.user", + Cache: cache, + TokenStore: tokenStore, + }) + + ctx := context.Background() + + t.Run("generate TOTP secret", func(t *testing.T) { + secret, qrURL, err := user.GenerateTOTPSecret(ctx, "test-user", "Test App", "testuser@example.com") + assert.NoError(t, err) + assert.NotEmpty(t, secret) + assert.NotEmpty(t, qrURL) + assert.Contains(t, qrURL, "otpauth://totp/") + assert.Contains(t, qrURL, "secret=") + assert.Contains(t, qrURL, "issuer=Test+App") + }) + + t.Run("generate TOTP secret with defaults", func(t *testing.T) { + secret, qrURL, err := user.GenerateTOTPSecret(ctx, "test-user", "", "") + assert.NoError(t, err) + assert.NotEmpty(t, secret) + assert.NotEmpty(t, qrURL) + assert.Contains(t, qrURL, "issuer=YAO+OAuth") + }) + }) + } +} + +func TestTOTPVerification(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + Model: "__yao.user", + Cache: cache, + TokenStore: tokenStore, + }) + + t.Run("verify TOTP with secret", func(t *testing.T) { + secret := "JBSWY3DPEHPK3PXP" // Test secret + + // Generate code for current time + now := time.Now().Unix() + timeCounter := now / 30 + expectedCode := user.generateTOTPCode([]byte("Hello!\xDE\xAD\xBE\xEF"), timeCounter, "SHA1", 6) + + // This test might be flaky due to time, so we'll test the method exists + result := user.verifyTOTPWithSecret(secret, expectedCode, "SHA1", 6, 30) + // We can't assert the exact result due to time dependencies + assert.IsType(t, false, result) + }) + + t.Run("generate TOTP code", func(t *testing.T) { + secret := []byte("Hello!\xDE\xAD\xBE\xEF") + timeCounter := int64(1234567890) + + code := user.generateTOTPCode(secret, timeCounter, "SHA1", 6) + assert.Len(t, code, 6) + assert.Regexp(t, `^\d{6}$`, code) + }) + }) + } +} + +func TestTOTPEnabledUserFlow(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + cleanupTestData(t) + defer cleanupTestData(t) + + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + // Create test user + testUser := createTestUser("2fa1") + setupTestUser(t, testUser) + + ctx := context.Background() + + t.Run("enable two factor with invalid code", func(t *testing.T) { + // This test is limited because we can't easily generate a valid TOTP code + // In a real scenario, we'd need to coordinate the secret generation and verification + + secret := "JBSWY3DPEHPK3PXP" + // Using a mock code - in real tests, you'd generate a proper TOTP code + code := "123456" + + err := user.EnableTwoFactor(ctx, fmt.Sprintf("%d", testUser.ID), secret, code) + // This will fail with invalid code, which is expected + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid verification code") + }) + + t.Run("generate recovery codes", func(t *testing.T) { + codes, err := user.GenerateRecoveryCodes(ctx, fmt.Sprintf("%d", testUser.ID)) + assert.NoError(t, err) + assert.Len(t, codes, 10) + + for _, code := range codes { + assert.Len(t, code, 16) // 8 bytes hex = 16 characters + assert.Regexp(t, `^[0-9a-f]{16}$`, code) + } + }) + }) + } +} + +func TestHelperMethods(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + t.Run("generate QR code URL", func(t *testing.T) { + qrURL := user.generateQRCodeURL("JBSWY3DPEHPK3PXP", "Test App", "testuser@example.com") + assert.Contains(t, qrURL, "otpauth://totp/") + assert.Contains(t, qrURL, "secret=JBSWY3DPEHPK3PXP") + assert.Contains(t, qrURL, "issuer=Test+App") + assert.Contains(t, qrURL, "algorithm=SHA1") + assert.Contains(t, qrURL, "digits=6") + assert.Contains(t, qrURL, "period=30") + }) + + t.Run("generate recovery codes list", func(t *testing.T) { + codes, err := user.generateRecoveryCodesList() + assert.NoError(t, err) + assert.Len(t, codes, 10) + + for _, code := range codes { + codeStr := code.(string) + assert.Len(t, codeStr, 16) // 8 bytes hex = 16 characters + assert.Regexp(t, `^[0-9a-f]{16}$`, codeStr) + } + }) + }) + } +} + +func TestErrorHandling(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + cleanupTestData(t) + defer cleanupTestData(t) + + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + ctx := context.Background() + + t.Run("get user by invalid subject", func(t *testing.T) { + retrievedUser, err := user.GetUserBySubject(ctx, "") + assert.Error(t, err) + assert.Nil(t, retrievedUser) + }) + + t.Run("verify TOTP code - user not found", func(t *testing.T) { + verified, err := user.VerifyTOTPCode(ctx, "999999", "123456") + assert.Error(t, err) + assert.False(t, verified) + }) + + t.Run("verify recovery code - user not found", func(t *testing.T) { + verified, err := user.VerifyRecoveryCode(ctx, "999999", "test-code") + assert.Error(t, err) + assert.False(t, verified) + }) + + t.Run("disable two factor - user not found", func(t *testing.T) { + err := user.DisableTwoFactor(ctx, "999999", "123456") + assert.Error(t, err) + }) + }) + } +} + +func TestCacheConsistency(t *testing.T) { + storeConfigs := getStoreConfigs() + + for _, config := range storeConfigs { + t.Run(config.Name, func(t *testing.T) { + cleanupTestData(t) + defer cleanupTestData(t) + + tokenStore := config.GetFunc(t) + cache := getLRUCache(t) + + user := NewDefaultUser(&DefaultUserOptions{ + Prefix: "test:", + + Cache: cache, + TokenStore: tokenStore, + }) + + // Create test user + testUser := createTestUser("cache1") + setupTestUser(t, testUser) + + ctx := context.Background() + + t.Run("cache invalidation on update", func(t *testing.T) { + // First, load user into cache + retrievedUser, err := user.GetUserBySubject(ctx, testUser.Subject) + assert.NoError(t, err) + assert.NotNil(t, retrievedUser) + + // Update user last login (should clear cache) + err = user.UpdateUserLastLogin(testUser.ID) + assert.NoError(t, err) + + // Verify cache was cleared by checking if the key exists + cacheKey := user.cacheKey(fmt.Sprintf("%d", testUser.ID)) + _, exists := cache.Get(cacheKey) + assert.False(t, exists) + }) + + t.Run("cache invalidation on two factor operations", func(t *testing.T) { + // Load user into cache + retrievedUser, err := user.GetUserBySubject(ctx, testUser.Subject) + assert.NoError(t, err) + assert.NotNil(t, retrievedUser) + + // Generate recovery codes (should clear cache) + codes, err := user.GenerateRecoveryCodes(ctx, fmt.Sprintf("%d", testUser.ID)) + assert.NoError(t, err) + assert.Len(t, codes, 10) + + // Verify cache was cleared + cacheKey := user.cacheKey(fmt.Sprintf("%d", testUser.ID)) + _, exists := cache.Get(cacheKey) + assert.False(t, exists) + }) + }) + } +} diff --git a/openapi/oauth/types/interfaces.go b/openapi/oauth/types/interfaces.go index 0478a00d..c95be61a 100644 --- a/openapi/oauth/types/interfaces.go +++ b/openapi/oauth/types/interfaces.go @@ -2,6 +2,7 @@ package types import ( "context" + "time" ) // OAuth interface defines the complete OAuth 2.1 and MCP authorization server functionality @@ -145,6 +146,55 @@ type UserProvider interface { // ValidateUserScope validates if a user has access to requested scopes ValidateUserScope(ctx context.Context, userID string, scopes []string) (bool, error) + + // Token management methods + // StoreToken stores a token with expiration time + StoreToken(accessToken string, tokenData map[string]interface{}, expiration time.Duration) error + + // RevokeToken revokes a token by removing it from storage + RevokeToken(accessToken string) error + + // TokenExists checks if a token exists in storage + TokenExists(accessToken string) bool + + // GetTokenData retrieves token data from storage + GetTokenData(accessToken string) (map[string]interface{}, error) + + // User management methods + // CreateUser creates a new user in the database + CreateUser(userData map[string]interface{}) (interface{}, error) + + // UpdateUserLastLogin updates the user's last login timestamp + UpdateUserLastLogin(userID interface{}) error + + // GetUserByUsername retrieves user by username + GetUserByUsername(username string) (interface{}, error) + + // GetUserByEmail retrieves user by email + GetUserByEmail(email string) (interface{}, error) + + // GetUserForAuth retrieves user information for authentication purposes (internal use only) + // This method includes sensitive fields like password_hash and should not be exposed to external APIs + GetUserForAuth(ctx context.Context, identifier string, identifierType string) (interface{}, error) + + // Two-factor authentication methods + // GenerateTOTPSecret generates a new TOTP secret for user + GenerateTOTPSecret(ctx context.Context, userID string, issuer string, accountName string) (string, string, error) // returns secret and QR code URL + + // EnableTwoFactor enables two-factor authentication for user + EnableTwoFactor(ctx context.Context, userID string, secret string, code string) error + + // DisableTwoFactor disables two-factor authentication for user + DisableTwoFactor(ctx context.Context, userID string, code string) error + + // VerifyTOTPCode verifies a TOTP code for user + VerifyTOTPCode(ctx context.Context, userID string, code string) (bool, error) + + // GenerateRecoveryCodes generates new recovery codes for user + GenerateRecoveryCodes(ctx context.Context, userID string) ([]string, error) + + // VerifyRecoveryCode verifies and consumes a recovery code + VerifyRecoveryCode(ctx context.Context, userID string, code string) (bool, error) } // ClientProvider interface for OAuth client management and persistence diff --git a/openapi/oauth/types/types.go b/openapi/oauth/types/types.go index 9a1af970..376d188e 100644 --- a/openapi/oauth/types/types.go +++ b/openapi/oauth/types/types.go @@ -162,41 +162,6 @@ type DeviceAuthorizationResponse struct { Interval int `json:"interval,omitempty"` } -// UserInfo represents user information from userinfo endpoint -type UserInfo struct { - Subject string `json:"sub"` - Name string `json:"name,omitempty"` - GivenName string `json:"given_name,omitempty"` - FamilyName string `json:"family_name,omitempty"` - MiddleName string `json:"middle_name,omitempty"` - Nickname string `json:"nickname,omitempty"` - PreferredUsername string `json:"preferred_username,omitempty"` - Profile string `json:"profile,omitempty"` - Picture string `json:"picture,omitempty"` - Website string `json:"website,omitempty"` - Email string `json:"email,omitempty"` - EmailVerified bool `json:"email_verified,omitempty"` - Gender string `json:"gender,omitempty"` - Birthdate string `json:"birthdate,omitempty"` - Zoneinfo string `json:"zoneinfo,omitempty"` - Locale string `json:"locale,omitempty"` - PhoneNumber string `json:"phone_number,omitempty"` - PhoneVerified bool `json:"phone_number_verified,omitempty"` - Address *UserAddress `json:"address,omitempty"` - UpdatedAt int64 `json:"updated_at,omitempty"` - CustomClaims map[string]interface{} `json:"-"` -} - -// UserAddress represents user address information -type UserAddress struct { - Formatted string `json:"formatted,omitempty"` - StreetAddress string `json:"street_address,omitempty"` - Locality string `json:"locality,omitempty"` - Region string `json:"region,omitempty"` - PostalCode string `json:"postal_code,omitempty"` - Country string `json:"country,omitempty"` -} - // ClientInfo represents OAuth client information type ClientInfo struct { ClientID string `json:"client_id"` diff --git a/test/utils.go b/test/utils.go index b7eb088f..402009b4 100644 --- a/test/utils.go +++ b/test/utils.go @@ -43,6 +43,7 @@ var testSystemModels = map[string]string{ "__yao.dsl": "yao/models/dsl.mod.yao", "__yao.history": "yao/models/history.mod.yao", "__yao.kb": "yao/models/kb.mod.yao", + "__yao.user": "yao/models/user.mod.yao", } // loadSystemModels load system models for testing diff --git a/yao/models/user.mod.yao b/yao/models/user.mod.yao new file mode 100644 index 00000000..2dcf371a --- /dev/null +++ b/yao/models/user.mod.yao @@ -0,0 +1,267 @@ +{ + "name": "OAuth User", + "label": "OAuth User", + "description": "OAuth user model for authentication and authorization", + "tags": ["oauth", "auth", "user"], + "table": { + "name": "oauth_users", + "comment": "OAuth users table for authentication and authorization" + }, + "columns": [ + { + "name": "id", + "type": "ID", + "label": "ID", + "comment": "Primary key identifier", + "primary": true + }, + { + "name": "subject", + "type": "string", + "label": "Subject", + "comment": "OAuth subject identifier (sub claim)", + "length": 255, + "nullable": true, + "unique": true, + "index": true + }, + { + "name": "username", + "type": "string", + "label": "Username", + "comment": "User login username", + "length": 100, + "nullable": true, + "unique": true, + "index": true + }, + { + "name": "email", + "type": "string", + "label": "Email", + "comment": "User email address", + "length": 255, + "nullable": true, + "unique": true, + "index": true + }, + { + "name": "password_hash", + "type": "string", + "label": "Password Hash", + "comment": "Hashed password for authentication", + "length": 255, + "nullable": true, + "crypt": "PASSWORD" + }, + { + "name": "first_name", + "type": "string", + "label": "First Name", + "comment": "User first name", + "length": 100, + "nullable": true + }, + { + "name": "last_name", + "type": "string", + "label": "Last Name", + "comment": "User last name", + "length": 100, + "nullable": true + }, + { + "name": "full_name", + "type": "string", + "label": "Full Name", + "comment": "User full display name", + "length": 200, + "nullable": true + }, + { + "name": "avatar_url", + "type": "string", + "label": "Avatar URL", + "comment": "URL to user profile picture", + "length": 500, + "nullable": true + }, + { + "name": "mobile", + "type": "string", + "label": "Mobile", + "comment": "User mobile phone number", + "length": 50, + "nullable": true, + "index": true + }, + { + "name": "address", + "type": "text", + "label": "Address", + "comment": "User address information", + "nullable": true + }, + { + "name": "scopes", + "type": "json", + "label": "Scopes", + "comment": "Available OAuth scopes for this user", + "nullable": true + }, + { + "name": "status", + "type": "enum", + "label": "Status", + "comment": "User account status", + "option": ["active", "inactive", "suspended", "pending"], + "default": "pending", + "index": true, + "nullable": false + }, + { + "name": "email_verified", + "type": "boolean", + "label": "Email Verified", + "comment": "Whether user email is verified", + "default": false, + "index": true + }, + { + "name": "mobile_verified", + "type": "boolean", + "label": "Mobile Verified", + "comment": "Whether user mobile phone is verified", + "default": false, + "index": true + }, + { + "name": "two_factor_enabled", + "type": "boolean", + "label": "Two Factor Enabled", + "comment": "Whether two-factor authentication is enabled", + "default": false, + "index": true + }, + { + "name": "two_factor_secret", + "type": "string", + "label": "Two Factor Secret", + "comment": "TOTP shared secret key (Base32 encoded)", + "length": 255, + "nullable": true, + "crypt": "AES" + }, + { + "name": "two_factor_issuer", + "type": "string", + "label": "Two Factor Issuer", + "comment": "Issuer name displayed in authenticator app", + "length": 100, + "nullable": true, + "default": "YAO OAuth" + }, + { + "name": "two_factor_algorithm", + "type": "enum", + "label": "Two Factor Algorithm", + "comment": "TOTP algorithm (SHA1, SHA256, SHA512)", + "option": ["SHA1", "SHA256", "SHA512"], + "default": "SHA1", + "nullable": true + }, + { + "name": "two_factor_digits", + "type": "integer", + "label": "Two Factor Digits", + "comment": "Number of digits in TOTP code (6 or 8)", + "default": 6, + "nullable": true + }, + { + "name": "two_factor_period", + "type": "integer", + "label": "Two Factor Period", + "comment": "TOTP time period in seconds (usually 30)", + "default": 30, + "nullable": true + }, + { + "name": "two_factor_account_name", + "type": "string", + "label": "Two Factor Account Name", + "comment": "Account name displayed in authenticator app (usually username or email)", + "length": 255, + "nullable": true + }, + { + "name": "two_factor_recovery_codes", + "type": "json", + "label": "Two Factor Recovery Codes", + "comment": "Backup recovery codes for two-factor authentication", + "nullable": true + }, + { + "name": "two_factor_enabled_at", + "type": "timestamp", + "label": "Two Factor Enabled At", + "comment": "When two-factor authentication was enabled", + "nullable": true, + "index": true + }, + { + "name": "two_factor_last_verified_at", + "type": "timestamp", + "label": "Two Factor Last Verified At", + "comment": "Last time two-factor authentication was verified", + "nullable": true, + "index": true + }, + { + "name": "last_login_at", + "type": "timestamp", + "label": "Last Login At", + "comment": "Last login timestamp", + "nullable": true, + "index": true + }, + { + "name": "password_changed_at", + "type": "timestamp", + "label": "Password Changed At", + "comment": "When password was last changed", + "nullable": true + }, + { + "name": "metadata", + "type": "json", + "label": "Metadata", + "comment": "Additional user metadata and custom fields", + "nullable": true + }, + { + "name": "preferences", + "type": "json", + "label": "Preferences", + "comment": "User preferences and settings", + "nullable": true + } + ], + "indexes": [ + { + "name": "idx_user_two_factor", + "columns": ["two_factor_enabled", "two_factor_enabled_at"], + "type": "index", + "comment": "Index on two-factor authentication status and time" + }, + { + "name": "idx_user_verification", + "columns": ["email_verified", "mobile_verified"], + "type": "index", + "comment": "Index on verification status for filtering" + } + ], + "relations": {}, + "values": [], + "option": { "timestamps": true, "soft_deletes": true } +}