From 655af2fcb1888203ffd8e762580dc0718c8e0479 Mon Sep 17 00:00:00 2001 From: veypi Date: Tue, 5 Nov 2024 02:57:48 +0800 Subject: [PATCH] feat: user fs auto auth cookie --- oa/api/init.go | 4 ++- oa/api/token/token.go | 54 ++++++++++++++++++++++++++--------- oa/builtin/fs/app.go | 17 ++++------- oa/builtin/fs/user.go | 44 ++++++++++++++++++++-------- oa/builtin/init.go | 5 +++- oa/libs/libs.go | 25 ++++++++++++++++ oa/libs/webdav/dir.html | 4 +-- oa/libs/webdav/dir_render.go | 5 ++-- oa/libs/webdav/webdav.go | 10 +++++-- oa/main.go | 5 ++++ oa/models/token.gen.go | 8 +++--- oa/models/token.go | 2 +- oaer/lib/api/token.ts | 3 +- oaer/lib/assets/css/oaer.scss | 39 ++++++++++++++++++++++++- oaer/lib/components/fsdom.ts | 4 +-- oaer/lib/components/fstree.ts | 48 +++++++++++++++++++++++++++---- oaer/lib/fs.ts | 4 +-- oaer/lib/logic.ts | 10 +++++-- oaer/lib/v2dom/v2dom.ts | 4 ++- oaer/src/main.ts | 14 +++++++-- 20 files changed, 240 insertions(+), 69 deletions(-) create mode 100644 oa/libs/libs.go diff --git a/oa/api/init.go b/oa/api/init.go index 79e2947..42acc6d 100644 --- a/oa/api/init.go +++ b/oa/api/init.go @@ -3,7 +3,6 @@ // 2024-09-20 16:10:16 // Distributed under terms of the MIT license. // -// Auto generated by OneBD. DO NOT EDIT package api @@ -14,11 +13,14 @@ import ( "oa/api/token" "oa/api/user" "oa/cfg" + "oa/libs" "github.com/veypi/OneBD/rest" ) func Use(r rest.Router) { + r.Set("/*", "OPTIONS", libs.CorsAllowAny) + r.Use(libs.CorsAllowAny) access.Use(r.SubRouter("access")) app.Use(r.SubRouter("app")) role.Use(r.SubRouter("role")) diff --git a/oa/api/token/token.go b/oa/api/token/token.go index 042949a..925bd5b 100644 --- a/oa/api/token/token.go +++ b/oa/api/token/token.go @@ -2,6 +2,7 @@ package token import ( "encoding/hex" + "net/http" "oa/cfg" "oa/errs" "oa/libs/auth" @@ -57,6 +58,10 @@ func tokenPost(x *rest.X) (any, error) { claim.IssuedAt = jwt.NewNumericDate(time.Now()) claim.Issuer = "oa" if opts.Refresh != nil { + typ := "app" + if opts.Typ != nil { + typ = *opts.Typ + } // for other app redirect refresh, err := auth.ParseJwt(*opts.Refresh) if err != nil { @@ -69,23 +74,47 @@ func tokenPost(x *rest.X) (any, error) { if err != nil { return nil, err } - if refresh.AID == aid { - // refresh token + if typ == "app" { + if refresh.AID == aid { + // refresh token + claim.AID = refresh.AID + claim.UID = refresh.UID + claim.Name = refresh.Name + claim.Icon = refresh.Icon + claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 10)) + acList := make(auth.Access, 0, 10) + logv.AssertError(cfg.DB().Table("accesses a"). + Select("a.name, a.t_id, a.level"). + Joins("INNER JOIN user_roles ur ON ur.role_id = a.role_id AND ur.user_id = ?", refresh.UID). + Scan(&acList).Error) + claim.Access = acList + } else { + // gen other app token + } + } else if typ == "ufs" { claim.AID = refresh.AID claim.UID = refresh.UID claim.Name = refresh.Name claim.Icon = refresh.Icon claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 10)) - acList := make(auth.Access, 0, 10) - logv.AssertError(cfg.DB().Table("accesses a"). - Select("a.name, a.t_id, a.level"). - Joins("INNER JOIN user_roles ur ON ur.role_id = a.role_id AND ur.user_id = ?", refresh.UID). - Scan(&acList).Error) - claim.Access = acList - } else { - // gen other app token + claim.Access = auth.Access{ + {Name: "fs", TID: "/", Level: auth.Do}, + } + token := logv.AssertFuncErr(auth.GenJwt(claim)) + + cookie := &http.Cookie{ + Name: "fstoken", // Cookie 的名称 + Value: token, // Cookie 的值 + Path: "/fs/u/", // Cookie 的路径,通常是根路径 + MaxAge: 600, // Cookie 的最大年龄,单位是秒 + HttpOnly: true, // 是否仅限 HTTP(S) 访问 + Secure: false, // 是否通过安全连接传输 Cookie + } + http.SetCookie(x, cookie) + + return token, nil } - } else if opts.Code != nil && aid == cfg.Config.ID && opts.Salt != nil { + } else if opts.Code != nil && aid == cfg.Config.ID && opts.Salt != nil && opts.UserID != nil { // for oa login user := &M.User{} err = cfg.DB().Where("id = ?", opts.UserID).Find(user).Error @@ -100,7 +129,7 @@ func tokenPost(x *rest.X) (any, error) { if err != nil || de != user.ID { return nil, errs.AuthFailed } - data.UserID = opts.UserID + data.UserID = *opts.UserID data.AppID = aid if opts.ExpiredAt != nil { data.ExpiredAt = *opts.ExpiredAt @@ -114,7 +143,6 @@ func tokenPost(x *rest.X) (any, error) { data.Device = *opts.Device } data.Ip = x.GetRemoteIp() - data.ExpiredAt = time.Now().Add(time.Hour) logv.AssertError(cfg.DB().Create(data).Error) claim.ID = data.ID claim.AID = aid diff --git a/oa/builtin/fs/app.go b/oa/builtin/fs/app.go index e1420c6..d58aff5 100644 --- a/oa/builtin/fs/app.go +++ b/oa/builtin/fs/app.go @@ -31,18 +31,10 @@ func NewAppFs(prefix string) func(http.ResponseWriter, *http.Request) { client := webdav.NewWebdav(tmp) client.Prefix = prefix + client.RootIndex = 3 client.GenSubPathFunc = func(r *http.Request) (string, error) { // /:aid/*p - dir := strings.TrimPrefix(r.URL.Path, prefix) - dirs := strings.Split(dir[1:], "/") - aid := "" - root := "/" - if len(dirs) > 0 { - aid = dirs[0] - } - if len(dirs) > 1 { - root = "/" + strings.Join(dirs[1:], "/") - } + aid, root := getid(r.URL.Path, prefix) if root == "/" { // if !utils.FileExists(tmp + "/" + aid) { // os.MkdirAll(tmp+"/"+aid, 0744) @@ -53,7 +45,7 @@ func NewAppFs(prefix string) func(http.ResponseWriter, *http.Request) { } if root == "/pub" || strings.HasPrefix(root, "/pub/") { switch r.Method { - case "OPTIONS", "GET", "HEAD", "POST": + case "GET", "HEAD", "POST": return "", nil default: } @@ -66,6 +58,9 @@ func NewAppFs(prefix string) func(http.ResponseWriter, *http.Request) { handlerLevle = auth.DoCreate case "DELETE": handlerLevle = auth.DoDelete + case "OPTIONS": + // options请求不需要权限 + return "", nil default: handlerLevle = auth.DoRead } diff --git a/oa/builtin/fs/user.go b/oa/builtin/fs/user.go index 8d345ac..1935ea7 100644 --- a/oa/builtin/fs/user.go +++ b/oa/builtin/fs/user.go @@ -33,31 +33,51 @@ func NewUserFs(prefix string) func(http.ResponseWriter, *http.Request) { client := webdav.NewWebdav(tmp) client.Prefix = prefix + client.RootIndex = 4 client.Logger = func(r *http.Request, err error) { } client.GenSubPathFunc = func(r *http.Request) (string, error) { - dir := strings.TrimPrefix(r.URL.Path, prefix) - logv.Warn().Msg(dir) + // /:aid/*p + uid, root := getid(r.URL.Path, prefix) + if root == "/" { + if !utils.FileExists(tmp + "/" + uid) { + os.MkdirAll(tmp+"/"+uid, 0744) + } + } + + if r.Method == "OPTIONS" { + return "", nil + } + payload, err := getToken(r) if err != nil { return "", err } - if !strings.HasPrefix(dir, "/") { - dir = "/" + dir - } - if payload.Access.CheckPrefix("fs", dir, auth.Do) { - if dir == "/" { - if !utils.FileExists(tmp + "/" + payload.UID) { - os.MkdirAll(tmp+"/"+payload.UID, 0744) - } - } - return "/" + payload.UID + dir, nil + if payload.Access.CheckPrefix("fs", root, auth.Do) && payload.UID == uid { + return "", nil } return "", errs.AuthNoPerm } return client.ServeHTTP } +func getid(url, prefix string) (string, string) { + if strings.HasSuffix(prefix, "/") { + prefix = prefix[:len(prefix)-1] + } + dir := strings.TrimPrefix(url, prefix) + dirs := strings.Split(dir[1:], "/") + id := "" + root := "/" + if len(dirs) > 0 { + id = dirs[0] + } + if len(dirs) > 1 { + root = "/" + strings.Join(dirs[1:], "/") + } + return id, root +} + func getToken(r *http.Request) (*auth.Claims, error) { authHeader := r.Header.Get("Authorization") token := "" diff --git a/oa/builtin/init.go b/oa/builtin/init.go index 09f3f10..2c91623 100644 --- a/oa/builtin/init.go +++ b/oa/builtin/init.go @@ -13,6 +13,7 @@ import ( "net/url" "oa/builtin/fs" "oa/cfg" + "oa/libs" "github.com/veypi/OneBD/rest" "github.com/veypi/utils/logv" @@ -21,8 +22,10 @@ import ( func Enable(app *rest.Application) { if cfg.Config.FsPath != "" { r := app.Router().SubRouter("fs") + r.Set("/*", "OPTIONS", libs.CorsAllowAny) + r.Use(libs.CorsAllowAny) r.Any("/a/:aid/*p", fs.NewAppFs("/fs/a")) - r.Any("/u/*", fs.NewUserFs("/fs/u")) + r.Any("/u/:uid/*p", fs.NewUserFs("/fs/u")) } tsPorxy := httputil.NewSingleHostReverseProxy(logv.AssertFuncErr(url.Parse("http://v.v:8428"))) fsProxy := fs.NewFs("/home/v/cache/", "") diff --git a/oa/libs/libs.go b/oa/libs/libs.go new file mode 100644 index 0000000..07d2ef9 --- /dev/null +++ b/oa/libs/libs.go @@ -0,0 +1,25 @@ +// +// libs.go +// Copyright (C) 2024 veypi +// 2024-11-04 21:50 +// Distributed under terms of the GPL license. +// + +package libs + +import ( + "net/http" + + "github.com/veypi/OneBD/rest" +) + +func CorsAllowAny(x *rest.X) { + origin := x.Request.Header.Get("Origin") + x.Header().Set("Access-Control-Allow-Origin", origin) + x.Header().Set("Access-Control-Allow-Credentials", "true") + x.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, PATCH, PROPFIND") + x.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, depth") + if x.Request.Method == http.MethodOptions && x.Request.Header.Get("Access-Control-Request-Method") != "" { + x.Stop() + } +} diff --git a/oa/libs/webdav/dir.html b/oa/libs/webdav/dir.html index 9d2f242..5cc85d4 100644 --- a/oa/libs/webdav/dir.html +++ b/oa/libs/webdav/dir.html @@ -130,7 +130,7 @@
- (`/token/salt`, { json }) } export interface PostOpts { - user_id: string refresh?: string + typ?: 'app' | 'ufs' + user_id?: string salt?: string code?: string app_id?: string diff --git a/oaer/lib/assets/css/oaer.scss b/oaer/lib/assets/css/oaer.scss index 0f8599d..cdd70d6 100644 --- a/oaer/lib/assets/css/oaer.scss +++ b/oaer/lib/assets/css/oaer.scss @@ -238,10 +238,47 @@ div[voa] { } .fsdir { + width: 100%; + cursor: pointer; + user-select: none; + .fsdir-header { + width: 100%; display: flex; - justify-content: space-between; + justify-content: start; + gap: 4px; align-items: center; + height: 1.5rem; + line-height: 1.5rem; + border-radius: 0.5rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:hover { + background: rgba(0, 0, 0, 0.1); + } + } + + .fsdir-body { + width: 100%; + + .fsdir-item { + width: 100%; + display: flex; + justify-content: start; + gap: 4px; + border-radius: 0.5rem; + height: 1.5rem; + line-height: 1.5rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:hover { + background: rgba(0, 0, 0, 0.1); + } + } } } } diff --git a/oaer/lib/components/fsdom.ts b/oaer/lib/components/fsdom.ts index 5f8f893..924c3c8 100644 --- a/oaer/lib/components/fsdom.ts +++ b/oaer/lib/components/fsdom.ts @@ -22,14 +22,14 @@ export default () => { class: 'voa-item-title', children: ['我的文件'], onclick: () => { - logic.goto('/') + // logic.goto('/') } }), v({ class: 'voa-fs-subtxt', children: '获取密钥', onclick: () => { api.token.Post({ refresh: logic.token.refresh.raw(), - user_id: logic.token.refresh.uid! + typ: 'ufs' }).then(e => { console.log(e) }) diff --git a/oaer/lib/components/fstree.ts b/oaer/lib/components/fstree.ts index 15577bf..f8cb0d3 100644 --- a/oaer/lib/components/fstree.ts +++ b/oaer/lib/components/fstree.ts @@ -8,28 +8,64 @@ import v, { proxy, vfor } from "../v2dom" import fs, { FileStat } from '../fs' import { vif } from "../v2dom/v2dom" +import logic from "../logic" +import api from "../api" -const dirTree = (url: string) => { +const dirTree = (root: string | FileStat, depth = 0): HTMLElement => { + let url = '' + let name = '' + if (typeof root === 'string') { + url = root + name = root + } else { + url = root.filename + name = root.basename + } let treeItems = proxy.Watch([] as FileStat[]) let expand = proxy.Watch({ value: false }) - let child = v('', - () => - expand.value && treeItems.length ? vfor(treeItems, (item) => v('div', item.basename), undefined, '123') : []) + let child = v({ + class: 'fsdir-body', + children: vfor(treeItems, + (item) => { + if (item.type === 'directory') { + return dirTree(item, depth + 1) + } + return v({ + class: 'fsdir-item', + style: `padding-left: ${depth * 1 + 1}rem`, + children: [v('div', '🔤'), v('div', item.basename)], + onclick: () => { + api.token.Post({ refresh: logic.token.refresh.raw(), typ: 'ufs' }).then(e => { + + logic.goto(fs.user.urlwrap(item.filename), true) + }) + } + }) + } + ), + }) return v('fsdir', [ v({ class: 'fsdir-header', - children: [v('div', () => expand.value + ''), v('div', url)], + children: [v('div', () => expand.value ? '📂' : '📁'), v('div', name)], + style: `padding-left: ${depth * 1}rem`, onclick: () => { expand.value = !expand.value if (expand.value) { fs.user.getDirectoryContents(url).then((e: any) => { + e.sort((a: FileStat, b: FileStat) => { + if (a.type === b.type) { + return a.filename.localeCompare(b.filename) + } + return a.type === 'directory' ? -1 : 1 + }) treeItems.splice(0) treeItems.push(...e) }) } } }), - vif([() => expand.value && treeItems.length, () => child], [1, () => v('div', 123)]) + vif([() => expand.value, () => child]) ]) } diff --git a/oaer/lib/fs.ts b/oaer/lib/fs.ts index 86d698c..123e63c 100644 --- a/oaer/lib/fs.ts +++ b/oaer/lib/fs.ts @@ -84,7 +84,7 @@ class davWraper { } let token = logic.token.oa.raw() -const user = new davWraper(logic.Host(), '/fs/u/', token) +const user = new davWraper(logic.Host(), '/fs/u/' + logic.token.refresh.uid, token) const app = new davWraper(logic.Host(), '/fs/a/' + logic.oa_id, token) @@ -92,7 +92,7 @@ const sync = () => { if (logic.token.oa.isVaild()) { let t = logic.token.oa.raw() // console.warn('sync oafs token: ' + t) - user.set(logic.Host(), '/fs/u/', t) + user.set(logic.Host(), '/fs/u/' + logic.token.refresh.uid, t) app.set(logic.Host(), '/fs/a/' + logic.app_id, t) } } diff --git a/oaer/lib/logic.ts b/oaer/lib/logic.ts index 7207083..5a8fa3c 100644 --- a/oaer/lib/logic.ts +++ b/oaer/lib/logic.ts @@ -68,7 +68,7 @@ class Token { if (this._typ === 'app') { aid = logic.app_id } - api.token.Post({ refresh: logic.token.refresh.raw(), user_id: logic.token.refresh.uid!, app_id: aid }, { needRetry: false }).then(e => { + api.token.Post({ refresh: logic.token.refresh.raw(), app_id: aid }, { needRetry: false }).then(e => { if (this._typ === 'oa' && logic.app_id == logic.oa_id) { logic.token.app.set(e) } @@ -167,8 +167,12 @@ const logic = proxy.Watch({ } return h + url }, - goto(url: string) { - window.location.href = logic.urlwarp(url) + goto(url: string, newtab = false) { + if (newtab) { + window.open(logic.urlwarp(url), '_blank'); + } else { + window.location.href = logic.urlwarp(url) + } }, }) diff --git a/oaer/lib/v2dom/v2dom.ts b/oaer/lib/v2dom/v2dom.ts index 7abf8c3..e09ad88 100644 --- a/oaer/lib/v2dom/v2dom.ts +++ b/oaer/lib/v2dom/v2dom.ts @@ -264,14 +264,16 @@ function handleChildVfor(dom: HTMLElement, data: vforChild, is_listen = fal } } let noneExist = false + let removeChilds: Element[] = [] for (let child of dom.children) { if (child.getAttribute('vbind-iter') === data.iterID && itemIDs.indexOf(child.getAttribute('vbind-iteridx') || '') == -1) { - dom.removeChild(child) + removeChilds.push(child) } if (child.getAttribute('vbind-iternone') === data.iterID) { noneExist = true } } + removeChilds.forEach(e => e.remove()) if (data.obj.length == 0) { if (!noneExist) { dom.appendChild(data.ifnone) diff --git a/oaer/src/main.ts b/oaer/src/main.ts index 83fc738..d63d396 100644 --- a/oaer/src/main.ts +++ b/oaer/src/main.ts @@ -1,7 +1,15 @@ import './style.css' -import oaer from '../lib/main' -oaer.init('http://localhost:3000', '') +// import { proxy } from '../lib/v2dom' +// let a = proxy.Ref(1) +// a.value++ +import oaer from '../lib/main' +let code = + `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI3ODA5YTZlNjhmNDE0OWM5ODFhMmFhNTI0NTE0MzIyZiIsImFpZCI6InRNUnhXejc3UDlBQk5aQTNaSXVvTlFJTGpWQkJJVWRmIiwibmFtZSI6ImFkbWluIiwiaWNvbiI6Imh0dHBzOi8vcHVibGljLnZleXBpLmNvbS9pbWcvYXZhdGFyLzAwNTEuanBnIiwiYWNjZXNzIjpudWxsLCJpc3MiOiJvYSIsImV4cCI6MTczMDk4NDY4MiwiaWF0IjoxNzMwNzI1NDgyLCJqdGkiOiIxOWZlZTc4YjQwN2M0ZTQ5OWI1Yjg2YmJjNTNjMTA2YyJ9.vIriE-L2AZLtigWXAXHrTG2_XIELqp0bAnFEBX0Hw8w` +oaer.init('http://localhost:4000', code).then(e => { + console.log(`login ${e.name}`) +}).catch(() => { + console.log('not login') +}) oaer.render_ui('voa') -console.log(oaer)