diff --git a/oa/api/app/app.go b/oa/api/app/app.go index bd4730f..cef1b3d 100644 --- a/oa/api/app/app.go +++ b/oa/api/app/app.go @@ -6,13 +6,14 @@ import ( M "oa/models" "github.com/veypi/OneBD/rest" + "github.com/veypi/utils" "gorm.io/gorm" ) func useApp(r rest.Router) { r.Delete("/:app_id", auth.Check("app", "app_id", auth.DoDelete), appDelete) r.Get("/:app_id", auth.Check("app", "app_id", auth.DoRead), appGet) - r.Get("/", auth.Check("app", "", auth.DoRead), appList) + r.Get("/", appList) r.Patch("/:app_id", auth.Check("app", "app_id", auth.DoUpdate), appPatch) r.Post("/", auth.Check("app", "", auth.DoCreate), appPost) } @@ -50,20 +51,24 @@ func appList(x *rest.X) (any, error) { M.App UserStatus string `json:"user_status"` }, 0, 10) - - query := cfg.DB().Table("apps").Select("apps.*,app_users.status user_status") - uid := x.Request.Context().Value("uid").(string) - if opts.Name != nil { - query = query.Joins("LEFT JOIN app_users ON app_users.app_id = apps.id AND app_users.user_id = ? AND apps.name LIKE ?", uid, opts.Name) + token, err := auth.CheckJWT(x) + if err == nil { + uid := token.UID + query := cfg.DB().Table("apps").Select("apps.*,app_users.status user_status") + if opts.Name != nil { + query = query.Joins("LEFT JOIN app_users ON app_users.app_id = apps.id AND app_users.user_id = ? AND apps.name LIKE ?", uid, opts.Name) + } else { + query = query.Joins("LEFT JOIN app_users ON app_users.app_id = apps.id AND app_users.user_id = ?", uid) + } + err = query.Scan(&data).Error } else { - query = query.Joins("LEFT JOIN app_users ON app_users.app_id = apps.id AND app_users.user_id = ?", uid) + err = cfg.DB().Table("apps").Select("id", "name", "icon").Find(&data).Error } // 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) - err = query.Scan(&data).Error return data, err } @@ -114,6 +119,7 @@ func appPost(x *rest.X) (any, error) { data.Des = *opts.Des } err = cfg.DB().Transaction(func(tx *gorm.DB) error { + data.Key = utils.RandSeq(32) err := tx.Create(data).Error if err != nil { return err diff --git a/oa/api/token/token.go b/oa/api/token/token.go index 925bd5b..7d59aa9 100644 --- a/oa/api/token/token.go +++ b/oa/api/token/token.go @@ -2,6 +2,7 @@ package token import ( "encoding/hex" + "encoding/json" "net/http" "oa/cfg" "oa/errs" @@ -50,13 +51,13 @@ func tokenPost(x *rest.X) (any, error) { return nil, err } aid := cfg.Config.ID - if opts.AppID != nil { + if opts.AppID != nil && *opts.AppID != "" { aid = *opts.AppID } data := &M.Token{} claim := &auth.Claims{} claim.IssuedAt = jwt.NewNumericDate(time.Now()) - claim.Issuer = "oa" + claim.Issuer = cfg.Config.ID if opts.Refresh != nil { typ := "app" if opts.Typ != nil { @@ -74,22 +75,57 @@ func tokenPost(x *rest.X) (any, error) { if err != nil { return nil, err } + claim.AID = aid + claim.UID = refresh.UID + claim.Name = refresh.Name + claim.Icon = refresh.Icon + claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 10)) 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). + Joins("INNER JOIN user_roles ur ON ur.role_id = a.role_id AND ur.user_id = ? AND a.app_id = ?", refresh.UID, aid). Scan(&acList).Error) claim.Access = acList - } else { + if aid == cfg.Config.ID { + return auth.GenJwt(claim) + } + app := &M.App{} + err = cfg.DB().Where("id = ?", aid).First(app).Error + if err != nil { + return nil, err + } + return auth.GenJwtWithKey(claim, app.Key) + } else if aid != cfg.Config.ID { + // 只能生成其他应用的refresh token + newToken := &M.Token{} + newToken.UserID = refresh.UID + newToken.AppID = aid + newToken.ExpiredAt = time.Now().Add(time.Hour * 24) + if opts.OverPerm != nil { + newToken.OverPerm = *opts.OverPerm + } + if opts.Device != nil { + newToken.Device = *opts.Device + } + newToken.Ip = x.GetRemoteIp() + logv.AssertError(cfg.DB().Create(newToken).Error) // gen other app token + claim.ID = newToken.ID + claim.ExpiresAt = jwt.NewNumericDate(newToken.ExpiredAt) + return auth.GenJwt(claim) + } else { + // 其他应用获取访问oa的token + claim.Access = make(auth.Access, 0, 10) + if data.OverPerm != "" { + err = json.Unmarshal([]byte(data.OverPerm), &claim.Access) + if err != nil { + return nil, err + } + } + return auth.GenJwt(claim) } } else if typ == "ufs" { claim.AID = refresh.AID @@ -113,6 +149,8 @@ func tokenPost(x *rest.X) (any, error) { http.SetCookie(x, cookie) return token, nil + } else { + return nil, errs.ArgsInvalid } } else if opts.Code != nil && aid == cfg.Config.ID && opts.Salt != nil && opts.UserID != nil { // for oa login @@ -131,11 +169,7 @@ func tokenPost(x *rest.X) (any, error) { } data.UserID = *opts.UserID data.AppID = aid - if opts.ExpiredAt != nil { - data.ExpiredAt = *opts.ExpiredAt - } else { - data.ExpiredAt = time.Now().Add(time.Hour * 72) - } + data.ExpiredAt = time.Now().Add(time.Hour * 72) if opts.OverPerm != nil { data.OverPerm = *opts.OverPerm } @@ -153,12 +187,10 @@ func tokenPost(x *rest.X) (any, error) { if user.Nickname != "" { claim.Name = user.Nickname } + return auth.GenJwt(claim) } else { return nil, errs.ArgsInvalid } - - token := logv.AssertFuncErr(auth.GenJwt(claim)) - return token, err } func tokenGet(x *rest.X) (any, error) { diff --git a/oa/libs/auth/jwt.go b/oa/libs/auth/jwt.go index 32fbc80..cc283be 100644 --- a/oa/libs/auth/jwt.go +++ b/oa/libs/auth/jwt.go @@ -21,15 +21,15 @@ import ( ) func GenJwt(claim *Claims) (string, error) { + return GenJwtWithKey(claim, cfg.Config.Key) +} +func GenJwtWithKey(claim *Claims, key string) (string, error) { if claim.ExpiresAt == nil { claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Hour)) } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim) - tokenString, err := token.SignedString([]byte(cfg.Config.Key)) - if err != nil { - return "", err - } - return tokenString, nil + return token.SignedString([]byte(key)) + } func ParseJwt(tokenString string) (*Claims, error) { @@ -67,7 +67,6 @@ func CheckJWT(x *rest.X) (*Claims, error) { return nil, err } - x.Request = x.Request.WithContext(context.WithValue(x.Request.Context(), "uid", claims.UID)) return claims, nil } @@ -84,6 +83,7 @@ func Check(target string, pid string, l AuthLevel) func(x *rest.X) error { if !claims.Access.Check(target, tid, l) { return errs.AuthNoPerm } + x.Request = x.Request.WithContext(context.WithValue(x.Request.Context(), "uid", claims.UID)) return nil } } diff --git a/oaer/lib/api/models.ts b/oaer/lib/api/models.ts index 56ceaac..e5800cc 100644 --- a/oaer/lib/api/models.ts +++ b/oaer/lib/api/models.ts @@ -15,11 +15,11 @@ export interface App { name: string icon: string des: string - participate: string init_role_id?: string init_role?: Role init_url: string user_count: number + typ: string status: string user_status: string } diff --git a/oaer/lib/assets/css/animate.scss b/oaer/lib/assets/css/animate.scss index 25fef38..2caa7c1 100644 --- a/oaer/lib/assets/css/animate.scss +++ b/oaer/lib/assets/css/animate.scss @@ -28,7 +28,7 @@ left: 50%; width: 0; height: 0.1em; - background-color: #000; + background-color: rgba(0, 0, 0, 0.2); transition: all 0.3s; } diff --git a/oaer/lib/assets/css/oaer.scss b/oaer/lib/assets/css/oaer.scss index cdd70d6..00978db 100644 --- a/oaer/lib/assets/css/oaer.scss +++ b/oaer/lib/assets/css/oaer.scss @@ -21,7 +21,6 @@ div[voa] { .voa { - user-select: none; cursor: pointer; height: inherit; @@ -30,7 +29,10 @@ div[voa] { justify-content: center; align-items: center; - .voa-off {} + .voa-off { + // color: var(--oafg-base); + font-size: 0.9rem; + } .voa-on { border-radius: 100%; @@ -156,12 +158,12 @@ div[voa] { } .voa-account { - padding: 0 1rem; .voa-account-header { display: flex; - justify-content: space-between; + justify-content: start; align-items: center; + gap: 0.5rem; } .voa-account-body { @@ -183,8 +185,10 @@ div[voa] { } .voa-ab-info { + height: 100%; width: calc(100% - 6rem); display: flex; + justify-content: space-between; flex-direction: column; div { @@ -196,23 +200,35 @@ div[voa] { } } -.voa-item-title { - height: 3rem; - line-height: 3rem; - font-size: 1.25rem; - cursor: pointer; - user-select: none; - font-weight: 500; - margin-left: -1rem; +.voa-item { + padding: 0 1rem; + + .voa-item-header { + display: flex; + margin: 0 -1rem; + height: 3rem; + align-items: center; + + .voa-item-title { + line-height: 3rem; + font-size: 1.25rem; + cursor: pointer; + user-select: none; + font-weight: 500; + } + + } + + .voa-item-body { + margin: 0.5rem 0; + } } -.voa-apps { - padding: 0 1rem; +.voa-apps { + .voa-apps-header {} .voa-apps-body { - margin-top: 1rem; - display: flex; flex-wrap: wrap; gap: 1rem; } @@ -229,13 +245,8 @@ div[voa] { } .voa-fs { - padding: 0 1rem; - .voa-fs-header { - display: flex; - justify-content: space-between; - align-items: center; - } + .voa-fs-header {} .fsdir { width: 100%; @@ -312,3 +323,17 @@ div[voa] { opacity: 0.8; } } + +.vflex { + display: flex; +} + +.vflex-center { + display: flex; + justify-content: center; + align-items: center; +} + +.vflex-grow { + flex-grow: 1; +} diff --git a/oaer/lib/components/account.ts b/oaer/lib/components/account.ts index 7cb4381..1bcb9c3 100644 --- a/oaer/lib/components/account.ts +++ b/oaer/lib/components/account.ts @@ -7,8 +7,8 @@ import { v } from '../v2dom' import logic from '../logic' -export default v('voa-account', [ - v('voa-account-header', [ +export default v('voa-account voa-item', [ + v('voa-account-header voa-item-header', [ v({ class: 'voa-item-title', children: ['我的账户'], @@ -16,9 +16,12 @@ export default v('voa-account', [ logic.goto('/user') } }), - v('voa-ah-2')] + v('vflex-grow'), + v('a', '🙍', { href: logic.urlwarp('/user') }), + v('a', '⚙️', { href: logic.urlwarp('/setting') }), + ] ), - v('voa-account-body', [ + v('voa-account-body voa-item-body', [ v('voa-ab-ico', [v({ typ: 'img', attrs: { 'src': () => `${logic.urlwarp(logic.user.icon)}` } })]), v('voa-ab-info', [ v('voa-abi-1', [v('span', '昵称:'), v('span', () => logic.user.nickname)]), diff --git a/oaer/lib/components/app.ts b/oaer/lib/components/app.ts index 896dcc5..667fd49 100644 --- a/oaer/lib/components/app.ts +++ b/oaer/lib/components/app.ts @@ -17,9 +17,9 @@ export default () => { apps.push(...e.filter((i) => i.user_status === 'ok')) }) return v({ - class: 'voa-apps', + class: 'voa-apps voa-item', children: [ - v('voa-apps-header', + v('voa-apps-header voa-item-header', [v({ class: 'voa-item-title', children: ['我的应用'], @@ -28,7 +28,7 @@ export default () => { } })] ), - v('voa-apps-body', [ + v('voa-apps-body voa-item-body vflex', [ vfor(apps, (data) => v({ class: 'voa-apps-box', diff --git a/oaer/lib/components/fsdom.ts b/oaer/lib/components/fsdom.ts index 924c3c8..d5cfec9 100644 --- a/oaer/lib/components/fsdom.ts +++ b/oaer/lib/components/fsdom.ts @@ -13,10 +13,11 @@ import fstree from "./fstree"; export default () => { + let fsdir = logic.token.oa.access?.Find('fs')?.tid || '/' return v({ - class: 'voa-fs', + class: 'voa-fs voa-item', children: [ - v('voa-fs-header', + v('voa-fs-header voa-item-header', [ v({ class: 'voa-item-title', @@ -25,6 +26,7 @@ export default () => { // logic.goto('/') } }), + v('vflex-grow'), v({ class: 'voa-fs-subtxt', children: '获取密钥', onclick: () => { api.token.Post({ @@ -32,12 +34,17 @@ export default () => { typ: 'ufs' }).then(e => { console.log(e) + navigator.clipboard.writeText(e).then(() => { + alert('文本已复制到剪贴板!'); + }).catch(err => { + console.error('无法复制文本:', err); + }); }) } }) ] ), - v('voa-fs-body', fstree('/')) + v('voa-fs-body voa-item-body', fstree(fsdir)) ] }) } diff --git a/oaer/lib/components/fstree.ts b/oaer/lib/components/fstree.ts index f8cb0d3..f1064c4 100644 --- a/oaer/lib/components/fstree.ts +++ b/oaer/lib/components/fstree.ts @@ -37,7 +37,7 @@ const dirTree = (root: string | FileStat, depth = 0): HTMLElement => { onclick: () => { api.token.Post({ refresh: logic.token.refresh.raw(), typ: 'ufs' }).then(e => { - logic.goto(fs.user.urlwrap(item.filename), true) + logic.goto(fs.user.urlwrap(item.filename), undefined, true) }) } }) diff --git a/oaer/lib/components/index.ts b/oaer/lib/components/index.ts index fe35690..b670447 100644 --- a/oaer/lib/components/index.ts +++ b/oaer/lib/components/index.ts @@ -35,7 +35,7 @@ export default class { let frame_login = v({ class: 'voa-off voa-hover-line-b', vclass: [() => !logic.ready ? 'voa-scale-in' : 'voa-scale-off'], - children: ['登录'], + children: ['Sign in'], onclick: () => { console.log('click login') bus.emit('login') diff --git a/oaer/lib/fs.ts b/oaer/lib/fs.ts index 123e63c..81850c5 100644 --- a/oaer/lib/fs.ts +++ b/oaer/lib/fs.ts @@ -170,11 +170,25 @@ const get_dav = (client: webdav.WebDAVClient, base_url: string) => { } } +const download = (url: string) => { + return new Promise((resolve, reject) => { + fetch(url).then((response: any) => { + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.statusText}`); + } + return response.text() + }).then(txt => { + resolve(txt) + }).catch(reject) + }) +} + export default { user, app, + download, rename, } diff --git a/oaer/lib/logic.ts b/oaer/lib/logic.ts index 5a8fa3c..edaf428 100644 --- a/oaer/lib/logic.ts +++ b/oaer/lib/logic.ts @@ -11,7 +11,9 @@ import { proxy } from './v2dom' class Token { iat?: string + // oa_id iss?: string + // token id jti?: string exp?: number aid?: string @@ -82,6 +84,11 @@ class Token { }) } } +function objectToUrlParams(obj: { [key: string]: any }) { + return Object.keys(obj).map(key => { + return encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]); + }).join('&'); +} // interface Token { // aid: string @@ -154,8 +161,15 @@ const logic = proxy.Watch({ Host() { return logic.host }, - urlwarp(url: string) { + urlwarp(url: string, params?: { [key: string]: any }) { let h = logic.host + if (params) { + if (url.includes('?')) { + url += '&' + objectToUrlParams(params) + } else { + url += '?' + objectToUrlParams(params) + } + } if (!url) { return '' } @@ -167,19 +181,23 @@ const logic = proxy.Watch({ } return h + url }, - goto(url: string, newtab = false) { + goto(url: string, params?: { [key: string]: any }, newtab = false) { if (newtab) { - window.open(logic.urlwarp(url), '_blank'); + window.open(logic.urlwarp(url, params), '_blank'); } else { - window.location.href = logic.urlwarp(url) + window.location.href = logic.urlwarp(url, params) } }, }) bus.on('login', () => { - logic.goto('/login') + logic.goto('/login', { redirect: window.location.href, uuid: logic.app_id }) }) bus.on('logout', () => { + apitoken.value = '' + if (logic.token.refresh.jti) { + api.token.Delete(logic.token.refresh.jti) + } logic.ready = false logic.token.refresh.clear() logic.token.oa.clear() diff --git a/oaer/lib/main.ts b/oaer/lib/main.ts index fece225..f5f2434 100644 --- a/oaer/lib/main.ts +++ b/oaer/lib/main.ts @@ -21,6 +21,9 @@ export default new class { private ui?: ui constructor() { } + logic() { + return logic + } local() { return logic.user } @@ -30,13 +33,18 @@ export default new class { fs() { return fs } - init(host?: string, code?: string) { + init(host?: string, aid?: string, code?: string) { if (host) { logic.host = host set_base_host(host) } + if (aid) { + logic.app_id = aid + } if (code) { logic.token.refresh.set(code) + logic.token.app.clear() + logic.token.oa.clear() } return logic.init() } @@ -60,6 +68,12 @@ export default new class { logout() { bus.emit('logout') } + urlwarp(url: string, params?: { [key: string]: any }) { + return logic.urlwarp(url, params) + } + goto(url: string, params?: { [key: string]: any }, newtab = false) { + return logic.goto(url, params, newtab) + } on(evt: string, fn: (d?: any) => void) { bus.on(evt, fn) } @@ -73,6 +87,9 @@ export default new class { Get: api.app.Get, List: api.app.List, }, + token: { + Post: api.token.Post + } } } Token() { diff --git a/oaer/lib/typs.ts b/oaer/lib/typs.ts index 0947130..c90a833 100644 --- a/oaer/lib/typs.ts +++ b/oaer/lib/typs.ts @@ -93,10 +93,14 @@ export class auths { } return new authLevel(l) } + Find(name: string): modelsSimpleAuth | undefined { + return this.list.find(i => i.name === name) + } } export interface Auths { Get(name: string, rid: string): authLevel + Find(name: string): modelsSimpleAuth | undefined } diff --git a/oaer/src/main.ts b/oaer/src/main.ts index d63d396..119c566 100644 --- a/oaer/src/main.ts +++ b/oaer/src/main.ts @@ -6,10 +6,20 @@ import './style.css' import oaer from '../lib/main' let code = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI3ODA5YTZlNjhmNDE0OWM5ODFhMmFhNTI0NTE0MzIyZiIsImFpZCI6InRNUnhXejc3UDlBQk5aQTNaSXVvTlFJTGpWQkJJVWRmIiwibmFtZSI6ImFkbWluIiwiaWNvbiI6Imh0dHBzOi8vcHVibGljLnZleXBpLmNvbS9pbWcvYXZhdGFyLzAwNTEuanBnIiwiYWNjZXNzIjpudWxsLCJpc3MiOiJvYSIsImV4cCI6MTczMDk4NDY4MiwiaWF0IjoxNzMwNzI1NDgyLCJqdGkiOiIxOWZlZTc4YjQwN2M0ZTQ5OWI1Yjg2YmJjNTNjMTA2YyJ9.vIriE-L2AZLtigWXAXHrTG2_XIELqp0bAnFEBX0Hw8w` -oaer.init('http://localhost:4000', code).then(e => { +let token = '' +let querys = window.location.search.match(/token=([^&]*)/) +if (querys && querys.length > 1) { + token = decodeURIComponent(querys[1]) +} +console.log(token) +oaer.init('http://localhost:3000', '149f48cfc9f24549a4a0269547a64db5', token).then(e => { console.log(`login ${e.name}`) }).catch(() => { console.log('not login') +}).finally(() => { + if (token) { + window.location.search = window.location.search.replace(/token=([^&]*)/, '') + } }) oaer.render_ui('voa') diff --git a/oaweb/app.config.ts b/oaweb/app.config.ts index f9dfb20..9718274 100644 --- a/oaweb/app.config.ts +++ b/oaweb/app.config.ts @@ -4,7 +4,22 @@ * 2024-05-31 18:10 * Distributed under terms of the MIT license. */ +import oaer from '@veypi/oaer' +const ready = ref(false) +oaer.init().then(() => { + api.apitoken.value = oaer.Token() + api.apitoken.set_updator(oaer.TokenRefresh) + console.log('oaer init') + ready.value = true + oaer.on('logout', () => { + api.apitoken.value = '' + ready.value = false + }) +}).catch(() => { + console.log('oaer init error') + oaer.logout() +}) export default defineAppConfig({ // host: window.location.protocol + '//' + window.location.host, @@ -12,8 +27,8 @@ export default defineAppConfig({ // primary: '#2196f3', // gray: '#111' }, - host: '', - id: 'tMRxWz77P9ABNZA3ZIuoNQILjVBBIUdf', + ready: computed(() => ready), + host: window.location.origin, layout: { theme: '', fullscreen: false, diff --git a/oaweb/components/appcard.vue b/oaweb/components/appcard.vue index 6c3a9e3..0c6ccf5 100644 --- a/oaweb/components/appcard.vue +++ b/oaweb/components/appcard.vue @@ -37,6 +37,10 @@ function Go() { router.push('/app/' + props.core.id) return } + if (!oaer.isValid()) { + oaer.login() + return + } // $q.dialog({ // title: '确认', // message: '是否确定申请加入应用 ' + props.core.name, diff --git a/oaweb/layouts/default.vue b/oaweb/layouts/default.vue index 1916366..be959cf 100644 --- a/oaweb/layouts/default.vue +++ b/oaweb/layouts/default.vue @@ -10,17 +10,23 @@
OneAuth
- - - + + + + +
- - -
- +
+ + +
+ +