feat: user fs auto auth cookie

v3
veypi 3 weeks ago
parent e819b138ae
commit 655af2fcb1

@ -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"))

@ -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

@ -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
}

@ -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 := ""

@ -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/", "")

@ -0,0 +1,25 @@
//
// libs.go
// Copyright (C) 2024 veypi <i@veypi.com>
// 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()
}
}

@ -130,7 +130,7 @@
<body>
<div class="header">
<a href="/" class="icon-home"><svg t="1729512687765" viewBox="0 0 1024 1024" version="1.1"
<a href="{{.root}}" class="icon-home"><svg t="1729512687765" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="3059" width="200" height="200">
<path
d="M946.5 505L560.1 118.8l-25.9-25.9c-12.3-12.2-32.1-12.2-44.4 0L77.5 505c-12.3 12.3-18.9 28.6-18.8 46 0.4 35.2 29.7 63.3 64.9 63.3h42.5V940h691.8V614.3h43.4c17.1 0 33.2-6.7 45.3-18.8 12.1-12.1 18.7-28.2 18.7-45.3 0-17-6.7-33.1-18.8-45.2zM568 868H456V664h112v204z m217.9-325.7V868H632V640c0-22.1-17.9-40-40-40H432c-22.1 0-40 17.9-40 40v228H238.1V542.3h-96l370-369.7 23.1 23.1L882 542.3h-96.1z"
@ -189,7 +189,7 @@
// 当文档加载完成后执行
document.addEventListener("DOMContentLoaded", function () {
var divs = document.getElementsByClassName('path_tag');
var base = "/"
var base = "{{.root}}"
for (var i = 0; i < divs.length; i++) {
var div = divs[i];
if (div.textContent) {

@ -52,7 +52,7 @@ func size2Label(s int64) string {
}
}
func dirList(w http.ResponseWriter, r *http.Request, f File, rootPath string) {
func dirList(w http.ResponseWriter, r *http.Request, f File, rootPath string, rootIndex int) {
// Prefer to use ReadDir instead of Readdir,
// because the former doesn't require calling
@ -104,5 +104,6 @@ func dirList(w http.ResponseWriter, r *http.Request, f File, rootPath string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tpl := logv.AssertFuncErr(template.New("").Parse(string(dirBody)))
logv.AssertError(tpl.Execute(w, map[string]any{"files": files, "dirs": dirs, "path": strings.Split(rootPath, "/"), "cdir": dir_count, "cfile": f_count, "size": size2Label(file_bytes)}))
pathTag := strings.Split(rootPath, "/")
logv.AssertError(tpl.Execute(w, map[string]any{"files": files, "dirs": dirs, "path": pathTag[rootIndex:], "root": strings.Join(pathTag[:rootIndex], "/") + "/", "cdir": dir_count, "cfile": f_count, "size": size2Label(file_bytes)}))
}

@ -16,6 +16,8 @@ import (
"path/filepath"
"strings"
"time"
"github.com/veypi/utils/logv"
)
func NewWebdav(p string) *Handler {
@ -35,6 +37,7 @@ type Handler struct {
Prefix string
EnableDirRender bool
GenSubPathFunc func(*http.Request) (string, error)
RootIndex int
// FileSystem is the virtual file system.
FileSystem FileSystem
// LockSystem is the lock management system.
@ -61,10 +64,11 @@ func (h *Handler) stripPrefix(p string, r *http.Request) (np string, ns int, ne
ns = http.StatusUnauthorized
return
}
if tmp != "" && tmp != "/" {
np = tmp + np
if tmp != "" {
np = "/" + tmp + np
}
}
logv.Warn().Msgf("subpath: %s", np)
return
}
@ -241,7 +245,7 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta
}
if fi.IsDir() {
if h.EnableDirRender {
dirList(w, r, f, r.URL.Path)
dirList(w, r, f, r.URL.Path, h.RootIndex)
return 0, nil
}
return http.StatusMethodNotAllowed, nil

@ -8,6 +8,7 @@
package main
import (
"embed"
"net/http"
"oa/api"
"oa/builtin"
@ -28,6 +29,9 @@ func main() {
}
}
//go:embed static/*
var staticFs embed.FS
func runWeb() error {
go cfg.RunNats()
app, err := rest.New(&cfg.Config.RestConf)
@ -40,6 +44,7 @@ func runWeb() error {
api.Use(apiRouter)
apiRouter.Use(errs.JsonResponse)
apiRouter.SetErrFunc(errs.JsonErrorResponse)
app.Router().EmbedDir("/", staticFs, "static/", "static/index.html")
app.Router().Print()
return app.Run()
}

@ -8,14 +8,14 @@ type TokenSalt struct {
}
type TokenPost struct {
UserID string `json:"user_id" gorm:"index;type:varchar(32)" parse:"json"`
// 两种获取token方式一种用refreshtoken换取apptoken(应用登录)一种用密码加密code换refreshtoken (oa登录)
Refresh *string `json:"refresh" parse:"json"`
Typ *string `json:"typ" parse:"json"`
// 登录方随机生成的salt非用户salt
Salt *string `json:"salt" parse:"json"`
Code *string `json:"code" parse:"json"`
UserID *string `json:"user_id" gorm:"index;type:varchar(32)" parse:"json"`
Salt *string `json:"salt" parse:"json"`
Code *string `json:"code" parse:"json"`
AppID *string `json:"app_id" gorm:"index;type:varchar(32)" parse:"json"`
ExpiredAt *time.Time `json:"expired_at" parse:"json"`

@ -7,7 +7,7 @@ import "time"
// OverPerm 非oa应用获取oa数据的权限由用户设定
type Token struct {
BaseModel
UserID string `json:"user_id" gorm:"index;type:varchar(32)" methods:"post,list" parse:"json"`
UserID string `json:"user_id" gorm:"index;type:varchar(32)" methods:"*post,list" parse:"json"`
User *User `json:"-"`
AppID string `json:"app_id" gorm:"index;type:varchar(32)" methods:"post,list" parse:"json"`
App *App `json:"-"`

@ -15,8 +15,9 @@ export function TokenSalt(json: TokenSaltOpts) {
return webapi.Post<{ id: string, salt: string }>(`/token/salt`, { json })
}
export interface PostOpts {
user_id: string
refresh?: string
typ?: 'app' | 'ufs'
user_id?: string
salt?: string
code?: string
app_id?: string

@ -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);
}
}
}
}
}

@ -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)
})

@ -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])
])
}

@ -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)
}
}

@ -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)
}
},
})

@ -264,14 +264,16 @@ function handleChildVfor(dom: HTMLElement, data: vforChild<any>, 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)

@ -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)

Loading…
Cancel
Save