diff --git a/oa/api/init.go b/oa/api/init.go index 22a591d..ed2c1c3 100644 --- a/oa/api/init.go +++ b/oa/api/init.go @@ -8,12 +8,14 @@ package api import ( - "github.com/veypi/OneBD/rest" "oa/api/access" "oa/api/app" "oa/api/role" "oa/api/token" "oa/api/user" + "oa/cfg" + + "github.com/veypi/OneBD/rest" ) func Use(r rest.Router) { @@ -22,4 +24,11 @@ func Use(r rest.Router) { role.Use(r.SubRouter("role")) user.Use(r.SubRouter("user")) token.Use(r.SubRouter("token")) + r.Get("", baseInfo) +} + +func baseInfo(x *rest.X) (any, error) { + return map[string]any{ + "id": cfg.Config.ID, + }, nil } diff --git a/oa/api/token/token.go b/oa/api/token/token.go index 82fa41e..ad05c1c 100644 --- a/oa/api/token/token.go +++ b/oa/api/token/token.go @@ -56,33 +56,33 @@ func tokenPost(x *rest.X) (any, error) { claim := &auth.Claims{} claim.IssuedAt = jwt.NewNumericDate(time.Now()) claim.Issuer = "oa" - if opts.Token != nil { + if opts.Refresh != nil { // for other app redirect - oldClaim, err := auth.ParseJwt(*opts.Token) - if err != nil { + refresh, err := auth.ParseJwt(*opts.Refresh) + if err != nil || refresh.ID == "" { return nil, err } - err = cfg.DB().Where("id = ?", oldClaim.ID).First(data).Error + err = cfg.DB().Where("id = ?", refresh.ID).First(data).Error if err != nil { return nil, err } - if oldClaim.AID == aid { + if refresh.AID == aid { // refresh token - claim.AID = oldClaim.AID - claim.UID = oldClaim.UID - claim.Name = oldClaim.Name - claim.Icon = oldClaim.Icon + 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 = ?", oldClaim.UID). + 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 opts.Code != nil && aid == cfg.Config.ID { + } else if opts.Code != nil && aid == cfg.Config.ID && opts.Salt != nil { // for oa login user := &M.User{} err = cfg.DB().Where("id = ?", opts.UserID).Find(user).Error diff --git a/oa/models/init.go b/oa/models/init.go index a67f214..8a86e2f 100644 --- a/oa/models/init.go +++ b/oa/models/init.go @@ -57,6 +57,7 @@ func InitDBData() error { initRole := map[string]map[string]uint{ "user": {"admin": 5, "normal": 1}, "app": {"admin": 5, "normal": 2}, + "fs": {"admin": 5, "normal": 5}, } adminID := "" for r, roles := range initRole { diff --git a/oa/models/token.gen.go b/oa/models/token.gen.go index 71b094b..4c824ec 100644 --- a/oa/models/token.gen.go +++ b/oa/models/token.gen.go @@ -10,8 +10,9 @@ type TokenSalt struct { type TokenPost struct { UserID string `json:"user_id" gorm:"index;type:varchar(32)" parse:"json"` - // 两种获取token方式,一种用token换取(应用登录),一种用密码加密code换(oa登录) - Token *string `json:"token" parse:"json"` + // 两种获取token方式,一种用refreshtoken换取apptoken(应用登录),一种用密码加密code换refreshtoken (oa登录) + Refresh *string `json:"refresh" parse:"json"` + // 登录方随机生成的salt,非用户salt Salt *string `json:"salt" parse:"json"` Code *string `json:"code" parse:"json"` diff --git a/oaer/index.d.ts b/oaer/index.d.ts index 534c632..20baf77 100644 --- a/oaer/index.d.ts +++ b/oaer/index.d.ts @@ -1 +1,3 @@ -export function setupCounter(element: HTMLButtonElement): void +export default interface OAER { + logout() +} diff --git a/oaer/lib/api/access.ts b/oaer/lib/api/access.ts new file mode 100644 index 0000000..90d163e --- /dev/null +++ b/oaer/lib/api/access.ts @@ -0,0 +1,34 @@ +// +// Copyright (C) 2024 veypi +// 2024-10-11 14:36:07 +// Distributed under terms of the MIT license. +// + +import webapi from "./webapi" +import * as models from "./models" +export interface ListOpts { + app_id: string + user_id?: string + role_id?: string + name?: string +} +export interface ListQuery { + created_at?: Date + updated_at?: Date +} +export function List(json: ListOpts, query: ListQuery) { + return webapi.Get<[models.Access]>(`/access`, { json, query }) +} + +export interface PostOpts { + app_id: string + user_id?: string + role_id?: string + name: string + t_id: string + level: number +} +export function Post(json: PostOpts) { + return webapi.Post(`/access`, { json }) +} + diff --git a/oaer/lib/api/app.ts b/oaer/lib/api/app.ts index 186dc66..4112338 100644 --- a/oaer/lib/api/app.ts +++ b/oaer/lib/api/app.ts @@ -1,28 +1,109 @@ -/* -* @name: app -* @author: veypi -* @date: 2021-11-17 14:44 -* @description:ap -* @update: 2021-11-17 14:44 -*/ -import ajax from './axios' -import cfg from '../cfg' - -export default { - local: () => cfg.BaseUrl() + '/app/', - getKey(uuid: string) { - return ajax.get(this.local() + uuid, { option: 'key' }) - }, - get(uuid: string) { - return ajax.get(this.local() + uuid) - }, - list() { - return ajax.get(this.local()) - }, - users(uuid: string, user_id: string, data?: any) { - if (uuid === '') { - uuid = '-' - } - return ajax.get(this.local() + uuid + '/user/' + user_id, data) - }, +// +// Copyright (C) 2024 veypi +// 2024-10-11 14:36:07 +// Distributed under terms of the MIT license. +// + +import webapi from "./webapi" +import * as models from "./models" +export function Get(app_id: string) { + return webapi.Get(`/app/${app_id}`, {}) +} + +export interface PatchOpts { + name?: string + icon?: string + des?: string + participate?: string + init_role_id?: string +} +export function Patch(app_id: string, json: PatchOpts) { + return webapi.Patch(`/app/${app_id}`, { json }) +} + +export function Delete(app_id: string) { + return webapi.Delete(`/app/${app_id}`, {}) +} + +export interface PostOpts { + name: string + icon: string + des: string + participate: string +} +export function Post(json: PostOpts) { + return webapi.Post(`/app`, { json }) +} + +export interface ListOpts { + name?: string +} +export function List(json: ListOpts) { + return webapi.Get<[models.App]>(`/app`, { json }) +} + +export interface AppUserGetOpts { + user_id: string +} +export function AppUserGet(app_user_id: string, app_id: string, json: AppUserGetOpts) { + return webapi.Get(`/app/${app_id}/app_user/${app_user_id}`, { json }) +} + +export interface AppUserPatchOpts { + status?: string +} +export function AppUserPatch(app_user_id: string, app_id: string, json: AppUserPatchOpts) { + return webapi.Patch(`/app/${app_id}/app_user/${app_user_id}`, { json }) +} + +export function AppUserDelete(app_user_id: string, app_id: string) { + return webapi.Delete(`/app/${app_id}/app_user/${app_user_id}`, {}) } + +export interface AppUserListOpts { + user_id?: string + status?: string +} +export function AppUserList(app_id: string, json: AppUserListOpts) { + return webapi.Get<[models.AppUser]>(`/app/${app_id}/app_user`, { json }) +} + +export interface AppUserPostOpts { + status: string + user_id: string +} +export function AppUserPost(app_id: string, json: AppUserPostOpts) { + return webapi.Post(`/app/${app_id}/app_user`, { json }) +} + +export interface ResourceListQuery { + created_at?: Date + updated_at?: Date +} +export function ResourceList(app_id: string, query: ResourceListQuery) { + return webapi.Get<[models.Resource]>(`/app/${app_id}/resource`, { query }) +} + +export interface ResourcePostOpts { + name: string + des: string +} +export function ResourcePost(app_id: string, json: ResourcePostOpts) { + return webapi.Post(`/app/${app_id}/resource`, { json }) +} + +export interface ResourceDeleteOpts { + name: string +} +export function ResourceDelete(app_id: string, json: ResourceDeleteOpts) { + return webapi.Delete(`/app/${app_id}/resource`, { json }) +} + +export function ResourceGet(app_id: string) { + return webapi.Get(`/app/${app_id}/resource`, {}) +} + +export function ResourcePatch(app_id: string) { + return webapi.Patch(`/app/${app_id}/resource`, {}) +} + diff --git a/oaer/lib/api/axios.ts b/oaer/lib/api/axios.ts deleted file mode 100644 index f050662..0000000 --- a/oaer/lib/api/axios.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * axios.ts - * Copyright (C) 2023 veypi - * 2023-09-22 20:22 - * Distributed under terms of the MIT license. - */ - -import axios, { AxiosError, AxiosResponse } from 'axios'; -import cfg from '../cfg'; - -// Be careful when using SSR for cross-request state pollution -// due to creating a Singleton instance here; -// If any client changes this (global) instance, it might be a -// good idea to move this instance creation inside of the -// "export default () => {}" function below (which runs individually -// for each client) - -axios.defaults.withCredentials = true -const proxy = axios.create({ - withCredentials: true, - headers: { - 'content-type': 'application/json;charset=UTF-8', - }, -}); - - -// 请求拦截 -const beforeRequest = (config: any) => { - // 设置 token - const token = cfg.token - config.retryTimes = 3 - // NOTE 添加自定义头部 - token && (config.headers.Authorization = `Bearer ${token}`) - // config.headers['auth_token'] = '' - return config -} -proxy.interceptors.request.use(beforeRequest) - -// 响应拦截器 -const responseSuccess = (response: AxiosResponse) => { - // eslint-disable-next-line yoda - // 这里没有必要进行判断,axios 内部已经判断 - // const isOk = 200 <= response.status && response.status < 300 - let data = response.data - if (response.config.method === 'head') { - data = JSON.parse(JSON.stringify(response.headers)) - } - return Promise.resolve(data) -} - -const responseFailed = (error: AxiosError) => { - const { response, config } = error - if (!window.navigator.onLine) { - - alert('没有网络') - return Promise.reject(new Error('请检查网络连接')) - } - - // @ts-ignore - if (!config || !config.retryTimes) { - return Promise.reject(response?.data || response?.headers.error) - }; - // @ts-ignore - const { __retryCount = 0, retryDelay = 300, retryTimes } = config; - // 在请求对象上设置重试次数 - // @ts-ignore - config.__retryCount = __retryCount; - // 判断是否超过了重试次数 - if (__retryCount >= retryTimes) { - return Promise.reject(response?.data || response?.headers.error) - } - // 增加重试次数 - // @ts-ignore - config.__retryCount++; - // 延时处理 - const delay = new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, retryDelay); - }); - // 重新发起请求 - return delay.then(function() { - return axios(config); - }); -} - -proxy.interceptors.response.use(responseSuccess, responseFailed) - - -const ajax = { - get(url: string, data = {}, header?: any) { - return proxy.get(url, { params: data, headers: header }) - }, - head(url: string, data = {}, header?: any) { - return proxy.head(url, { params: data, headers: header }) - }, - delete(url: string, data = {}, header?: any) { - return proxy.delete(url, { params: data, headers: header }) - }, - - post(url: string, data = {}, header?: any) { - return proxy.post(url, data, { headers: header }) - }, - put(url: string, data = {}, header?: any) { - return proxy.put(url, data, { headers: header }) - }, - patch(url: string, data = {}, header?: any) { - return proxy.patch(url, data, { headers: header }) - }, -} - -export default ajax - diff --git a/oaer/lib/api/index.ts b/oaer/lib/api/index.ts index a930c06..c896b0d 100644 --- a/oaer/lib/api/index.ts +++ b/oaer/lib/api/index.ts @@ -1,35 +1,26 @@ -/* - * Copyright (C) 2019 light - * - * Distributed under terms of the MIT license. - */ -import user from './user' -import app from './app' -import cfg from '../cfg' -import ajax from './axios' -import nats from './nats' -import tsdb from './tsdb' +import * as user from "./user" +import * as token from "./token" +import * as role from "./role" +import * as app from "./app" +import * as access from "./access" +import webapi, { token as apitoken } from "./webapi" +const info = () => { + return webapi.Get<{ + id: string + }>('/', {}) +} -const api = { - nats: nats, - tsdb: tsdb, - user: user, - app: app, - info: () => { - return ajax.get(cfg.BaseUrl() + '/info') - }, - refresh_token: () => { - ajax.post(cfg.BaseUrl() + '/app/' + cfg.uuid + '/token/', { app_id: cfg.uuid, token: cfg.refresh }).then(e => { - cfg.token = e - // bus.emit('sync', e) - }).catch(e => { - console.warn(e) - // bus.emit('logout', 'get token failed ' + e) - }) - } +export { + apitoken } -export { api } -export default api +export default { + info, + user, + token, + role, + app, + access +} diff --git a/oaer/lib/api/models.ts b/oaer/lib/api/models.ts new file mode 100644 index 0000000..81f3c55 --- /dev/null +++ b/oaer/lib/api/models.ts @@ -0,0 +1,81 @@ +export interface Access { + created_at: Date + updated_at: Date + app_id: string + user_id?: string + role_id?: string + name: string + tid: string + level: number +} +export interface App { + id: string + created_at: Date + updated_at: Date + name: string + icon: string + des: string + participate: string + init_role_id?: string + init_role?: Role + init_url: string + user_count: number +} +export interface AppUser { + id: string + created_at: Date + updated_at: Date + app_id: string + user_id: string + status: string + app?: App + user?: User +} +export interface Resource { + created_at: Date + updated_at: Date + app_id: string + name: string + des: string +} +export interface Role { + id: string + created_at: Date + updated_at: Date + name: string + des: string + app_id: string + user_count: number +} +export interface Token { + id: string + created_at: Date + updated_at: Date + user_id: string + app_id: string + expired_at: Date + over_perm: string + device: string + ip: string +} +export interface User { + id: string + created_at: Date + updated_at: Date + username: string + nickname: string + icon: string + email: string + phone: string + status: number +} +export interface UserRole { + id: string + created_at: Date + updated_at: Date + user_id: string + role_id: string + app_id: string + status: string +} + diff --git a/oaer/lib/api/nats.ts b/oaer/lib/api/nats.ts deleted file mode 100644 index 6ee3ed6..0000000 --- a/oaer/lib/api/nats.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * nats.ts - * Copyright (C) 2023 veypi - * 2023-10-19 21:36 - * Distributed under terms of the MIT license. - */ - - -import cfg from '../cfg' -import ajax from './axios' - -export default { - local: () => cfg.BaseUrl() + '/nats/', - general() { - return ajax.get(this.local() + 'varz') - }, - conns() { - return ajax.get(this.local() + 'connz', { subs: true }) - }, -} diff --git a/oaer/lib/api/role.ts b/oaer/lib/api/role.ts new file mode 100644 index 0000000..fe5e874 --- /dev/null +++ b/oaer/lib/api/role.ts @@ -0,0 +1,41 @@ +// +// Copyright (C) 2024 veypi +// 2024-10-11 14:36:07 +// Distributed under terms of the MIT license. +// + +import webapi from "./webapi" +import * as models from "./models" +export function Get(role_id: string) { + return webapi.Get(`/role/${role_id}`, { }) +} + +export interface PatchOpts { + name?: string + des?: string + app_id?: string +} +export function Patch(role_id: string, json: PatchOpts) { + return webapi.Patch(`/role/${role_id}`, { json }) +} + +export function Delete(role_id: string) { + return webapi.Delete(`/role/${role_id}`, { }) +} + +export interface PostOpts { + name: string + des: string + app_id: string +} +export function Post(json: PostOpts) { + return webapi.Post(`/role`, { json }) +} + +export interface ListOpts { + name?: string +} +export function List(json: ListOpts) { + return webapi.Get<[models.Role]>(`/role`, { json }) +} + diff --git a/oaer/lib/api/token.ts b/oaer/lib/api/token.ts new file mode 100644 index 0000000..541f3f0 --- /dev/null +++ b/oaer/lib/api/token.ts @@ -0,0 +1,55 @@ +// +// Copyright (C) 2024 veypi +// 2024-10-11 14:36:07 +// Distributed under terms of the MIT license. +// + +import webapi from "./webapi" +import * as models from "./models" +export interface TokenSaltOpts { + username: string + typ?: string +} +// keep +export function TokenSalt(json: TokenSaltOpts) { + return webapi.Post<{ id: string, salt: string }>(`/token/salt`, { json }) +} +export interface PostOpts { + user_id: string + refresh?: string + salt?: string + code?: string + app_id?: string + expired_at?: Date + over_perm?: string + device?: string +} + +// keep +export function Post(json: PostOpts) { + return webapi.Post(`/token`, { json }) +} +export function Get(token_id: string) { + return webapi.Get(`/token/${token_id}`, {}) +} + +export interface PatchOpts { + expired_at?: Date + over_perm?: string +} +export function Patch(token_id: string, json: PatchOpts) { + return webapi.Patch(`/token/${token_id}`, { json }) +} + +export function Delete(token_id: string) { + return webapi.Delete(`/token/${token_id}`, {}) +} + +export interface ListOpts { + user_id: string + app_id: string +} +export function List(json: ListOpts) { + return webapi.Get<[models.Token]>(`/token`, { json }) +} + diff --git a/oaer/lib/api/tsdb.ts b/oaer/lib/api/tsdb.ts deleted file mode 100644 index 9feb20a..0000000 --- a/oaer/lib/api/tsdb.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * tsdb.ts - * Copyright (C) 2023 veypi - * 2023-10-20 23:21 - * Distributed under terms of the MIT license. - */ - - -import cfg from '../cfg' -import ajax from './axios' - -export default { - local: () => cfg.BaseUrl() + '/ts/', - range(query: string, props?: { start?: string, end?: string, step?: string, query?: string }) { - if (query !== undefined) { - // @ts-ignore - props.query = query - } else { - props = { query: query } - } - return ajax.get(this.local() + 'query_range', props) - }, - query(query: string) { - return ajax.get(this.local() + 'query', { query: query }) - } -} diff --git a/oaer/lib/api/user.ts b/oaer/lib/api/user.ts index 10380cc..d672772 100644 --- a/oaer/lib/api/user.ts +++ b/oaer/lib/api/user.ts @@ -1,24 +1,87 @@ -/* - * user.ts - * Copyright (C) 2023 veypi - * 2023-10-05 15:37 - * Distributed under terms of the MIT license. - */ +// +// Copyright (C) 2024 veypi +// 2024-10-11 14:35:47 +// Distributed under terms of the MIT license. +// +import webapi from "./webapi" +import * as models from "./models" +export function Get(user_id: string) { + return webapi.Get(`/user/${user_id}`, { }) +} + +export interface PatchOpts { + username?: string + nickname?: string + icon?: string + email?: string + phone?: string + status?: number +} +export function Patch(user_id: string, json: PatchOpts) { + return webapi.Patch(`/user/${user_id}`, { json }) +} + +export function Delete(user_id: string) { + return webapi.Delete(`/user/${user_id}`, { }) +} + +export interface PostOpts { + username: string + nickname?: string + icon?: string + email?: string + phone?: string + salt: string + code: string +} +export function Post(json: PostOpts) { + return webapi.Post(`/user`, { json }) +} + +export interface ListOpts { + username?: string + nickname?: string + email?: string + phone?: string + status?: number +} +export function List(json: ListOpts) { + return webapi.Get<[models.User]>(`/user`, { json }) +} +export function UserRoleGet(user_role_id: string, user_id: string) { + return webapi.Get(`/user/${user_id}/user_role/${user_role_id}`, { }) +} -import ajax from './axios' -import cfg from '../cfg' +export interface UserRolePatchOpts { + status?: string +} +export function UserRolePatch(user_role_id: string, user_id: string, json: UserRolePatchOpts) { + return webapi.Patch(`/user/${user_id}/user_role/${user_role_id}`, { json }) +} +export interface UserRoleDeleteOpts { + role_id: string + app_id: string +} +export function UserRoleDelete(user_role_id: string, user_id: string, json: UserRoleDeleteOpts) { + return webapi.Delete(`/user/${user_id}/user_role/${user_role_id}`, { json }) +} +export interface UserRolePostOpts { + status: string + role_id: string + app_id: string +} +export function UserRolePost(user_id: string, json: UserRolePostOpts) { + return webapi.Post(`/user/${user_id}/user_role`, { json }) +} -export default { - local: () => cfg.BaseUrl() + '/user/', - search(q: string) { - return ajax.get(this.local(), { username: q }) - }, - get(id: number) { - return ajax.get(this.local() + id) - }, +export interface UserRoleListOpts { + status?: string +} +export function UserRoleList(user_id: string, json: UserRoleListOpts) { + return webapi.Get<[models.UserRole]>(`/user/${user_id}/user_role`, { json }) } diff --git a/oaer/lib/api/webapi.ts b/oaer/lib/api/webapi.ts new file mode 100644 index 0000000..6a48e22 --- /dev/null +++ b/oaer/lib/api/webapi.ts @@ -0,0 +1,155 @@ +// +// Copyright (C) 2024 veypi +// 2024-10-22 17:13:42 +// Distributed under terms of the MIT license. +// + +import axios, { AxiosError, type AxiosResponse } from 'axios'; + +// Be careful when using SSR for cross-request state pollution +// due to creating a Singleton instance here; +// If any client changes this (global) instance, it might be a +// good idea to move this instance creation inside of the +// "export default () => {}" function below (which runs individually +// for each client) + +axios.defaults.withCredentials = true +const proxy = axios.create({ + withCredentials: true, + baseURL: "/api/", + headers: { + 'content-type': 'application/json;charset=UTF-8', + }, +}); + +export const token = { + value: '', + update: () => { + return new Promise((resolve) => { resolve(token.value) }) + }, + set_updator: (fn: () => Promise<{ raw: () => string }>) => { + token.update = () => { + return new Promise((resolve, reject) => { + fn().then((e) => { + resolve(e.raw()) + }).catch(() => { + reject() + }) + }) + } + } +} +// 请求拦截 +const beforeRequest = (config: any) => { + config.retryTimes = 3 + // NOTE 添加自定义头部 + token.value && (config.headers.Authorization = `Bearer ${token.value}`) + // config.headers['auth_token'] = '' + return config +} +proxy.interceptors.request.use(beforeRequest) + +// 响应拦截器 +const responseSuccess = (response: AxiosResponse) => { + // eslint-disable-next-line yoda + // 这里没有必要进行判断,axios 内部已经判断 + // const isOk = 200 <= response.status && response.status < 300 + let data = response.data + if (typeof data === 'object') { + if (data.code !== 0) { + return responseFailed({ response } as any) + } + data = data.data + } + return Promise.resolve(data) +} + +const responseFailed = (error: AxiosError) => { + const { response } = error + const config = response?.config + const data = response?.data || {} as any + if (!window.navigator.onLine) { + alert('没有网络') + return Promise.reject(new Error('请检查网络连接')) + } + + let needRetry = true + if (response?.status == 404) { + needRetry = false + } else if (response?.status == 401) { + needRetry = false + if (data.code === 40103) { + } + } + if (!needRetry) { + return Promise.reject(data || response) + }; + // @ts-ignore + const { __retryCount = 0, retryDelay = 1000, retryTimes } = config; + // 在请求对象上设置重试次数 + // @ts-ignore + config.__retryCount = __retryCount + 1; + // 判断是否超过了重试次数 + if (__retryCount >= retryTimes) { + return Promise.reject(response?.data || response?.headers.error) + } + // 延时处理 + const delay = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, retryDelay); + }); + // 重新发起请求 + return delay.then(function() { + return proxy.request(config as any); + }); +} + +proxy.interceptors.response.use(responseSuccess, responseFailed) + +interface data { + json?: any + query?: any + form?: any + header?: any +} + +function transData(d: data) { + let opts = { params: d.query, data: {}, headers: {} as any } + if (d.form) { + opts.data = d.form + opts.headers['content-type'] = 'application/x-www-form-urlencoded' + } + if (d.json) { + opts.data = d.json + opts.headers['content-type'] = 'application/json' + } + if (d.header) { + opts.headers = Object.assign(opts.headers, d.header) + } + return opts +} + +export const webapi = { + Get(url: string, req: data): Promise { + return proxy.request(Object.assign({ method: 'get', url: url }, transData(req))) + }, + Head(url: string, req: data): Promise { + return proxy.request(Object.assign({ method: 'head', url: url }, transData(req))) + }, + Delete(url: string, req: data): Promise { + return proxy.request(Object.assign({ method: 'delete', url: url }, transData(req))) + }, + + Post(url: string, req: data): Promise { + return proxy.request(Object.assign({ method: 'post', url: url }, transData(req))) + }, + Put(url: string, req: data): Promise { + return proxy.request(Object.assign({ method: 'put', url: url }, transData(req))) + }, + Patch(url: string, req: data): Promise { + return proxy.request(Object.assign({ method: 'patch', url: url }, transData(req))) + }, +} + +export default webapi diff --git a/oaer/lib/cfg.ts b/oaer/lib/cfg.ts deleted file mode 100644 index 2db1939..0000000 --- a/oaer/lib/cfg.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * cfg.ts - * Copyright (C) 2024 veypi - * 2024-07-03 18:22 - * Distributed under terms of the MIT license. - */ - -import proxy from './components/proxy' -let cfg = proxy.Watch({ - uuid: '', - refresh: '', - token: '', - - - app_data: {}, - user: { - username: 'asd', - nickname: '', - email: '', - phone: '', - id: 1, - icon: 'https://public.veypi.com/img/avatar/0001.jpg' - }, - myapps: [ - { name: 'a' }, - { name: 'b' }, - ], - ready: false, - local_user: {}, - - host: '', - _host_nats: '', - nats_pub_key: '', - prefix: '/api', - BaseUrl() { - return this.host + this.prefix - }, - NatsHost() { - if (this._host_nats.startsWith('ws')) { - return this._host_nats - } - return 'ws://' + this._host_nats - }, - media(u: string) { - return this.host + u - }, - goto(url: string) { - if (url.startsWith('http')) { - window.location.href = url - return - } - if (!url.startsWith('/')) { - url = '/' + url - } - window.location.href = this.host + url - }, - Host() { - return this.host || window.location.host - }, - userFileUrl() { - return '/fs/u/' - }, - appFileUrl() { - return `/fs/a/${this.uuid}/` - }, -}) - - -export default cfg diff --git a/oaer/lib/components/account.ts b/oaer/lib/components/account.ts index f9c88c8..322c469 100644 --- a/oaer/lib/components/account.ts +++ b/oaer/lib/components/account.ts @@ -5,22 +5,22 @@ * Distributed under terms of the GPL license. */ import v from './v2dom' -import cfg from '../cfg' +import logic from '../logic' export default v('voa-account', [ v('voa-account-header', [v('voa-ah-1', '我的账户'), v('voa-ah-2')]), v('voa-account-body', [ // v('voa-ab-ico', [v('img', '', (d) => { d.setAttribute('src', cfg.user.icon) })]), - v('voa-ab-ico', [v({ typ: 'img', attrs: { 'src': () => `https://public.veypi.com/img/avatar/000${cfg.user.id}.jpg`, test: () => cfg.user.id } })]), + v('voa-ab-ico', [v({ typ: 'img', attrs: { 'src': () => `https://public.veypi.com/img/avatar/000${logic.user.id}.jpg`, test: () => logic.user.id } })]), v('voa-ab-info', [ - v('voa-abi-1', [v('span', '昵称:'), v('span', () => cfg.user.nickname)]), - v('voa-abi-2', [v('span', '账户:'), v('span', () => cfg.user.username)]), - v('voa-abi-3', [v('span', '邮箱:'), v('span', () => cfg.user.email)]), - v('voa-abi-4', [v('span', '手机:'), v('span', () => cfg.user.phone)]), + v('voa-abi-1', [v('span', '昵称:'), v('span', () => logic.user.nickname)]), + v('voa-abi-2', [v('span', '账户:'), v('span', () => logic.user.username)]), + v('voa-abi-3', [v('span', '邮箱:'), v('span', () => logic.user.email)]), + v('voa-abi-4', [v('span', '手机:'), v('span', () => logic.user.phone)]), v({ typ: 'input', attrs: { 'type': 'number' }, - vbind: [cfg.user, 'id'] + vbind: [logic.user, 'id'] }) ]) ]) diff --git a/oaer/lib/components/app.ts b/oaer/lib/components/app.ts index 5fec02d..2016cfa 100644 --- a/oaer/lib/components/app.ts +++ b/oaer/lib/components/app.ts @@ -5,8 +5,8 @@ * Distributed under terms of the GPL license. */ -import cfg from "../cfg"; import v from "./v2dom"; +import logic from '../logic' export default () => v({ @@ -14,16 +14,16 @@ export default () => v({ children: [ v('voa-apps-header', [v('voa-apps-title', '我的应用')]), v('voa-apps-body', [ - [cfg.myapps, + [logic.myapps, (data) => v('voa-app-box', [v('span', () => data.name)])], v('div', '222'), - [cfg.myapps, + [logic.myapps, (data) => v('voa-app-box', () => data.name)], v({ - typ: 'div', innerHtml: 'add', vbind: [cfg.myapps[0], 'name'], + typ: 'div', inner: 'add', vbind: [logic.myapps[0], 'name'], onclick: () => { - cfg.myapps.splice(0, 1) - cfg.myapps.push({ name: new Date().toLocaleString() }) + logic.myapps.splice(0, 1) + logic.myapps.push({ name: new Date().toLocaleString() }) } }) ]) diff --git a/oaer/lib/components/index.ts b/oaer/lib/components/index.ts index 7102a51..6bfc9fd 100644 --- a/oaer/lib/components/index.ts +++ b/oaer/lib/components/index.ts @@ -7,53 +7,44 @@ import slide from './slide' import v from './v2dom' -import bus from '../bus' +import logic from '../logic' export default class { slide: slide frame: HTMLElement - frame_login?: HTMLElement - frame_user?: HTMLElement - constructor(frame: HTMLElement) { - this.frame = frame - this.frame.classList.add('voa') + constructor() { this.slide = new slide() - this.mount_user() - bus.on('logout', () => { - this.mount_login() - this.slide.hide() - }) + this.frame = v('voa', this.gen_avatar()) + } + mount(root: Element) { + root.appendChild(this.frame) } - mount_login() { - this.frame_login = v({ - class: 'voa-off voa-hover-line-b voa-scale-in', - innerHtml: '登录', + gen_avatar() { + let frame_user = v({ + class: 'voa-on', + vclass: [() => logic.ready ? 'voa-scale-in' : 'voa-scale-off'], + inner: ``, onclick: () => { - console.log('click login') - this.frame_login?.classList.add('voa-scale-off') - this.mount_user() + this.slide.show() + // logic.logout() } }) - if (this.frame_user) { - this.frame.removeChild(this.frame_user) - } - this.frame.appendChild(this.frame_login) - } - mount_user() { - let icon = 'https://public.veypi.com/img/avatar/0001.jpg' - this.frame_user = v({ - class: 'voa-on voa-scale-in', - innerHtml: ` - -`, + let frame_login = v({ + class: 'voa-off voa-hover-line-b', + vclass: [() => !logic.ready ? 'voa-scale-in' : 'voa-scale-off'], + inner: '登录', onclick: () => { - this.slide.show() - // this.mount_login() + console.log('click login') + logic.login() } }) - if (this.frame_login) { - this.frame.removeChild(this.frame_login) + return () => { + console.log(logic.ready) + if (logic.ready) { + return frame_user + } else { + return frame_login + } } - this.frame.appendChild(this.frame_user) } } diff --git a/oaer/lib/components/proxy.ts b/oaer/lib/components/proxy.ts index 87cac77..2d981e3 100644 --- a/oaer/lib/components/proxy.ts +++ b/oaer/lib/components/proxy.ts @@ -6,40 +6,46 @@ */ type voidFn = () => void -const callbackCache: (voidFn | undefined)[] = [] +const callbackCache: ([boolean, voidFn])[] = [] const cacheUpdateList: number[] = [] // 界面更新响应频率40hz setInterval(() => { let list = new Set(cacheUpdateList.splice(0)) for (let l of list) { - if (callbackCache[l]) { - callbackCache[l]() + if (callbackCache[l][0]) { + callbackCache[l][1]() } } }, 25) -// 绑定事件删除 -setInterval(() => { - let exists = new Set() - let check = (dom: Element) => { - let ids = dom.getAttribute('vbind-fns')?.split(',') - if (ids?.length) { - for (let i of ids) { - exists.add(i) - } - } - for (let child of dom.children) { - check(child) - } - } - check(document.body) - for (let i in callbackCache) { - if (!exists.has(i)) { - // 只删除元素,保留位置 - delete callbackCache[i] - // console.log('remove ' + i) - } - } -}, 1000) + +// TODO: 已删除的元素有可能会重新添加回来, +// // 绑定事件删除 +// +// setInterval(() => { +// let exists = new Set() +// let check = (dom: Element) => { +// let ids = dom.getAttribute('vbind-fns')?.split(',') +// if (ids?.length) { +// for (let i of ids) { +// exists.add(i) +// if (!callbackCache[Number(i)][0]) { +// callbackCache[Number(i)][0] = true +// } +// } +// } +// for (let child of dom.children) { +// check(child) +// } +// } +// check(document.body) +// for (let i in callbackCache) { +// if (!exists.has(i)) { +// // 只使能在页面上的更新 +// callbackCache[i][0] = false +// // console.log('remove ' + i) +// } +// } +// }, 1000) function generateUniqueId() { const timestamp = performance.now().toString(36); @@ -49,22 +55,33 @@ function generateUniqueId() { function ForceUpdate() { for (let c of callbackCache) { - if (c) { - c() + if (c[0]) { + c[1]() } } } var listen_tags: number[] = [] -function Listen(callback: voidFn): number { +function listen(callback: voidFn): number { let idx = callbackCache.length listen_tags.push(idx) - callbackCache.push(callback) + callbackCache.push([true, callback]) callback() listen_tags.pop() return idx } +function Listen(dom: HTMLElement, fn: () => void) { + let fns = dom.getAttribute('vbind-fns') + let fnid = listen(fn) + if (fns) { + fns += ',' + fnid + } else { + fns = String(fnid) + } + dom.setAttribute('vbind-fns', fns) +} + const isProxy = Symbol("isProxy") const DataID = Symbol("DataID") diff --git a/oaer/lib/components/slide.ts b/oaer/lib/components/slide.ts index dc3c022..35a1fe6 100644 --- a/oaer/lib/components/slide.ts +++ b/oaer/lib/components/slide.ts @@ -5,9 +5,11 @@ * Distributed under terms of the GPL license. */ import v from './v2dom' -import bus from '../bus' import account from './account' import app from './app' +import logic from '../logic' +import proxy from './proxy' +import bus from '../bus' /* mask @@ -31,7 +33,7 @@ export default class { }) this.footer = v({ class: 'voa-slide-footer', - innerHtml: 'logout', + inner: 'logout', onclick: () => { bus.emit('logout') } @@ -65,6 +67,11 @@ export default class { } } }) + proxy.Listen(this.mask, () => { + if (!logic.ready) { + this.hide() + } + }) document.body.appendChild(this.mask) } show() { diff --git a/oaer/lib/components/v2dom.ts b/oaer/lib/components/v2dom.ts index 3a16bcd..e5d957c 100644 --- a/oaer/lib/components/v2dom.ts +++ b/oaer/lib/components/v2dom.ts @@ -12,9 +12,10 @@ interface buildOpts { id?: string typ?: string class?: string + vclass?: [() => string] attrs?: attrs style?: string - innerHtml?: string + inner?: string | computedFn onclick?: any children?: childTyp[] updator?: (p: HTMLElement) => void @@ -36,17 +37,20 @@ export default (opts: buildOpts | string, inner?: string | computedFn | ch } } let dom = document.createElement(opts.typ || 'div') + if (opts.inner) { + inner = opts.inner + } if (inner) { if (typeof inner == 'string') { - opts.innerHtml = inner + dom.innerHTML = inner } else if (typeof inner == 'function') { - addListener(dom, () => { + proxy.Listen(dom, () => { let res = inner() if (typeof res === 'string') { dom.innerHTML = res } else { dom.innerHTML = '' - dom.appendChild(inner()) + dom.appendChild(res) } }) } else { @@ -62,14 +66,21 @@ export default (opts: buildOpts | string, inner?: string | computedFn | ch if (opts.class) { dom.classList.add(...opts.class.split(' ')) } - if (opts.innerHtml) { - dom.innerHTML = opts.innerHtml + if (opts.vclass) { + for (let vc of opts.vclass) { + let oldvc: string[] = [] + proxy.Listen(dom, () => { + dom.classList.remove(...oldvc) + oldvc = vc().split(' ') + dom.classList.add(...oldvc) + }) + } } if (opts.attrs) { for (let a in opts.attrs) { let attr = opts.attrs[a] if (typeof attr === 'function') { - addListener(dom, () => { + proxy.Listen(dom, () => { dom.setAttribute(a, attr()) }) } else { @@ -86,7 +97,7 @@ export default (opts: buildOpts | string, inner?: string | computedFn | ch if (Object.prototype.toString.call(c) === '[object Array]') { const iterID = proxy.generateUniqueId() const iterLast = tmpID - addListener(dom, () => { + proxy.Listen(dom, () => { c = c as iterChild let itemIDs: string[] = [] for (let i = 0; i < c[0].length; i++) { @@ -171,7 +182,7 @@ export default (opts: buildOpts | string, inner?: string | computedFn | ch } dom.setAttribute('voa', '1') if (opts.updator) { - addListener(dom, () => { + proxy.Listen(dom, () => { opts.updator!(dom) }) } @@ -186,13 +197,3 @@ export default (opts: buildOpts | string, inner?: string | computedFn | ch } -function addListener(dom: HTMLElement, fn: () => void) { - let fns = dom.getAttribute('vbind-fns') - let fnid = proxy.Listen(fn) - if (fns) { - fns += ',' + fnid - } else { - fns = String(fnid) - } - dom.setAttribute('vbind-fns', fns) -} diff --git a/oaer/lib/logic.ts b/oaer/lib/logic.ts index e69de29..f742935 100644 --- a/oaer/lib/logic.ts +++ b/oaer/lib/logic.ts @@ -0,0 +1,170 @@ +/* + * logic.ts + * Copyright (C) 2024 veypi + * 2024-10-24 16:36 + * Distributed under terms of the GPL license. + */ +import bus from './bus' +import proxy from './components/proxy' +import api, { apitoken } from './api' + +class Token { + iat?: string + iss?: string + jti?: string + exp?: number + aid?: string + icon?: string + name?: string + uid?: string + access?: any + private _raw: string + private _typ: 'refresh' | 'oa' | 'app' + constructor(typ: 'refresh' | 'oa' | 'app') { + this._typ = typ + this._raw = localStorage.getItem(typ) || '' + if (this._raw) { + this.set(this._raw) + } + } + isVaild() { + if (this.exp) { + const now = Math.floor(Date.now() / 1000); + return now < this.exp + } + return false + } + set(t: string) { + try { + const parts = t.split('.'); + // header = JSON.parse(atob(parts[0])); + let body = JSON.parse(atob(parts[1])); + // sign = parts[2] + this._raw = t + Object.assign(this, body) + localStorage.setItem(this._typ, t) + } catch (error) { + console.warn('Error parsing JWT:', error); + } + } + clear() { + localStorage.removeItem(this._typ) + this.exp = undefined + } + raw() { + return this._raw + } + update() { + return new Promise((resolve, reject) => { + if (!logic.token.refresh.isVaild() || this._typ === 'refresh') { + reject() + } + let aid = logic.oa_id + 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 }).then(e => { + if (this._typ === 'oa' && logic.app_id == logic.oa_id) { + logic.token.app.set(e) + } + this.set(e) + resolve(this) + }).catch(e => { + console.warn(`get oa token failed: ${e} `) + reject() + }) + }) + } +} + +// interface Token { +// aid: string +// exp: number +// iat: string +// icon: string +// iss: string +// jti: string +// name: string +// uid: string +// isVaild: () => boolean +// row: () => string +// } + +const logic = proxy.Watch({ + oa_id: '', + app_id: '', + token: { + refresh: new Token('refresh'), + oa: new Token('oa'), + app: new Token('app'), + }, + + user: { + username: 'asd', + nickname: '', + email: '', + phone: '', + id: 1, + icon: 'https://public.veypi.com/img/avatar/0001.jpg' + }, + myapps: [ + { name: 'a' }, + { name: 'b' }, + ], + ready: false, + init: () => { + return new Promise((resolve, reject) => { + api.info().then(e => { + logic.oa_id = e.id + if (logic.token.refresh.isVaild()) { + logic.app_id = logic.token.refresh.aid! + logic.token.oa.update().then((e) => { + apitoken.set_updator(logic.token.oa.update) + if (logic.app_id !== logic.oa_id) { + logic.token.app.update().then((e) => { + logic.ready = true + resolve(e); + }).catch(() => reject()) + } else { + logic.ready = true + resolve(e); + } + }).catch(() => { + reject() + }) + } else { + reject() + } + }).catch(e => { + console.warn(`can not get info from ${logic.host}: ${e} `) + reject(); + }) + }) + }, + host: '', + Host() { + return this.host || (window.location.protocol + window.location.host) + }, + goto(url: string) { + if (url.startsWith('http')) { + window.location.href = url + return + } + if (!url.startsWith('/')) { + url = '/' + url + } + window.location.href = this.host + url + }, +}) + +bus.on('login', () => { + logic.goto('/login') +}) +bus.on('logout', () => { + logic.ready = false + logic.token.refresh.clear() + logic.token.oa.clear() + logic.token.app.clear() +}) + +export default logic diff --git a/oaer/lib/main.ts b/oaer/lib/main.ts index 0484ea2..39b93ef 100644 --- a/oaer/lib/main.ts +++ b/oaer/lib/main.ts @@ -7,28 +7,45 @@ import './assets/css/oaer.scss' import bus from './bus' +import logic from './logic' import ui from './components' -export class OAer { - host: string - domid?: string - ui?: ui - constructor(host: string, domid?: string) { - this.host = host - if (domid) { - this.domid = domid - this.ui = new ui(document.querySelector(`#${this.domid}`)!) +export default new class { + private ui?: ui + constructor() { + } + init(host?: string, code?: string) { + if (host) { + logic.host = host + } + if (code) { + logic.token.refresh.set(code) + } + return logic.init() + } + render_ui(domid = 'voa') { + if (!this.ui) { + this.ui = new ui() + } + let root = document.querySelector('#' + domid) + if (root) { + this.ui.mount(root) + } else { + console.warn(`not found
`) } } + isValid() { + return logic.token.refresh.isVaild() + } login() { bus.emit('login') } logout() { bus.emit('logout') } - onlogout(fc: () => void) { - bus.on('logout', fc) + on(evt: string, fn: (d?: any) => void) { + bus.on(evt, fn) } -} +}() diff --git a/oaer/src/main.ts b/oaer/src/main.ts index 06b394d..83fc738 100644 --- a/oaer/src/main.ts +++ b/oaer/src/main.ts @@ -1,6 +1,7 @@ import './style.css' -import { OAer } from '../lib/main' +import oaer from '../lib/main' -let t = new OAer('http://localhost:3000', 'voa') -console.log(t.domid) +oaer.init('http://localhost:3000', '') +oaer.render_ui('voa') +console.log(oaer) diff --git a/oaweb/app.config.ts b/oaweb/app.config.ts index 1a64611..b7cbb56 100644 --- a/oaweb/app.config.ts +++ b/oaweb/app.config.ts @@ -9,7 +9,7 @@ export default defineAppConfig({ // host: window.location.protocol + '//' + window.location.host, host: '', - id: 'FR9P5t8debxc11aFF', + id: 'tMRxWz77P9ABNZA3ZIuoNQILjVBBIUdf', layout: { theme: '', fullscreen: false, diff --git a/oaweb/composables/api/app.ts b/oaweb/composables/api/app.ts index 4112338..f61a6f1 100644 --- a/oaweb/composables/api/app.ts +++ b/oaweb/composables/api/app.ts @@ -7,7 +7,7 @@ import webapi from "./webapi" import * as models from "./models" export function Get(app_id: string) { - return webapi.Get(`/app/${app_id}`, {}) + return webapi.Get(`/app/${app_id}`, { }) } export interface PatchOpts { @@ -22,7 +22,7 @@ export function Patch(app_id: string, json: PatchOpts) { } export function Delete(app_id: string) { - return webapi.Delete(`/app/${app_id}`, {}) + return webapi.Delete(`/app/${app_id}`, { }) } export interface PostOpts { @@ -57,7 +57,7 @@ export function AppUserPatch(app_user_id: string, app_id: string, json: AppUserP } export function AppUserDelete(app_user_id: string, app_id: string) { - return webapi.Delete(`/app/${app_id}/app_user/${app_user_id}`, {}) + return webapi.Delete(`/app/${app_id}/app_user/${app_user_id}`, { }) } export interface AppUserListOpts { @@ -100,10 +100,10 @@ export function ResourceDelete(app_id: string, json: ResourceDeleteOpts) { } export function ResourceGet(app_id: string) { - return webapi.Get(`/app/${app_id}/resource`, {}) + return webapi.Get(`/app/${app_id}/resource`, { }) } export function ResourcePatch(app_id: string) { - return webapi.Patch(`/app/${app_id}/resource`, {}) + return webapi.Patch(`/app/${app_id}/resource`, { }) } diff --git a/oaweb/composables/api/models.ts b/oaweb/composables/api/models.ts index de0b073..81f3c55 100644 --- a/oaweb/composables/api/models.ts +++ b/oaweb/composables/api/models.ts @@ -28,6 +28,8 @@ export interface AppUser { app_id: string user_id: string status: string + app?: App + user?: User } export interface Resource { created_at: Date diff --git a/oaweb/composables/api/token.ts b/oaweb/composables/api/token.ts index c00c2af..e787090 100644 --- a/oaweb/composables/api/token.ts +++ b/oaweb/composables/api/token.ts @@ -16,13 +16,13 @@ export function TokenSalt(json: TokenSaltOpts) { } export interface PostOpts { user_id: string - token?: string salt?: string code?: string app_id?: string expired_at?: Date over_perm?: string device?: string + refresh?: string } // keep @@ -30,7 +30,7 @@ export function Post(json: PostOpts) { return webapi.Post(`/token`, { json }) } export function Get(token_id: string) { - return webapi.Get(`/token/${token_id}`, { }) + return webapi.Get(`/token/${token_id}`, {}) } export interface PatchOpts { @@ -42,7 +42,7 @@ export function Patch(token_id: string, json: PatchOpts) { } export function Delete(token_id: string) { - return webapi.Delete(`/token/${token_id}`, { }) + return webapi.Delete(`/token/${token_id}`, {}) } export interface ListOpts { diff --git a/oaweb/composables/api/webapi.ts b/oaweb/composables/api/webapi.ts index 389347b..6e7c925 100644 --- a/oaweb/composables/api/webapi.ts +++ b/oaweb/composables/api/webapi.ts @@ -1,6 +1,6 @@ // // Copyright (C) 2024 veypi -// 2024-10-11 15:31:08 +// 2024-10-25 18:42:03 // Distributed under terms of the MIT license. // diff --git a/oaweb/composables/models.ts b/oaweb/composables/models.ts index 51b440b..9c101bb 100644 --- a/oaweb/composables/models.ts +++ b/oaweb/composables/models.ts @@ -53,10 +53,10 @@ export interface DocGroup { } export enum AUStatus { - OK = 0, - Disabled = 1, - Applying = 2, - Deny = 3, + OK = "ok", + Disabled = "disabled", + Applying = "applying", + Deny = "deny", } diff --git a/oaweb/layouts/default.vue b/oaweb/layouts/default.vue index b79ad0a..3d02f48 100644 --- a/oaweb/layouts/default.vue +++ b/oaweb/layouts/default.vue @@ -1,4 +1,4 @@ - + +