oaweb quasar and oab rust update

master
veypi 1 year ago
parent ae0ede106a
commit 5d71525ce7

@ -1,16 +1,37 @@
module github.com/veypi/OneAuth module github.com/veypi/OneAuth
go 1.16 go 1.21
require ( require (
github.com/json-iterator/go v1.1.10 github.com/json-iterator/go v1.1.12
github.com/olivere/elastic/v7 v7.0.29 github.com/olivere/elastic/v7 v7.0.29
github.com/urfave/cli/v2 v2.2.0 github.com/urfave/cli/v2 v2.2.0
github.com/veypi/OneBD v0.4.3 github.com/veypi/OneBD v0.4.3
github.com/veypi/utils v0.3.1 github.com/veypi/utils v0.3.1
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
gorm.io/driver/mysql v1.0.5 gorm.io/driver/mysql v1.0.5
gorm.io/driver/sqlite v1.1.4 gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.21.3 gorm.io/gorm v1.21.3
) )
require (
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kardianos/service v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-sqlite3 v1.14.5 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rs/zerolog v1.17.2 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
)
replace github.com/veypi/OneBD v0.4.3 => ../OceanCurrent/OneBD replace github.com/veypi/OneBD v0.4.3 => ../OceanCurrent/OneBD

@ -50,8 +50,8 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kardianos/service v1.1.0 h1:QV2SiEeWK42P0aEmGcsAgjApw/lRxkwopvT+Gu6t1/0= github.com/kardianos/service v1.1.0 h1:QV2SiEeWK42P0aEmGcsAgjApw/lRxkwopvT+Gu6t1/0=
github.com/kardianos/service v1.1.0/go.mod h1:RrJI2xn5vve/r32U5suTbeaSGoMU6GbNPoj36CVYcHc= github.com/kardianos/service v1.1.0/go.mod h1:RrJI2xn5vve/r32U5suTbeaSGoMU6GbNPoj36CVYcHc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@ -65,8 +65,8 @@ github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KK
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/olivere/elastic/v7 v7.0.29 h1:zvorjSPHFli/0owqfoLq0ZOtVhZSyHsMbRi29Vj7T14= github.com/olivere/elastic/v7 v7.0.29 h1:zvorjSPHFli/0owqfoLq0ZOtVhZSyHsMbRi29Vj7T14=
github.com/olivere/elastic/v7 v7.0.29/go.mod h1:8PlkMD2Xb690IPhIPii2SypuuXtXX3dDcSKGqnEGXzE= github.com/olivere/elastic/v7 v7.0.29/go.mod h1:8PlkMD2Xb690IPhIPii2SypuuXtXX3dDcSKGqnEGXzE=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=

@ -41,6 +41,8 @@ func JSONResponse(m OneBD.Meta, data interface{}, err error) {
res["err"] = err.Error() res["err"] = err.Error()
} else { } else {
res["status"] = 1 res["status"] = 1
}
if data != nil {
res["content"] = data res["content"] = data
} }
p, err := json.Marshal(res) p, err := json.Marshal(res)

@ -21,6 +21,11 @@ thiserror = "1.0"
sqlx = { version = "0.5", features = [ "runtime-tokio-rustls", "mysql", "macros", "migrate", "chrono"] } sqlx = { version = "0.5", features = [ "runtime-tokio-rustls", "mysql", "macros", "migrate", "chrono"] }
sea-orm = { version = "^0.12.0", features = [ "sqlx-mysql",
"runtime-tokio-rustls", "macros", "debug-print", "with-chrono",
"with-json", "with-uuid" ] }
actix-web = "4" actix-web = "4"
actix-files = "0.6.2" actix-files = "0.6.2"
jsonwebtoken = "8" jsonwebtoken = "8"

@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS `user`
`id` varchar(32) NOT NULL DEFAULT '' COMMENT 'User UUID', `id` varchar(32) NOT NULL DEFAULT '' COMMENT 'User UUID',
`created` datetime DEFAULT CURRENT_TIMESTAMP, `created` datetime DEFAULT CURRENT_TIMESTAMP,
`updated` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `updated` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`delete_flag` tinyint(1) NOT NULL, `delete_flag` tinyint(1) NOT NULL DEFAULT 0,
`username` varchar(255) NOT NULL UNIQUE, `username` varchar(255) NOT NULL UNIQUE,
`nickname` varchar(255), `nickname` varchar(255),
@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS `user`
`real_code` varchar(32), `real_code` varchar(32),
`check_code` binary(48), `check_code` binary(48),
`status` int NOT NULL COMMENT '状态0ok1disabled', `status` int NOT NULL COMMENT '状态0ok1disabled' DEFAULT 0,
`used` int NOT NULL DEFAULT 0, `used` int NOT NULL DEFAULT 0,
`space` int DEFAULT 300, `space` int DEFAULT 300,
@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS `app`
`id` varchar(32) NOT NULL, `id` varchar(32) NOT NULL,
`created` datetime DEFAULT CURRENT_TIMESTAMP, `created` datetime DEFAULT CURRENT_TIMESTAMP,
`updated` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `updated` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`delete_flag` tinyint(1) NOT NULL, `delete_flag` tinyint(1) NOT NULL DEFAULT 0,
`key` varchar(32) NOT NULL, `key` varchar(32) NOT NULL,
`name` varchar(255) NOT NULL, `name` varchar(255) NOT NULL,
@ -44,7 +44,7 @@ CREATE TABLE IF NOT EXISTS `app`
`role_id` varchar(32), `role_id` varchar(32),
`redirect` varchar(255), `redirect` varchar(255),
`status` int NOT NULL COMMENT '状态0ok1disabled', `status` int NOT NULL COMMENT '状态0ok1disabled' DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@ -56,7 +56,7 @@ CREATE TABLE IF NOT EXISTS `app_user`
`app_id` varchar(32) NOT NULL, `app_id` varchar(32) NOT NULL,
`user_id` varchar(32) NOT NULL, `user_id` varchar(32) NOT NULL,
`status` int NOT NULL DEFAULT 0, `status` int NOT NULL DEFAULT 0 COMMENT '0: ok,1:disabled,2:applying,3:deny',
PRIMARY KEY (`user_id`,`app_id`) USING BTREE, PRIMARY KEY (`user_id`,`app_id`) USING BTREE,
FOREIGN KEY (`app_id`) REFERENCES `app`(`id`), FOREIGN KEY (`app_id`) REFERENCES `app`(`id`),
@ -70,12 +70,12 @@ CREATE TABLE IF NOT EXISTS `role`
`id` varchar(32) NOT NULL, `id` varchar(32) NOT NULL,
`created` datetime DEFAULT CURRENT_TIMESTAMP, `created` datetime DEFAULT CURRENT_TIMESTAMP,
`updated` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `updated` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`delete_flag` tinyint(1) NOT NULL, `delete_flag` tinyint(1) NOT NULL DEFAULT 0,
`app_id` varchar(32) NOT NULL, `app_id` varchar(32) NOT NULL,
`name` varchar(255) NOT NULL, `name` varchar(255) NOT NULL,
`des` varchar(255), `des` varchar(255),
`user_count` int NOT NULL, `user_count` int NOT NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
FOREIGN KEY (`app_id`) REFERENCES `app`(`id`) FOREIGN KEY (`app_id`) REFERENCES `app`(`id`)
@ -88,7 +88,7 @@ CREATE TABLE IF NOT EXISTS `user_role`
`user_id` varchar(32) NOT NULL, `user_id` varchar(32) NOT NULL,
`role_id` varchar(32) NOT NULL, `role_id` varchar(32) NOT NULL,
`status` varchar(32) NOT NULL, `status` varchar(32) NOT NULL DEFAULT 0,
PRIMARY KEY (`user_id`,`role_id`) USING BTREE, PRIMARY KEY (`user_id`,`role_id`) USING BTREE,
FOREIGN KEY (`role_id`) REFERENCES `role`(`id`), FOREIGN KEY (`role_id`) REFERENCES `role`(`id`),
@ -99,7 +99,7 @@ CREATE TABLE IF NOT EXISTS `resource`
( (
`created` datetime DEFAULT CURRENT_TIMESTAMP, `created` datetime DEFAULT CURRENT_TIMESTAMP,
`updated` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `updated` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`delete_flag` tinyint(1) NOT NULL, `delete_flag` tinyint(1) NOT NULL DEFAULT 0,
`app_id` varchar(32) NOT NULL, `app_id` varchar(32) NOT NULL,
`name` varchar(32) NOT NULL, `name` varchar(32) NOT NULL,
@ -113,9 +113,10 @@ CREATE TABLE IF NOT EXISTS `resource`
CREATE TABLE IF NOT EXISTS `access` CREATE TABLE IF NOT EXISTS `access`
( (
`id` int NOT NULL AUTO_INCREMENT,
`created` datetime DEFAULT CURRENT_TIMESTAMP, `created` datetime DEFAULT CURRENT_TIMESTAMP,
`updated` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `updated` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`delete_flag` tinyint(1) NOT NULL, `delete_flag` tinyint(1) NOT NULL DEFAULT 0,
`app_id` varchar(32) NOT NULL, `app_id` varchar(32) NOT NULL,
`name` varchar(32) NOT NULL, `name` varchar(32) NOT NULL,
@ -126,6 +127,7 @@ CREATE TABLE IF NOT EXISTS `access`
`level` int DEFAULT 0, `level` int DEFAULT 0,
-- PRIMARY KEY (`app_id`,`name`, `role_id`, `user_id`) USING BTREE, -- PRIMARY KEY (`app_id`,`name`, `role_id`, `user_id`) USING BTREE,
PRIMARY KEY (`id`),
FOREIGN KEY (`role_id`) REFERENCES `role`(`id`), FOREIGN KEY (`role_id`) REFERENCES `role`(`id`),
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`), FOREIGN KEY (`user_id`) REFERENCES `user`(`id`),
FOREIGN KEY (`app_id`,`name`) REFERENCES `resource`(`app_id`,`name`) FOREIGN KEY (`app_id`,`name`) REFERENCES `resource`(`app_id`,`name`)
@ -144,8 +146,8 @@ INSERT INTO `role` (`id`, `app_id`, `name`)
VALUES ('1lytMwQL4uiNd0vsc', 'FR9P5t8debxc11aFF', 'admin'); VALUES ('1lytMwQL4uiNd0vsc', 'FR9P5t8debxc11aFF', 'admin');
INSERT INTO `access` (`app_id`, `name`, `role_id`, `user_id`,`level`) INSERT INTO `access` (`app_id`, `name`, `role_id`, `user_id`,`level`)
VALUES ('FR9P5t8debxc11aFF', 'app', '1lytMwQL4uiNd0vsc', NULL,6), VALUES ('FR9P5t8debxc11aFF', 'app', '1lytMwQL4uiNd0vsc', NULL,5),
('FR9P5t8debxc11aFF', 'user', '1lytMwQL4uiNd0vsc', NULL,6); ('FR9P5t8debxc11aFF', 'user', '1lytMwQL4uiNd0vsc', NULL,5);

@ -0,0 +1,39 @@
//
// appuser.rs
// Copyright (C) 2023 veypi <i@veypi.com>
// 2023-09-30 23:11
// Distributed under terms of the MIT license.
//
use actix_web::{delete, get, post, web, Responder};
use proc::access_read;
use serde::{Deserialize, Serialize};
use tracing::info;
use crate::{models, Error, Result, CONFIG};
#[get("/app/{aid}/user/{uid}")]
#[access_read("app")]
pub async fn get(params: web::Path<(String, String)>) -> Result<impl Responder> {
let (mut aid, mut uid) = params.into_inner();
if uid == "-" {
uid = "".to_string();
}
if aid == "-" {
aid = "".to_string();
}
let sql = format!("select * from app_user where");
info!("111|{}|{}|", aid, uid);
if uid.is_empty() && aid.is_empty() {
Err(Error::Missing("uid or aid".to_string()))
} else {
let s = sqlx::query_as::<_, models::AppUser>(
"select * from app_user where app_id = ? and user_id = ?",
)
.bind(aid)
.bind(uid)
.fetch_all(CONFIG.db())
.await?;
Ok(web::Json(s))
}
}

@ -8,6 +8,7 @@
mod access; mod access;
mod app; mod app;
mod appuser;
mod resource; mod resource;
mod role; mod role;
mod user; mod user;
@ -34,5 +35,8 @@ pub fn routes(cfg: &mut web::ServiceConfig) {
.service(app::list) .service(app::list)
.service(app::create) .service(app::create)
.service(app::del); .service(app::del);
cfg.service(appuser::get);
cfg.service(greet); cfg.service(greet);
} }

@ -184,7 +184,7 @@ pub struct RegisterOpt {
pub async fn register(q: web::Json<RegisterOpt>) -> Result<String> { pub async fn register(q: web::Json<RegisterOpt>) -> Result<String> {
let q = q.into_inner(); let q = q.into_inner();
// let mut tx = dbtx().await; // let mut tx = dbtx().await;
println!("{:#?}", q); info!("{:#?}", q);
let u: Option<models::User> = let u: Option<models::User> =
sqlx::query_as::<_, models::User>("select * from user where username = ?") sqlx::query_as::<_, models::User>("select * from user where username = ?")
.bind(q.username.clone()) .bind(q.username.clone())

@ -9,7 +9,6 @@
// //
use std::{ use std::{
borrow::Borrow,
fs::File, fs::File,
io::{self, Read}, io::{self, Read},
}; };
@ -17,7 +16,6 @@ use std::{
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use sqlx::{mysql::MySqlPoolOptions, Pool}; use sqlx::{mysql::MySqlPoolOptions, Pool};
use tracing::log::warn;
lazy_static! { lazy_static! {
pub static ref CLI: AppCli = AppCli::new(); pub static ref CLI: AppCli = AppCli::new();
@ -110,7 +108,7 @@ impl ApplicationConfig {
debug: true, debug: true,
server_url: "127.0.0.1:4001".to_string(), server_url: "127.0.0.1:4001".to_string(),
media_path: "/Users/veypi/test/media/".to_string(), media_path: "/Users/veypi/test/media/".to_string(),
db_url: "127.0.0.1:3306".to_string(), db_url: "localhost:3306".to_string(),
db_user: "root".to_string(), db_user: "root".to_string(),
db_pass: "123456".to_string(), db_pass: "123456".to_string(),
db_name: "test".to_string(), db_name: "test".to_string(),

@ -189,6 +189,7 @@ pub enum AccessLevel {
Create = 2, Create = 2,
Update = 3, Update = 3,
Delete = 4, Delete = 4,
ALL = 5,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -217,7 +218,6 @@ impl Token {
} }
} }
pub fn is_valid(&self) -> bool { pub fn is_valid(&self) -> bool {
info!("{}/{}", self.exp, Utc::now().timestamp());
if self.exp > Utc::now().timestamp() { if self.exp > Utc::now().timestamp() {
true true
} else { } else {
@ -234,7 +234,7 @@ impl Token {
} }
fn check(&self, domain: &str, did: &str, l: AccessLevel) -> bool { fn check(&self, domain: &str, did: &str, l: AccessLevel) -> bool {
println!("{:#?}|{:#?}|{}|", self.access, domain, did); info!("{:#?}|{:#?}|{}|", self.access, domain, did);
match &self.access { match &self.access {
Some(ac) => { Some(ac) => {
for ele in ac { for ele in ac {

@ -6,3 +6,4 @@
.eslintrc.js .eslintrc.js
/src-ssr /src-ssr
/quasar.config.*.temporary.compiled* /quasar.config.*.temporary.compiled*
/src/libs/

@ -69,6 +69,10 @@ module.exports = {
// add your custom rules here // add your custom rules here
rules: { rules: {
'@typescript-eslint/no-empty-function': 0,
'vue/multi-word-component-names': 0,
'@typescript-eslint/no-unused-vars': 0,
'@typescript-eslint/no-explicit-any': 0,
'prefer-promise-reject-errors': 'off', 'prefer-promise-reject-errors': 'off',

@ -1,21 +1,24 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title><%= productName %></title> <title>
OA
</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="description" content="<%= productDescription %>"> <meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no"> <meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>"> <meta name="viewport"
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
<link rel="icon" type="image/ico" href="favicon.ico"> <link rel="icon" type="image/ico" href="favicon.ico">
</head> </head>
<body> <body>
<div id='v-msg'></div>
<!-- quasar:entry-point --> <!-- quasar:entry-point -->
</body> </body>
</html> </html>

@ -14,7 +14,9 @@
}, },
"dependencies": { "dependencies": {
"@quasar/extras": "^1.16.4", "@quasar/extras": "^1.16.4",
"@veypi/msg": "^0.1.0",
"axios": "^1.2.1", "axios": "^1.2.1",
"js-base64": "^3.7.5",
"pinia": "^2.0.11", "pinia": "^2.0.11",
"quasar": "^2.6.0", "quasar": "^2.6.0",
"vue": "^3.0.0", "vue": "^3.0.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

@ -14,8 +14,11 @@ const path = require('path');
module.exports = configure(function(/* ctx */) { module.exports = configure(function(/* ctx */) {
return { return {
resolve: {
},
eslint: { eslint: {
// fix: true, fix: true,
// include: [], // include: [],
// exclude: [], // exclude: [],
// rawOptions: {}, // rawOptions: {},
@ -31,7 +34,8 @@ module.exports = configure(function(/* ctx */) {
// https://v2.quasar.dev/quasar-cli-vite/boot-files // https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: [ boot: [
'i18n', 'i18n',
'axios', 'api',
'pack',
], ],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
@ -123,7 +127,11 @@ module.exports = configure(function(/* ctx */) {
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
framework: { framework: {
config: {}, config: {
loadingBar: {
}
},
// iconSet: 'material-icons', // Quasar icon set // iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack // lang: 'en-US', // Quasar language pack
@ -136,7 +144,9 @@ module.exports = configure(function(/* ctx */) {
// directives: [], // directives: [],
// Quasar plugins // Quasar plugins
plugins: [] plugins: [
'LoadingBar'
]
}, },
// animations: 'all', // --- includes all animations // animations: 'all', // --- includes all animations

@ -3,5 +3,31 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useQuasar } from 'quasar';
import { onBeforeMount } from 'vue';
import { useUserStore } from './stores/user';
const $q = useQuasar()
$q.iconMapFn = (iconName) => {
// iconName is the content of QIcon "name" prop
// your custom approach, the following
// is just an example:
console.log(iconName)
if (iconName.startsWith('app:') === true) {
// we strip the "app:" part
const name = iconName.substring(4)
return {
cls: 'my-app-icon ' + name
}
}
}
onBeforeMount(() => {
useUserStore().fetchUserData()
})
</script> </script>

File diff suppressed because one or more lines are too long

@ -0,0 +1,48 @@
/*
* app.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-09-30 17:31
* Distributed under terms of the MIT license.
*/
import ajax from './axios'
export default {
local: './app/',
self() {
return ajax.get(this.local, { option: 'oa' })
},
getKey(uuid: string) {
return ajax.get(this.local + uuid, { option: 'key' })
},
create(name: string, icon: string) {
return ajax.post(this.local, { name, icon })
},
get(uuid: string) {
return ajax.get(this.local + uuid)
},
list() {
return ajax.get(this.local)
},
update(uuid: string, props: any) {
return ajax.patch(this.local + uuid, props)
},
user(uuid: string) {
if (uuid === '') {
uuid = '-'
}
return {
local: this.local + uuid + '/user/',
list(uid: number) {
return ajax.get(this.local + uid)
},
add(uid: number) {
return ajax.post(this.local + uid)
},
update(uid: number, status: string) {
return ajax.patch(this.local + uid, { status })
},
}
},
}

@ -0,0 +1,97 @@
/*
* axios.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-09-22 20:22
* Distributed under terms of the MIT license.
*/
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import msg from '@veypi/msg'
// 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)
const proxy = axios.create({
baseURL: '/api/',
withCredentials: true,
headers: {
'content-type': 'application/json;charset=UTF-8',
},
});
// 请求拦截
const beforeRequest = (config: any) => {
// 设置 token
const token = localStorage.getItem('auth_token')
// NOTE 添加自定义头部
token && (config.headers.auth_token = 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 } = error
const code = error.response?.status
const e_msg = error.response?.headers.error
if (e_msg) {
msg.Warn(e_msg)
}
if (code == 404) {
console.warn('api not exist: ')
return
}
if (response) {
return Promise.reject()
} else if (!window.navigator.onLine) {
alert('没有网络')
return Promise.reject(new Error('请检查网络连接'))
}
return Promise.reject(error.response)
}
proxy.interceptors.response.use(responseSuccess, responseFailed)
const ajax = {
get(url: string, data = {}, header?: any) {
return proxy.get<any, any>(url, { params: data, headers: header })
},
head(url: string, data = {}, header?: any) {
return proxy.head<any, any>(url, { params: data, headers: header })
},
delete(url: string, data = {}, header?: any) {
return proxy.delete<any, any>(url, { params: data, headers: header })
},
post(url: string, data = {}, header?: any) {
return proxy.post<any, any>(url, data, { headers: header })
},
put(url: string, data = {}, header?: any) {
return proxy.put<any, any>(url, data, { headers: header })
},
patch(url: string, data = {}, header?: any) {
return proxy.patch<any, any>(url, data, { headers: header })
},
}
export default ajax

@ -0,0 +1,23 @@
/*
* index.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-09-22 20:17
* Distributed under terms of the MIT license.
*/
import app from "./app";
import token from "./token";
import user from "./user";
const api = {
user: user,
app: app,
token: token
}
export default api;

@ -0,0 +1,18 @@
/*
* token.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-09-30 17:37
* Distributed under terms of the MIT license.
*/
import ajax from './axios'
export default (uuid: string) => {
return {
local: './app/' + uuid + '/token/',
get() {
return ajax.get(this.local)
},
}
}

@ -0,0 +1,41 @@
/*
* user.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-09-22 20:18
* Distributed under terms of the MIT license.
*/
import { Base64 } from 'js-base64'
import ajax from './axios'
export default {
local: './user/',
register(username: string, password: string, prop?: any) {
const data = Object.assign({
username: username,
password: Base64.encode(password),
}, prop)
return ajax.post(this.local, data)
},
login(username: string, password: string) {
return ajax.head(this.local + username, {
typ: 'username',
password: Base64.encode(password),
})
},
search(q: string) {
return ajax.get(this.local, { username: q })
},
get(id: number) {
return ajax.get(this.local + id)
},
list() {
return ajax.get(this.local)
},
update(id: number, props: any) {
return ajax.patch(this.local + id, props)
},
}

@ -1,31 +0,0 @@
import { boot } from 'quasar/wrappers';
import axios, { AxiosInstance } from 'axios';
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$axios: AxiosInstance;
$api: AxiosInstance;
}
}
// 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)
const api = axios.create({ baseURL: 'https://api.example.com' });
export default boot(({ app }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios;
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file
app.config.globalProperties.$api = api;
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
// so you can easily perform requests against your app's API
});
export { api };

@ -0,0 +1,18 @@
/*
* pack.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-09-26 20:38
* Distributed under terms of the MIT license.
*/
import { boot } from 'quasar/wrappers'
import '@veypi/msg/index.css'
import '../assets/icon.js'
// "async" is optional;
// more info on params: https://v2.quasar.dev/quasar-cli/boot-files
export default boot(async (/* { app, router, ... } */) => {
// something to do
})

@ -0,0 +1,80 @@
<template>
<div class="core rounded-2xl p-3">
<div class="grid gap-4 grid-cols-5">
<div class="col-span-2">
<q-avatar style="--color: none" @click="Go" round size="xl" :icon="core.icon">
</q-avatar>
</div>
<div class="col-span-3 grid grid-cols-1 items-center text-left">
<div class="h-10 flex items-center text-2xl italic font-bold">
{{ core.name }}
</div>
<span class="truncate">{{ core.des }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import msg from "@veypi/msg";
import api from "src/boot/api";
import { AUStatus, modelsApp, modelsAppUser } from "src/models";
import { useUserStore } from "src/stores/user";
import { useRouter } from "vue-router"
const router = useRouter()
let props = withDefaults(defineProps<{
core: modelsApp
}>(),
{}
)
function Go() {
switch (props.core.au.status) {
case AUStatus.OK:
router.push({ name: "app.main", params: { uuid: props.core.UUID } });
return;
case AUStatus.Applying:
msg.Info("请等待管理员审批进入");
return;
case AUStatus.Deny:
msg.Warn("进入申请未通过");
return;
case AUStatus.Disabled:
msg.Warn("已被禁止使用");
return;
}
// api.app.user(props.core.id).add(useUserStore().id).then(e => {
// console.log(e)
// })
// api.app
// .user(props.core.UUID)
// .add(store.state.user.id)
// .Start(
// (e) => {
// bar.finish();
// if (e.Status === "ok") {
// router.push({ name: "app.main", params: { uuid: props.core.UUID } });
// return;
// }
// props.core.UserStatus = e.Status;
// msg.info("");
// },
// (e) => {
// msg.warning(": " + e);
// bar.error();
// }
// );
// return;
}
</script>
<style scoped>
.core {
width: 256px;
background: rgba(146, 145, 145, 0.1);
}
</style>

@ -1,53 +1,54 @@
<template> <template>
<q-layout view="lHh Lpr lFf"> <q-layout view="hHh LpR fFf">
<q-header elevated> <q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar> <q-toolbar>
<q-btn <q-icon size="xl" color="aqua" name='svguse:#icon-glassdoor'></q-icon>
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>
<q-toolbar-title> <q-toolbar-title>
Quasar App 统一认证系统
</q-toolbar-title> </q-toolbar-title>
<div>Quasar v{{ $q.version }}</div> <div>OneAuth v2.0.0</div>
</q-toolbar>
<q-toolbar class="">
<q-icon @click="toggleLeftDrawer" class="cursor-pointer" name="menu" size="sm"></q-icon>
<q-tabs align="left">
<q-route-tab to="/page1" label="Page One" />
<q-route-tab to="/page2" label="Page Two" />
<q-route-tab to="/page3" label="Page Three" />
</q-tabs>
</q-toolbar> </q-toolbar>
</q-header> </q-header>
<q-drawer <q-drawer v-model="leftDrawerOpen" side="left" bordered>
v-model="leftDrawerOpen"
show-if-above
bordered
>
<q-list> <q-list>
<q-item-label <q-item-label header>
header
>
Essential Links Essential Links
</q-item-label> </q-item-label>
<EssentialLink <EssentialLink v-for="link in essentialLinks" :key="link.title" v-bind="link" />
v-for="link in essentialLinks"
:key="link.title"
v-bind="link"
/>
</q-list> </q-list>
</q-drawer> </q-drawer>
<q-page-container> <q-page-container>
<router-view /> <router-view />
</q-page-container> </q-page-container>
<q-footer bordered class="bg-grey-8 text-white flex justify-around">
<span class="hover:text-black cursor-pointer" @click="$router.push({ name: 'about' })">关于OA</span>
<span class="hover:text-black cursor-pointer">使用须知</span>
<span class="hover:text-black cursor-pointer" @click="util.goto('https://veypi.com')">
©2021 veypi
</span>
</q-footer>
</q-layout> </q-layout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue'; import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue';
import { util } from 'src/libs';
const essentialLinks: EssentialLinkProps[] = [ const essentialLinks: EssentialLinkProps[] = [
{ {
@ -62,37 +63,7 @@ const essentialLinks: EssentialLinkProps[] = [
icon: 'code', icon: 'code',
link: 'https://github.com/quasarframework' link: 'https://github.com/quasarframework'
}, },
{ ]
title: 'Discord Chat Channel',
caption: 'chat.quasar.dev',
icon: 'chat',
link: 'https://chat.quasar.dev'
},
{
title: 'Forum',
caption: 'forum.quasar.dev',
icon: 'record_voice_over',
link: 'https://forum.quasar.dev'
},
{
title: 'Twitter',
caption: '@quasarframework',
icon: 'rss_feed',
link: 'https://twitter.quasar.dev'
},
{
title: 'Facebook',
caption: '@QuasarFramework',
icon: 'public',
link: 'https://facebook.quasar.dev'
},
{
title: 'Quasar Awesome',
caption: 'Community Quasar projects',
icon: 'favorite',
link: 'https://awesome.quasar.dev'
}
];
const leftDrawerOpen = ref(false) const leftDrawerOpen = ref(false)

@ -0,0 +1,10 @@
/*
* @name: index
* @author: veypi <i@veypi.com>
* @date: 2021-11-17 22:22
* @descriptionindex
* @update: 2021-11-17 22:22
*/
import u from './util'
export const util = u

@ -0,0 +1,75 @@
import axios from 'axios'
function padLeftZero(str: string): string {
return ('00' + str).substr(str.length)
}
const util = {
goto(url: string) {
window.open(url, '_blank')
},
title: function (title: string) {
window.document.title = title ? title + ' - oa' : 'veypi project'
},
getCookie(name: string) {
const reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)')
const arr = document.cookie.match(reg)
if (arr) {
return unescape(arr[2])
} else return null
},
delCookie(name: string) {
const exp = new Date()
exp.setTime(exp.getTime() - 1)
const cval = this.getCookie(name)
if (cval !== null) {
document.cookie = name + '=' + cval + ';expires=' + exp.toLocaleString()
}
},
setCookie(name: string, value: string, time: number) {
const exp = new Date()
exp.setTime(exp.getTime() + time)
document.cookie =
name + '=' + escape(value) + ';expires=' + exp.toLocaleString()
},
getToken() {
return localStorage.auth_token
},
addTokenOf(url: string) {
return url + '?auth_token=' + encodeURIComponent(this.getToken())
},
checkLogin() {
// return parseInt(this.getCookie('stat')) === 1
return Boolean(localStorage.auth_token)
},
formatDate(date: Date, fmt: string) {
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
(date.getFullYear() + '').substr(4 - RegExp.$1.length),
)
}
const o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds(),
}
for (const k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
const str = o[k] + ''
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1 ? str : padLeftZero(str),
)
}
}
return fmt
},
}
export default util

@ -0,0 +1,7 @@
import { toBase64 } from "../tools/encode";
import { AuthHeader } from "../types";
export function generateBasicAuthHeader(username: string, password: string): AuthHeader {
const encoded = toBase64(`${username}:${password}`);
return `Basic ${encoded}`;
}

@ -0,0 +1,82 @@
import md5 from "md5";
import { ha1Compute } from "../tools/crypto";
import { DigestContext, Response } from "../types";
const NONCE_CHARS = "abcdef0123456789";
const NONCE_SIZE = 32;
export function createDigestContext(username: string, password: string): DigestContext {
return { username, password, nc: 0, algorithm: "md5", hasDigestAuth: false };
}
export function generateDigestAuthHeader(options, digest: DigestContext): string {
const url = options.url.replace("//", "");
const uri = url.indexOf("/") == -1 ? "/" : url.slice(url.indexOf("/"));
const method = options.method ? options.method.toUpperCase() : "GET";
const qop = /(^|,)\s*auth\s*($|,)/.test(digest.qop) ? "auth" : false;
const ncString = `00000000${digest.nc}`.slice(-8);
const ha1 = ha1Compute(
digest.algorithm,
digest.username,
digest.realm,
digest.password,
digest.nonce,
digest.cnonce
);
const ha2 = md5(`${method}:${uri}`);
const digestResponse = qop
? md5(`${ha1}:${digest.nonce}:${ncString}:${digest.cnonce}:${qop}:${ha2}`)
: md5(`${ha1}:${digest.nonce}:${ha2}`);
const authValues = {
username: digest.username,
realm: digest.realm,
nonce: digest.nonce,
uri,
qop,
response: digestResponse,
nc: ncString,
cnonce: digest.cnonce,
algorithm: digest.algorithm,
opaque: digest.opaque
};
const authHeader = [];
for (const k in authValues) {
if (authValues[k]) {
if (k === "qop" || k === "nc" || k === "algorithm") {
authHeader.push(`${k}=${authValues[k]}`);
} else {
authHeader.push(`${k}="${authValues[k]}"`);
}
}
}
return `Digest ${authHeader.join(", ")}`;
}
function makeNonce(): string {
let uid = "";
for (let i = 0; i < NONCE_SIZE; ++i) {
uid = `${uid}${NONCE_CHARS[Math.floor(Math.random() * NONCE_CHARS.length)]}`;
}
return uid;
}
export function parseDigestAuth(response: Response, _digest: DigestContext): boolean {
const authHeader = response.headers["www-authenticate"] || "";
if (authHeader.split(/\s/)[0].toLowerCase() !== "digest") {
return false;
}
const re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi;
for (;;) {
const match = re.exec(authHeader);
if (!match) {
break;
}
_digest[match[1]] = match[2] || match[3];
}
_digest.nc += 1;
_digest.cnonce = makeNonce();
return true;
}

@ -0,0 +1,36 @@
import { Layerr } from "layerr";
import { createDigestContext } from "./digest";
import { generateBasicAuthHeader } from "./basic";
import { generateTokenAuthHeader } from "./oauth";
import { AuthType, ErrorCode, OAuthToken, WebDAVClientContext } from "../types";
export function setupAuth(
context: WebDAVClientContext,
username: string,
password: string,
oauthToken: OAuthToken
): void {
switch (context.authType) {
case AuthType.Digest:
context.digest = createDigestContext(username, password);
break;
case AuthType.None:
// Do nothing
break;
case AuthType.Password:
context.headers.Authorization = generateBasicAuthHeader(username, password);
break;
case AuthType.Token:
context.headers.Authorization = generateTokenAuthHeader(oauthToken);
break;
default:
throw new Layerr(
{
info: {
code: ErrorCode.InvalidAuthType
}
},
`Invalid auth type: ${context.authType}`
);
}
}

@ -0,0 +1,5 @@
import { AuthHeader, OAuthToken } from "../types";
export function generateTokenAuthHeader(token: OAuthToken): AuthHeader {
return `${token.token_type} ${token.access_token}`;
}

@ -0,0 +1,10 @@
const hasArrayBuffer = typeof ArrayBuffer === "function";
const { toString: objToString } = Object.prototype;
// Taken from: https://github.com/fengyuanchen/is-array-buffer/blob/master/src/index.js
export function isArrayBuffer(value: any): boolean {
return (
hasArrayBuffer &&
(value instanceof ArrayBuffer || objToString.call(value) === "[object ArrayBuffer]")
);
}

@ -0,0 +1,8 @@
export function isBuffer(value: any): boolean {
return (
value != null &&
value.constructor != null &&
typeof value.constructor.isBuffer === "function" &&
value.constructor.isBuffer(value)
);
}

@ -0,0 +1,10 @@
import HotPatcher from "hot-patcher";
let __patcher: HotPatcher = null;
export function getPatcher(): HotPatcher {
if (!__patcher) {
__patcher = new HotPatcher();
}
return __patcher;
}

@ -0,0 +1,114 @@
import Stream from "stream";
import { extractURLPath } from "./tools/url";
import { setupAuth } from "./auth/index";
import { copyFile } from "./operations/copyFile";
import { createDirectory } from "./operations/createDirectory";
import { createReadStream, createWriteStream } from "./operations/createStream";
import { customRequest } from "./operations/customRequest";
import { deleteFile } from "./operations/deleteFile";
import { exists } from "./operations/exists";
import { getDirectoryContents } from "./operations/directoryContents";
import { getFileContents, getFileDownloadLink } from "./operations/getFileContents";
import { lock, unlock } from "./operations/lock";
import { getQuota } from "./operations/getQuota";
import { getStat } from "./operations/stat";
import { moveFile } from "./operations/moveFile";
import { getFileUploadLink, putFileContents } from "./operations/putFileContents";
import {
AuthType,
BufferLike,
CreateReadStreamOptions,
CreateWriteStreamCallback,
CreateWriteStreamOptions,
GetDirectoryContentsOptions,
GetFileContentsOptions,
GetQuotaOptions,
Headers,
LockOptions,
PutFileContentsOptions,
RequestOptionsCustom,
StatOptions,
WebDAVClient,
WebDAVClientContext,
WebDAVClientOptions,
WebDAVMethodOptions
} from "./types";
const DEFAULT_CONTACT_HREF =
"https://github.com/perry-mitchell/webdav-client/blob/master/LOCK_CONTACT.md";
export function createClient(remoteURL: string, options: WebDAVClientOptions = {}): WebDAVClient {
const {
authType: authTypeRaw = null,
contactHref = DEFAULT_CONTACT_HREF,
headers = {},
httpAgent,
httpsAgent,
maxBodyLength,
maxContentLength,
password,
token,
username,
withCredentials
} = options;
let authType = authTypeRaw;
if (!authType) {
authType = username || password ? AuthType.Password : AuthType.None;
}
const context: WebDAVClientContext = {
authType,
contactHref,
headers: Object.assign({}, headers),
httpAgent,
httpsAgent,
maxBodyLength,
maxContentLength,
remotePath: extractURLPath(remoteURL),
remoteURL,
password,
token,
username,
withCredentials
};
setupAuth(context, username, password, token);
return {
copyFile: (filename: string, destination: string, options?: WebDAVMethodOptions) =>
copyFile(context, filename, destination, options),
createDirectory: (path: string, options?: WebDAVMethodOptions) =>
createDirectory(context, path, options),
createReadStream: (filename: string, options?: CreateReadStreamOptions) =>
createReadStream(context, filename, options),
createWriteStream: (
filename: string,
options?: CreateWriteStreamOptions,
callback?: CreateWriteStreamCallback
) => createWriteStream(context, filename, options, callback),
customRequest: (path: string, requestOptions: RequestOptionsCustom) =>
customRequest(context, path, requestOptions),
deleteFile: (filename: string, options?: WebDAVMethodOptions) =>
deleteFile(context, filename, options),
exists: (path: string, options?: WebDAVMethodOptions) => exists(context, path, options),
getDirectoryContents: (path: string, options?: GetDirectoryContentsOptions) =>
getDirectoryContents(context, path, options),
getFileContents: (filename: string, options?: GetFileContentsOptions) =>
getFileContents(context, filename, options),
getFileDownloadLink: (filename: string) => getFileDownloadLink(context, filename),
getFileUploadLink: (filename: string) => getFileUploadLink(context, filename),
getHeaders: () => Object.assign({}, context.headers),
getQuota: (options?: GetQuotaOptions) => getQuota(context, options),
lock: (path: string, options?: LockOptions) => lock(context, path, options),
moveFile: (filename: string, destinationFilename: string, options?: WebDAVMethodOptions) =>
moveFile(context, filename, destinationFilename, options),
putFileContents: (
filename: string,
data: string | BufferLike | Stream.Readable,
options?: PutFileContentsOptions
) => putFileContents(context, filename, data, options),
setHeaders: (headers: Headers) => {
context.headers = Object.assign({}, headers);
},
stat: (path: string, options?: StatOptions) => getStat(context, path, options),
unlock: (path: string, token: string, options?: WebDAVMethodOptions) =>
unlock(context, path, token, options)
};
}

@ -0,0 +1,5 @@
export { createClient } from "./factory";
export { getPatcher } from "./compat/patcher";
export * from "./types";
export { parseStat, parseXML } from "./tools/dav";

@ -0,0 +1,26 @@
import { joinURL } from "../tools/url";
import { encodePath } from "../tools/path";
import { request, prepareRequestOptions } from "../request";
import { handleResponseCode } from "../response";
import { WebDAVClientContext, WebDAVMethodOptions } from "../types";
export async function copyFile(
context: WebDAVClientContext,
filename: string,
destination: string,
options: WebDAVMethodOptions = {}
): Promise<void> {
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(filename)),
method: "COPY",
headers: {
Destination: joinURL(context.remoteURL, encodePath(destination))
}
},
context,
options
);
const response = await request(requestOptions);
handleResponseCode(context, response);
}

@ -0,0 +1,81 @@
import { joinURL } from "../tools/url";
import { encodePath, getAllDirectories, normalisePath } from "../tools/path";
import { request, prepareRequestOptions } from "../request";
import { handleResponseCode } from "../response";
import { getStat } from "./stat";
import { CreateDirectoryOptions, FileStat, WebDAVClientContext, WebDAVClientError } from "../types";
export async function createDirectory(
context: WebDAVClientContext,
dirPath: string,
options: CreateDirectoryOptions = {}
): Promise<void> {
if (options.recursive === true) return createDirectoryRecursively(context, dirPath, options);
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, ensureCollectionPath(encodePath(dirPath))),
method: "MKCOL"
},
context,
options
);
const response = await request(requestOptions);
handleResponseCode(context, response);
}
/**
* Ensure the path is a proper "collection" path by ensuring it has a trailing "/".
* The proper format of collection according to the specification does contain the trailing slash.
* http://www.webdav.org/specs/rfc4918.html#rfc.section.5.2
* @param path Path of the collection
* @return string Path of the collection with appended trailing "/" in case the `path` does not have it.
*/
function ensureCollectionPath(path: string): string {
if (!path.endsWith("/")) {
return path + "/";
}
return path;
}
async function createDirectoryRecursively(
context: WebDAVClientContext,
dirPath: string,
options: CreateDirectoryOptions = {}
): Promise<void> {
const paths = getAllDirectories(normalisePath(dirPath));
paths.sort((a, b) => {
if (a.length > b.length) {
return 1;
} else if (b.length > a.length) {
return -1;
}
return 0;
});
let creating: boolean = false;
for (const testPath of paths) {
if (creating) {
await createDirectory(context, testPath, {
...options,
recursive: false
});
continue;
}
try {
const testStat = (await getStat(context, testPath)) as FileStat;
if (testStat.type !== "directory") {
throw new Error(`Path includes a file: ${dirPath}`);
}
} catch (err) {
const error = err as WebDAVClientError;
if (error.status === 404) {
creating = true;
await createDirectory(context, testPath, {
...options,
recursive: false
});
} else {
throw err;
}
}
}
}

@ -0,0 +1,109 @@
import Stream from "stream";
import { joinURL } from "../tools/url";
import { encodePath } from "../tools/path";
import { request, prepareRequestOptions } from "../request";
import { handleResponseCode } from "../response";
import {
CreateReadStreamOptions,
CreateWriteStreamCallback,
CreateWriteStreamOptions,
Headers,
WebDAVClientContext,
WebDAVClientError
} from "../types";
const NOOP = () => {};
export function createReadStream(
context: WebDAVClientContext,
filePath: string,
options: CreateReadStreamOptions = {}
): Stream.Readable {
const PassThroughStream = Stream.PassThrough;
const outStream = new PassThroughStream();
getFileStream(context, filePath, options)
.then(stream => {
stream.pipe(outStream);
})
.catch(err => {
outStream.emit("error", err);
});
return outStream;
}
export function createWriteStream(
context: WebDAVClientContext,
filePath: string,
options: CreateWriteStreamOptions = {},
callback: CreateWriteStreamCallback = NOOP
): Stream.Writable {
const PassThroughStream = Stream.PassThrough;
const writeStream = new PassThroughStream();
const headers = {};
if (options.overwrite === false) {
headers["If-None-Match"] = "*";
}
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(filePath)),
method: "PUT",
headers,
data: writeStream,
maxRedirects: 0
},
context,
options
);
request(requestOptions)
.then(response => handleResponseCode(context, response))
.then(response => {
// Fire callback asynchronously to avoid errors
setTimeout(() => {
callback(response);
}, 0);
})
.catch(err => {
writeStream.emit("error", err);
});
return writeStream;
}
async function getFileStream(
context: WebDAVClientContext,
filePath: string,
options: CreateReadStreamOptions = {}
): Promise<Stream.Readable> {
const headers: Headers = {};
if (typeof options.range === "object" && typeof options.range.start === "number") {
let rangeHeader = `bytes=${options.range.start}-`;
if (typeof options.range.end === "number") {
rangeHeader = `${rangeHeader}${options.range.end}`;
}
headers.Range = rangeHeader;
}
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(filePath)),
method: "GET",
headers,
responseType: "stream"
},
context,
options
);
const response = await request(requestOptions);
handleResponseCode(context, response);
if (headers.Range && response.status !== 206) {
const responseError: WebDAVClientError = new Error(
`Invalid response code for partial request: ${response.status}`
);
responseError.status = response.status;
throw responseError;
}
if (options.callback) {
setTimeout(() => {
options.callback(response);
}, 0);
}
return response.data as Stream.Readable;
}

@ -0,0 +1,19 @@
import { joinURL } from "../tools/url";
import { encodePath } from "../tools/path";
import { request, prepareRequestOptions } from "../request";
import { handleResponseCode } from "../response";
import { RequestOptionsCustom, Response, WebDAVClientContext } from "../types";
export async function customRequest(
context: WebDAVClientContext,
remotePath: string,
requestOptions: RequestOptionsCustom
): Promise<Response> {
if (!requestOptions.url) {
requestOptions.url = joinURL(context.remoteURL, encodePath(remotePath));
}
const finalOptions = prepareRequestOptions(requestOptions, context, {});
const response = await request(finalOptions);
handleResponseCode(context, response);
return response;
}

@ -0,0 +1,22 @@
import { joinURL } from "../tools/url";
import { encodePath } from "../tools/path";
import { request, prepareRequestOptions } from "../request";
import { handleResponseCode } from "../response";
import { WebDAVClientContext, WebDAVMethodOptions } from "../types";
export async function deleteFile(
context: WebDAVClientContext,
filename: string,
options: WebDAVMethodOptions = {}
): Promise<void> {
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(filename)),
method: "DELETE"
},
context,
options
);
const response = await request(requestOptions);
handleResponseCode(context, response);
}

@ -0,0 +1,78 @@
import pathPosix from "path-posix";
import { joinURL, normaliseHREF } from "../tools/url";
import { encodePath, normalisePath } from "../tools/path";
import { parseXML, prepareFileFromProps } from "../tools/dav";
import { request, prepareRequestOptions } from "../request";
import { handleResponseCode, processGlobFilter, processResponsePayload } from "../response";
import {
DAVResult,
FileStat,
GetDirectoryContentsOptions,
ResponseDataDetailed,
WebDAVClientContext
} from "../types";
export async function getDirectoryContents(
context: WebDAVClientContext,
remotePath: string,
options: GetDirectoryContentsOptions = {}
): Promise<Array<FileStat> | ResponseDataDetailed<Array<FileStat>>> {
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(remotePath), "/"),
method: "PROPFIND",
headers: {
Accept: "text/plain",
Depth: options.deep ? "infinity" : "1"
},
responseType: "text"
},
context,
options
);
const response = await request(requestOptions);
handleResponseCode(context, response);
const davResp = await parseXML(response.data as string);
let files = getDirectoryFiles(davResp, context.remotePath, remotePath, options.details);
if (options.glob) {
files = processGlobFilter(files, options.glob);
}
return processResponsePayload(response, files, options.details);
}
function getDirectoryFiles(
result: DAVResult,
serverBasePath: string,
requestPath: string,
isDetailed: boolean = false
): Array<FileStat> {
const serverBase = pathPosix.join(serverBasePath, "/");
// Extract the response items (directory contents)
const {
multistatus: { response: responseItems }
} = result;
return (
responseItems
// Map all items to a consistent output structure (results)
.map(item => {
// HREF is the file path (in full)
const href = normaliseHREF(item.href);
// Each item should contain a stat object
const {
propstat: { prop: props }
} = item;
// Process the true full filename (minus the base server path)
const filename =
serverBase === "/"
? decodeURIComponent(normalisePath(href))
: decodeURIComponent(normalisePath(pathPosix.relative(serverBase, href)));
return prepareFileFromProps(props, filename, isDetailed);
})
// Filter out the item pointing to the current directory (not needed)
.filter(
item =>
item.basename &&
(item.type === "file" || item.filename !== requestPath.replace(/\/$/, ""))
)
);
}

@ -0,0 +1,18 @@
import { getStat } from "./stat";
import { WebDAVClientContext, WebDAVMethodOptions } from "../types";
export async function exists(
context: WebDAVClientContext,
remotePath: string,
options: WebDAVMethodOptions = {}
): Promise<boolean> {
try {
await getStat(context, remotePath, options);
return true;
} catch (err) {
if (err.status === 404) {
return false;
}
throw err;
}
}

@ -0,0 +1,102 @@
import { Layerr } from "layerr";
import { joinURL } from "../tools/url";
import { encodePath } from "../tools/path";
import { fromBase64 } from "../tools/encode";
import { request, prepareRequestOptions } from "../request";
import { handleResponseCode, processResponsePayload } from "../response";
import {
AuthType,
BufferLike,
ErrorCode,
GetFileContentsOptions,
ResponseDataDetailed,
WebDAVClientContext
} from "../types";
const TRANSFORM_RETAIN_FORMAT = (v: any) => v;
export async function getFileContents(
context: WebDAVClientContext,
filePath: string,
options: GetFileContentsOptions = {}
): Promise<BufferLike | string | ResponseDataDetailed<BufferLike | string>> {
const { format = "binary" } = options;
if (format !== "binary" && format !== "text") {
throw new Layerr(
{
info: {
code: ErrorCode.InvalidOutputFormat
}
},
`Invalid output format: ${format}`
);
}
return format === "text"
? getFileContentsString(context, filePath, options)
: getFileContentsBuffer(context, filePath, options);
}
async function getFileContentsBuffer(
context: WebDAVClientContext,
filePath: string,
options: GetFileContentsOptions = {}
): Promise<BufferLike | ResponseDataDetailed<BufferLike>> {
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(filePath)),
method: "GET",
responseType: "arraybuffer"
},
context,
options
);
const response = await request(requestOptions);
handleResponseCode(context, response);
return processResponsePayload(response, response.data as BufferLike, options.details);
}
async function getFileContentsString(
context: WebDAVClientContext,
filePath: string,
options: GetFileContentsOptions = {}
): Promise<string | ResponseDataDetailed<string>> {
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(filePath)),
method: "GET",
responseType: "text",
transformResponse: [TRANSFORM_RETAIN_FORMAT]
},
context,
options
);
const response = await request(requestOptions);
handleResponseCode(context, response);
return processResponsePayload(response, response.data as string, options.details);
}
export function getFileDownloadLink(context: WebDAVClientContext, filePath: string): string {
let url = joinURL(context.remoteURL, encodePath(filePath));
const protocol = /^https:/i.test(url) ? "https" : "http";
switch (context.authType) {
case AuthType.None:
// Do nothing
break;
case AuthType.Password: {
const authPart = context.headers.Authorization.replace(/^Basic /i, "").trim();
const authContents = fromBase64(authPart);
url = url.replace(/^https?:\/\//, `${protocol}://${authContents}@`);
break;
}
default:
throw new Layerr(
{
info: {
code: ErrorCode.LinkUnsupportedAuthType
}
},
`Unsupported auth type for file link: ${context.authType}`
);
}
return url;
}

@ -0,0 +1,30 @@
import { prepareRequestOptions, request } from "../request";
import { handleResponseCode, processResponsePayload } from "../response";
import { parseXML } from "../tools/dav";
import { joinURL } from "../tools/url";
import { parseQuota } from "../tools/quota";
import { DiskQuota, GetQuotaOptions, ResponseDataDetailed, WebDAVClientContext } from "../types";
export async function getQuota(
context: WebDAVClientContext,
options: GetQuotaOptions = {}
): Promise<DiskQuota | null | ResponseDataDetailed<DiskQuota | null>> {
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, "/"),
method: "PROPFIND",
headers: {
Accept: "text/plain",
Depth: "0"
},
responseType: "text"
},
context,
options
);
const response = await request(requestOptions);
handleResponseCode(context, response);
const result = await parseXML(response.data as string);
const quota = parseQuota(result);
return processResponsePayload(response, quota, options.details);
}

@ -0,0 +1,79 @@
import nestedProp from "nested-property";
import { joinURL } from "../tools/url";
import { encodePath } from "../tools/path";
import { generateLockXML, parseGenericResponse } from "../tools/xml";
import { request, prepareRequestOptions } from "../request";
import { createErrorFromResponse, handleResponseCode } from "../response";
import {
Headers,
LockOptions,
LockResponse,
WebDAVClientContext,
WebDAVMethodOptions
} from "../types";
const DEFAULT_TIMEOUT = "Infinite, Second-4100000000";
export async function lock(
context: WebDAVClientContext,
path: string,
options: LockOptions = {}
): Promise<LockResponse> {
const { refreshToken, timeout = DEFAULT_TIMEOUT } = options;
const headers: Headers = {
Accept: "text/plain,application/xml",
Timeout: timeout
};
if (refreshToken) {
headers.If = refreshToken;
}
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(path)),
method: "LOCK",
headers,
data: generateLockXML(context.contactHref),
responseType: "text"
},
context,
options
);
const response = await request(requestOptions);
handleResponseCode(context, response);
const lockPayload = parseGenericResponse(response.data as string);
const token = nestedProp.get(lockPayload, "prop.lockdiscovery.activelock.locktoken.href");
const serverTimeout = nestedProp.get(lockPayload, "prop.lockdiscovery.activelock.timeout");
if (!token) {
const err = createErrorFromResponse(response, "No lock token received: ");
throw err;
}
return {
token,
serverTimeout
};
}
export async function unlock(
context: WebDAVClientContext,
path: string,
token: string,
options: WebDAVMethodOptions = {}
): Promise<void> {
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(path)),
method: "UNLOCK",
headers: {
"Lock-Token": token
}
},
context,
options
);
const response = await request(requestOptions);
handleResponseCode(context, response);
if (response.status !== 204 && response.status !== 200) {
const err = createErrorFromResponse(response);
throw err;
}
}

@ -0,0 +1,26 @@
import { joinURL } from "../tools/url";
import { encodePath } from "../tools/path";
import { request, prepareRequestOptions } from "../request";
import { handleResponseCode } from "../response";
import { WebDAVClientContext, WebDAVMethodOptions } from "../types";
export async function moveFile(
context: WebDAVClientContext,
filename: string,
destination: string,
options: WebDAVMethodOptions = {}
): Promise<void> {
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(filename)),
method: "MOVE",
headers: {
Destination: joinURL(context.remoteURL, encodePath(destination))
}
},
context,
options
);
const response = await request(requestOptions);
handleResponseCode(context, response);
}

@ -0,0 +1,94 @@
import { Layerr } from "layerr";
import Stream from "stream";
import { fromBase64 } from "../tools/encode";
import { joinURL } from "../tools/url";
import { encodePath } from "../tools/path";
import { request, prepareRequestOptions } from "../request";
import { handleResponseCode } from "../response";
import { calculateDataLength } from "../tools/size";
import {
AuthType,
BufferLike,
ErrorCode,
Headers,
PutFileContentsOptions,
WebDAVClientContext,
WebDAVClientError
} from "../types";
declare var WEB: boolean;
export async function putFileContents(
context: WebDAVClientContext,
filePath: string,
data: string | BufferLike | Stream.Readable,
options: PutFileContentsOptions = {}
): Promise<boolean> {
const { contentLength = true, overwrite = true } = options;
const headers: Headers = {
"Content-Type": "application/octet-stream"
};
if (typeof WEB === "undefined") {
// Skip, no content-length
} else if (contentLength === false) {
// Skip, disabled
} else if (typeof contentLength === "number") {
headers["Content-Length"] = `${contentLength}`;
} else {
headers["Content-Length"] = `${calculateDataLength(data as string | BufferLike)}`;
}
if (!overwrite) {
headers["If-None-Match"] = "*";
}
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(filePath)),
method: "PUT",
headers,
data
},
context,
options
);
const response = await request(requestOptions);
try {
handleResponseCode(context, response);
} catch (err) {
const error = err as WebDAVClientError;
if (error.status === 412 && !overwrite) {
return false;
} else {
throw error;
}
}
return true;
}
export function getFileUploadLink(context: WebDAVClientContext, filePath: string): string {
let url: string = `${joinURL(
context.remoteURL,
encodePath(filePath)
)}?Content-Type=application/octet-stream`;
const protocol = /^https:/i.test(url) ? "https" : "http";
switch (context.authType) {
case AuthType.None:
// Do nothing
break;
case AuthType.Password: {
const authPart = context.headers.Authorization.replace(/^Basic /i, "").trim();
const authContents = fromBase64(authPart);
url = url.replace(/^https?:\/\//, `${protocol}://${authContents}@`);
break;
}
default:
throw new Layerr(
{
info: {
code: ErrorCode.LinkUnsupportedAuthType
}
},
`Unsupported auth type for file link: ${context.authType}`
);
}
return url;
}

@ -0,0 +1,32 @@
import { parseStat, parseXML } from "../tools/dav";
import { joinURL } from "../tools/url";
import { encodePath } from "../tools/path";
import { request, prepareRequestOptions } from "../request";
import { handleResponseCode, processResponsePayload } from "../response";
import { FileStat, ResponseDataDetailed, StatOptions, WebDAVClientContext } from "../types";
export async function getStat(
context: WebDAVClientContext,
filename: string,
options: StatOptions = {}
): Promise<FileStat | ResponseDataDetailed<FileStat>> {
const { details: isDetailed = false } = options;
const requestOptions = prepareRequestOptions(
{
url: joinURL(context.remoteURL, encodePath(filename)),
method: "PROPFIND",
headers: {
Accept: "text/plain,application/xml",
Depth: "0"
},
responseType: "text"
},
context,
options
);
const response = await request(requestOptions);
handleResponseCode(context, response);
const result = await parseXML(response.data as string);
const stat = parseStat(result, filename, isDetailed);
return processResponsePayload(response, stat, isDetailed);
}

@ -0,0 +1,108 @@
import axios from "axios";
import { getPatcher } from "./compat/patcher";
import { generateDigestAuthHeader, parseDigestAuth } from "./auth/digest";
import { cloneShallow, merge } from "./tools/merge";
import { mergeHeaders } from "./tools/headers";
import {
RequestOptionsCustom,
RequestOptionsWithState,
RequestOptions,
Response,
WebDAVClientContext,
WebDAVMethodOptions
} from "./types";
function _request(requestOptions: RequestOptions) {
return getPatcher().patchInline(
"request",
(options: RequestOptions) => axios(options as any),
requestOptions
);
}
export function prepareRequestOptions(
requestOptions: RequestOptionsCustom | RequestOptionsWithState,
context: WebDAVClientContext,
userOptions: WebDAVMethodOptions
): RequestOptionsWithState {
const finalOptions = cloneShallow(requestOptions) as RequestOptionsWithState;
finalOptions.headers = mergeHeaders(
context.headers,
finalOptions.headers || {},
userOptions.headers || {}
);
if (typeof userOptions.data !== "undefined") {
finalOptions.data = userOptions.data;
}
if (context.httpAgent) {
finalOptions.httpAgent = context.httpAgent;
}
if (context.httpsAgent) {
finalOptions.httpsAgent = context.httpsAgent;
}
if (context.digest) {
finalOptions._digest = context.digest;
}
if (typeof context.withCredentials === "boolean") {
finalOptions.withCredentials = context.withCredentials;
}
if (context.maxContentLength) {
finalOptions.maxContentLength = context.maxContentLength;
}
if (context.maxBodyLength) {
finalOptions.maxBodyLength = context.maxBodyLength;
}
if (userOptions.hasOwnProperty("onUploadProgress")) {
finalOptions.onUploadProgress = userOptions["onUploadProgress"];
}
// Take full control of all response status codes
finalOptions.validateStatus = () => true;
return finalOptions;
}
export function request(requestOptions: RequestOptionsWithState): Promise<Response> {
// Client not configured for digest authentication
if (!requestOptions._digest) {
return _request(requestOptions);
}
// Remove client's digest authentication object from request options
const _digest = requestOptions._digest;
delete requestOptions._digest;
// If client is already using digest authentication, include the digest authorization header
if (_digest.hasDigestAuth) {
requestOptions = merge(requestOptions, {
headers: {
Authorization: generateDigestAuthHeader(requestOptions, _digest)
}
});
}
// Perform the request and handle digest authentication
return _request(requestOptions).then(function(response: Response) {
if (response.status == 401) {
_digest.hasDigestAuth = parseDigestAuth(response, _digest);
if (_digest.hasDigestAuth) {
requestOptions = merge(requestOptions, {
headers: {
Authorization: generateDigestAuthHeader(requestOptions, _digest)
}
});
return _request(requestOptions).then(function(response2: Response) {
if (response2.status == 401) {
_digest.hasDigestAuth = false;
} else {
_digest.nc++;
}
return response2;
});
}
} else {
_digest.nc++;
}
return response;
});
}

@ -0,0 +1,46 @@
import minimatch from "minimatch";
import {
FileStat,
Response,
ResponseDataDetailed,
WebDAVClientContext,
WebDAVClientError
} from "./types";
export function createErrorFromResponse(response: Response, prefix: string = ""): Error {
const err: WebDAVClientError = new Error(
`${prefix}Invalid response: ${response.status} ${response.statusText}`
) as WebDAVClientError;
err.status = response.status;
err.response = response;
return err;
}
export function handleResponseCode(context: WebDAVClientContext, response: Response): Response {
const { status } = response;
if (status === 401 && context.digest) return response;
if (status >= 400) {
const err = createErrorFromResponse(response);
throw err;
}
return response;
}
export function processGlobFilter(files: Array<FileStat>, glob: string): Array<FileStat> {
return files.filter(file => minimatch(file.filename, glob, { matchBase: true }));
}
export function processResponsePayload<T>(
response: Response,
data: T,
isDetailed: boolean = false
): ResponseDataDetailed<T> | T {
return isDetailed
? {
data,
headers: response.headers || {},
status: response.status,
statusText: response.statusText
}
: data;
}

@ -0,0 +1,16 @@
import md5 from "md5";
export function ha1Compute(
algorithm: string,
user: string,
realm: string,
pass: string,
nonce: string,
cnonce: string
): string {
const ha1 = md5(`${user}:${realm}:${pass}`) as string;
if (algorithm && algorithm.toLowerCase() === "md5-sess") {
return md5(`${ha1}:${nonce}:${cnonce}`) as string;
}
return ha1;
}

@ -0,0 +1,171 @@
import path from "path-posix";
import xmlParser from "fast-xml-parser";
import nestedProp from "nested-property";
import { decodeHTMLEntities } from "./encode";
import { normalisePath } from "./path";
import {
DAVResult,
DAVResultRaw,
DAVResultResponse,
DAVResultResponseProps,
DiskQuotaAvailable,
FileStat,
WebDAVClientError
} from "../types";
enum PropertyType {
Array = "array",
Object = "object",
Original = "original"
}
function getPropertyOfType(
obj: Object,
prop: string,
type: PropertyType = PropertyType.Original
): any {
const val = nestedProp.get(obj, prop);
if (type === "array" && Array.isArray(val) === false) {
return [val];
} else if (type === "object" && Array.isArray(val)) {
return val[0];
}
return val;
}
function normaliseResponse(response: any): DAVResultResponse {
const output = Object.assign({}, response);
nestedProp.set(output, "propstat", getPropertyOfType(output, "propstat", PropertyType.Object));
nestedProp.set(
output,
"propstat.prop",
getPropertyOfType(output, "propstat.prop", PropertyType.Object)
);
return output;
}
function normaliseResult(result: DAVResultRaw): DAVResult {
const { multistatus } = result;
if (multistatus === "") {
return {
multistatus: {
response: []
}
};
}
if (!multistatus) {
throw new Error("Invalid response: No root multistatus found");
}
const output: any = {
multistatus: Array.isArray(multistatus) ? multistatus[0] : multistatus
};
nestedProp.set(
output,
"multistatus.response",
getPropertyOfType(output, "multistatus.response", PropertyType.Array)
);
nestedProp.set(
output,
"multistatus.response",
nestedProp.get(output, "multistatus.response").map(response => normaliseResponse(response))
);
return output as DAVResult;
}
export function parseXML(xml: string): Promise<DAVResult> {
return new Promise(resolve => {
const result = xmlParser.parse(xml, {
arrayMode: false,
ignoreNameSpace: true
// // We don't use the processors here as decoding is done manually
// // later on - decoding early would break some path checks.
// attrValueProcessor: val => decodeHTMLEntities(decodeURIComponent(val)),
// tagValueProcessor: val => decodeHTMLEntities(decodeURIComponent(val))
});
resolve(normaliseResult(result));
});
}
export function prepareFileFromProps(
props: DAVResultResponseProps,
rawFilename: string,
isDetailed: boolean = false
): FileStat {
// Last modified time, raw size, item type and mime
const {
getlastmodified: lastMod = null,
getcontentlength: rawSize = "0",
resourcetype: resourceType = null,
getcontenttype: mimeType = null,
getetag: etag = null
} = props;
const type =
resourceType &&
typeof resourceType === "object" &&
typeof resourceType.collection !== "undefined"
? "directory"
: "file";
const filename = decodeHTMLEntities(rawFilename);
const stat: FileStat = {
filename,
basename: path.basename(filename),
lastmod: lastMod,
size: parseInt(rawSize, 10),
type,
etag: typeof etag === "string" ? etag.replace(/"/g, "") : null
};
if (type === "file") {
stat.mime = mimeType && typeof mimeType === "string" ? mimeType.split(";")[0] : "";
}
if (isDetailed) {
stat.props = props;
}
return stat;
}
export function parseStat(
result: DAVResult,
filename: string,
isDetailed: boolean = false
): FileStat {
let responseItem: DAVResultResponse = null;
try {
responseItem = result.multistatus.response[0];
} catch (e) {
/* ignore */
}
if (!responseItem) {
throw new Error("Failed getting item stat: bad response");
}
const {
propstat: { prop: props, status: statusLine }
} = responseItem;
// As defined in https://tools.ietf.org/html/rfc2068#section-6.1
const [_, statusCodeStr, statusText] = statusLine.split(" ", 3);
const statusCode = parseInt(statusCodeStr, 10);
if (statusCode >= 400) {
const err: WebDAVClientError = new Error(
`Invalid response: ${statusCode} ${statusText}`
) as WebDAVClientError;
err.status = statusCode;
throw err;
}
const filePath = normalisePath(filename);
return prepareFileFromProps(props, filePath, isDetailed);
}
export function translateDiskSpace(value: string | number): DiskQuotaAvailable {
switch (value.toString()) {
case "-3":
return "unlimited";
case "-2":
/* falls-through */
case "-1":
// -1 is non-computed
return "unknown";
default:
return parseInt(value as string, 10);
}
}

@ -0,0 +1,24 @@
import { decode, encode } from "base-64";
declare var WEB: boolean;
export function decodeHTMLEntities(text: string): string {
if (typeof WEB === "undefined") {
// Node
const he = require("he");
return he.decode(text);
} else {
// Nasty browser way
const txt = document.createElement("textarea");
txt.innerHTML = text;
return txt.value;
}
}
export function fromBase64(text: string): string {
return decode(text);
}
export function toBase64(text: string): string {
return encode(text);
}

@ -0,0 +1,18 @@
import { Headers } from "../types";
export function mergeHeaders(...headerPayloads: Headers[]): Headers {
if (headerPayloads.length === 0) return {};
const headerKeys = {};
return headerPayloads.reduce((output: Headers, headers: Headers) => {
Object.keys(headers).forEach(header => {
const lowerHeader = header.toLowerCase();
if (headerKeys.hasOwnProperty(lowerHeader)) {
output[headerKeys[lowerHeader]] = headers[header];
} else {
headerKeys[lowerHeader] = header;
output[header] = headers[header];
}
});
return output;
}, {});
}

@ -0,0 +1,62 @@
export function cloneShallow<T extends Object>(obj: T): T {
return isPlainObject(obj)
? Object.assign({}, obj)
: Object.setPrototypeOf(Object.assign({}, obj), Object.getPrototypeOf(obj));
}
function isPlainObject(obj: Object | any): boolean {
if (
typeof obj !== "object" ||
obj === null ||
Object.prototype.toString.call(obj) != "[object Object]"
) {
// Not an object
return false;
}
if (Object.getPrototypeOf(obj) === null) {
return true;
}
let proto = obj;
// Find the prototype
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto);
}
return Object.getPrototypeOf(obj) === proto;
}
export function merge(...args: Object[]) {
let output = null,
items = [...args];
while (items.length > 0) {
const nextItem = items.shift();
if (!output) {
output = cloneShallow(nextItem);
} else {
output = mergeObjects(output, nextItem);
}
}
return output;
}
function mergeObjects(obj1: Object, obj2: Object): Object {
const output = cloneShallow(obj1);
Object.keys(obj2).forEach(key => {
if (!output.hasOwnProperty(key)) {
output[key] = obj2[key];
return;
}
if (Array.isArray(obj2[key])) {
output[key] = Array.isArray(output[key])
? [...output[key], ...obj2[key]]
: [...obj2[key]];
} else if (typeof obj2[key] === "object" && !!obj2[key]) {
output[key] =
typeof output[key] === "object" && !!output[key]
? mergeObjects(output[key], obj2[key])
: cloneShallow(obj2[key]);
} else {
output[key] = obj2[key];
}
});
return output;
}

@ -0,0 +1,36 @@
import { dirname } from "path-posix";
const SEP_PATH_POSIX = "__PATH_SEPARATOR_POSIX__";
const SEP_PATH_WINDOWS = "__PATH_SEPARATOR_WINDOWS__";
export function encodePath(path) {
const replaced = path.replace(/\//g, SEP_PATH_POSIX).replace(/\\\\/g, SEP_PATH_WINDOWS);
const formatted = encodeURIComponent(replaced);
return formatted
.split(SEP_PATH_WINDOWS)
.join("\\\\")
.split(SEP_PATH_POSIX)
.join("/");
}
export function getAllDirectories(path: string): Array<string> {
if (!path || path === "/") return [];
let currentPath = path;
const output: Array<string> = [];
do {
output.push(currentPath);
currentPath = dirname(currentPath);
} while (currentPath && currentPath !== "/");
return output;
}
export function normalisePath(pathStr: string): string {
let normalisedPath = pathStr;
if (normalisedPath[0] !== "/") {
normalisedPath = "/" + normalisedPath;
}
if (/^.+\/$/.test(normalisedPath)) {
normalisedPath = normalisedPath.substr(0, normalisedPath.length - 1);
}
return normalisedPath;
}

@ -0,0 +1,22 @@
import { translateDiskSpace } from "./dav";
import { DAVResult, DiskQuota } from "../types";
export function parseQuota(result: DAVResult): DiskQuota | null {
try {
const [responseItem] = result.multistatus.response;
const {
propstat: {
prop: { "quota-used-bytes": quotaUsed, "quota-available-bytes": quotaAvail }
}
} = responseItem;
return typeof quotaUsed !== "undefined" && typeof quotaAvail !== "undefined"
? {
used: parseInt(quotaUsed, 10),
available: translateDiskSpace(quotaAvail)
}
: null;
} catch (err) {
/* ignore */
}
return null;
}

@ -0,0 +1,22 @@
import { Layerr } from "layerr";
import { isArrayBuffer } from "../compat/arrayBuffer";
import { isBuffer } from "../compat/buffer";
import { BufferLike, ErrorCode } from "../types";
export function calculateDataLength(data: string | BufferLike): number {
if (isArrayBuffer(data)) {
return (<ArrayBuffer>data).byteLength;
} else if (isBuffer(data)) {
return (<Buffer>data).length;
} else if (typeof data === "string") {
return (<string>data).length;
}
throw new Layerr(
{
info: {
code: ErrorCode.DataTypeNoLength
}
},
"Cannot calculate data length: Invalid type"
);
}

@ -0,0 +1,32 @@
import URL from "url-parse";
import _joinURL from "url-join";
import { normalisePath } from "./path";
export function extractURLPath(fullURL: string): string {
const url = new URL(fullURL);
let urlPath = url.pathname;
if (urlPath.length <= 0) {
urlPath = "/";
}
return normalisePath(urlPath);
}
export function joinURL(...parts: Array<string>): string {
return _joinURL(
parts.reduce((output, nextPart, partIndex) => {
if (
partIndex === 0 ||
nextPart !== "/" ||
(nextPart === "/" && output[output.length - 1] !== "/")
) {
output.push(nextPart);
}
return output;
}, [])
);
}
export function normaliseHREF(href: string): string {
const normalisedHref = href.replace(/^https?:\/\/[^\/]+/, "");
return normalisedHref;
}

@ -0,0 +1,55 @@
import xmlParser, { j2xParser as XMLParser } from "fast-xml-parser";
export function generateLockXML(ownerHREF: string): string {
return getParser().parse(
namespace(
{
lockinfo: {
"@_xmlns:d": "DAV:",
lockscope: {
exclusive: {}
},
locktype: {
write: {}
},
owner: {
href: ownerHREF
}
}
},
"d"
)
);
}
function getParser(): XMLParser {
return new XMLParser({
attributeNamePrefix: "@_",
format: true,
ignoreAttributes: false,
supressEmptyNode: true
});
}
function namespace<T extends Object>(obj: T, ns: string): T {
const copy = { ...obj };
for (const key in copy) {
if (copy[key] && typeof copy[key] === "object" && key.indexOf(":") === -1) {
copy[`${ns}:${key}`] = namespace(copy[key], ns);
delete copy[key];
} else if (/^@_/.test(key) === false) {
copy[`${ns}:${key}`] = copy[key];
delete copy[key];
}
}
return copy;
}
export function parseGenericResponse(xml: string): Object {
return xmlParser.parse(xml, {
arrayMode: false,
ignoreNameSpace: true,
parseAttributeValue: true,
parseNodeValue: true
});
}

@ -0,0 +1,288 @@
import Stream from "stream";
export type AuthHeader = string;
export enum AuthType {
Digest = "digest",
None = "none",
Password = "password",
Token = "token"
}
export type BufferLike = Buffer | ArrayBuffer;
export interface CreateDirectoryOptions extends WebDAVMethodOptions {
recursive?: boolean;
}
export interface CreateReadStreamOptions extends WebDAVMethodOptions {
callback?: (response: Response) => void;
range?: {
start: number;
end?: number;
};
}
export type CreateWriteStreamCallback = (response: Response) => void;
export interface CreateWriteStreamOptions extends WebDAVMethodOptions {
overwrite?: boolean;
}
export interface DAVResultResponse {
href: string;
propstat: {
prop: DAVResultResponseProps;
status: string;
};
}
export interface DAVResultResponseProps {
displayname: string;
resourcetype: {
collection?: boolean;
};
getlastmodified?: string;
getetag?: string;
getcontentlength?: string;
getcontenttype?: string;
"quota-available-bytes"?: any;
"quota-used-bytes"?: string;
}
export interface DAVResult {
multistatus: {
response: Array<DAVResultResponse>;
};
}
export interface DAVResultRawMultistatus {
response: DAVResultResponse | [DAVResultResponse];
}
export interface DAVResultRaw {
multistatus: "" | DAVResultRawMultistatus | [DAVResultRawMultistatus];
}
export interface DigestContext {
username: string;
password: string;
nc: number;
algorithm: string;
hasDigestAuth: boolean;
cnonce?: string;
nonce?: string;
realm?: string;
qop?: string;
opaque?: string;
}
export interface DiskQuota {
used: number;
available: DiskQuotaAvailable;
}
export type DiskQuotaAvailable = "unknown" | "unlimited" | number;
export enum ErrorCode {
DataTypeNoLength = "data-type-no-length",
InvalidAuthType = "invalid-auth-type",
InvalidOutputFormat = "invalid-output-format",
LinkUnsupportedAuthType = "link-unsupported-auth"
}
export interface FileStat {
filename: string;
basename: string;
lastmod: string;
size: number;
type: "file" | "directory";
etag: string | null;
mime?: string;
props?: DAVResultResponseProps;
}
export interface GetDirectoryContentsOptions extends WebDAVMethodOptions {
deep?: boolean;
details?: boolean;
glob?: string;
}
export interface GetFileContentsOptions extends WebDAVMethodOptions {
details?: boolean;
format?: "binary" | "text";
}
export interface GetQuotaOptions extends WebDAVMethodOptions {
details?: boolean;
}
export interface Headers {
[key: string]: string;
}
export interface LockOptions extends WebDAVMethodOptions {
refreshToken?: string;
timeout?: string;
}
export interface LockResponse {
serverTimeout: string;
token: string;
}
export interface OAuthToken {
access_token: string;
token_type: string;
refresh_token?: string;
}
export interface PutFileContentsOptions extends WebDAVMethodOptions {
contentLength?: boolean | number;
overwrite?: boolean;
onUploadProgress?: UploadProgressCallback;
}
export type RequestDataPayload = string | Buffer | ArrayBuffer | { [key: string]: any };
interface RequestOptionsBase {
data?: RequestDataPayload;
headers?: Headers;
httpAgent?: any;
httpsAgent?: any;
maxBodyLength?: number;
maxContentLength?: number;
maxRedirects?: number;
method: string;
onUploadProgress?: UploadProgressCallback;
responseType?: string;
transformResponse?: Array<(value: any) => any>;
url?: string;
validateStatus?: (status: number) => boolean;
withCredentials?: boolean;
}
export interface RequestOptionsCustom extends RequestOptionsBase {}
export interface RequestOptions extends RequestOptionsBase {
url: string;
}
export interface RequestOptionsWithState extends RequestOptions {
_digest?: DigestContext;
}
export interface Response {
data: ResponseData;
status: number;
headers: Headers;
statusText: string;
}
export type ResponseData = string | Buffer | ArrayBuffer | Object | Array<any>;
export interface ResponseDataDetailed<T> {
data: T;
headers: Headers;
status: number;
statusText: string;
}
export interface ResponseStatusValidator {
(status: number): boolean;
}
export interface StatOptions extends WebDAVMethodOptions {
details?: boolean;
}
export interface UploadProgress {
loaded: number;
total: number;
}
export interface UploadProgressCallback {
(progress: UploadProgress): void;
}
export interface WebDAVClient {
copyFile: (filename: string, destination: string) => Promise<void>;
createDirectory: (path: string, options?: CreateDirectoryOptions) => Promise<void>;
createReadStream: (filename: string, options?: CreateReadStreamOptions) => Stream.Readable;
createWriteStream: (
filename: string,
options?: CreateWriteStreamOptions,
callback?: CreateWriteStreamCallback
) => Stream.Writable;
customRequest: (path: string, requestOptions: RequestOptionsCustom) => Promise<Response>;
deleteFile: (filename: string) => Promise<void>;
exists: (path: string) => Promise<boolean>;
getDirectoryContents: (
path: string,
options?: GetDirectoryContentsOptions
) => Promise<Array<FileStat> | ResponseDataDetailed<Array<FileStat>>>;
getFileContents: (
filename: string,
options?: GetFileContentsOptions
) => Promise<BufferLike | string | ResponseDataDetailed<BufferLike | string>>;
getFileDownloadLink: (filename: string) => string;
getFileUploadLink: (filename: string) => string;
getHeaders: () => Headers;
getQuota: (
options?: GetQuotaOptions
) => Promise<DiskQuota | null | ResponseDataDetailed<DiskQuota | null>>;
lock: (path: string, options?: LockOptions) => Promise<LockResponse>;
moveFile: (filename: string, destinationFilename: string) => Promise<void>;
putFileContents: (
filename: string,
data: string | BufferLike | Stream.Readable,
options?: PutFileContentsOptions
) => Promise<boolean>;
setHeaders: (headers: Headers) => void;
stat: (
path: string,
options?: StatOptions
) => Promise<FileStat | ResponseDataDetailed<FileStat>>;
unlock: (path: string, token: string, options?: WebDAVMethodOptions) => Promise<void>;
}
export interface WebDAVClientContext {
authType: AuthType;
contactHref: string;
digest?: DigestContext;
headers: Headers;
httpAgent?: any;
httpsAgent?: any;
maxBodyLength?: number;
maxContentLength?: number;
password?: string;
remotePath: string;
remoteURL: string;
token?: OAuthToken;
username?: string;
withCredentials?: boolean;
}
export interface WebDAVClientError extends Error {
status?: number;
response?: Response;
}
export interface WebDAVClientOptions {
authType?: AuthType;
contactHref?: string;
headers?: Headers;
httpAgent?: any;
httpsAgent?: any;
maxBodyLength?: number;
maxContentLength?: number;
password?: string;
token?: OAuthToken;
username?: string;
withCredentials?: boolean;
}
export interface WebDAVMethodOptions {
data?: RequestDataPayload;
headers?: Headers;
}

@ -0,0 +1,2 @@
!function(a,b,c){function d(c){var d=b.createElement("iframe"),e="https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid="+c.appid+"&agentid="+c.agentid+"&redirect_uri="+c.redirect_uri+"&state="+c.state+"&login_type=jssdk";e+=c.style?"&style="+c.style:"",e+=c.href?"&href="+c.href:"",d.src=e,d.frameBorder="0",d.allowTransparency="true",d.scrolling="no",d.width="300px",d.height="400px";var f=b.getElementById(c.id);f.innerHTML="",f.appendChild(d),d.onload=function(){d.contentWindow.postMessage&&a.addEventListener&&(a.addEventListener("message",function(b){
b.data&&b.origin.indexOf("work.weixin.qq.com")>-1&&(a.location.href=b.data)}),d.contentWindow.postMessage("ask_usePostMessage","*"))}}a.WwLogin=d}(window,document);

@ -0,0 +1,86 @@
export interface modelsSimpleAuth {
level: number
name: string
rid: string
// RID: string
// RUID: string
}
export const R = {
// 应用管理配置权限
App: 'app',
// 用户管理和绑定应用权限
User: 'user',
// 权限资源定义权限
Resource: 'resource',
// 角色管理和绑定用户权限
Role: 'role',
// 权限管理和绑定角色权限
Auth: 'auth',
}
const level = {
None: 0,
Do: 1,
Part: 1,
Read: 2,
Create: 3,
Update: 4,
Delete: 5,
All: 6
}
class authLevel {
level = level.None
constructor(level: number) {
this.level = level
}
CanDo(): boolean {
return this.level >= level.Do
}
CanRead(): boolean {
return this.level >= level.Read
}
CanCreate(): boolean {
return this.level >= level.Create
}
CanUpdate(): boolean {
return this.level >= level.Update
}
CanDelete(): boolean {
return this.level >= level.Delete
}
CanDoAny(): boolean {
return this.level >= level.All
}
}
export class auths {
private readonly list: modelsSimpleAuth[]
constructor(auths: modelsSimpleAuth[]) {
this.list = auths
}
Get(name: string, rid: string): authLevel {
let l = level.None
for (let i of this.list) {
if (i.name == name && (i.rid === '' || i.rid === rid) && i.level > l) {
l = i.level
}
}
return new authLevel(l)
}
}
export interface Auths {
Get(name: string, rid: string): authLevel
}
export function NewAuths(a: modelsSimpleAuth[]): Auths {
return new auths(a)
}

@ -0,0 +1,146 @@
/*
* @name: index
* @author: veypi <i@veypi.com>
* @date: 2021-11-18 17:36
* @descriptionindex
*/
import { NewAuths, Auths, modelsSimpleAuth } from './auth'
export { NewAuths }
export interface modelsBread {
Index: number
Name: string
Type?: string
RName: string
RParams?: any
RQuery?: any
}
export interface modelsApp {
created: string
updated: string
delete_flag: boolean
des: string
hide: boolean
icon: string
id: string
name: string
redirect: string
role_id: string
status: number
user_count: number
au: modelsAppUser
// Creator: number
// Des: string
// EnableEmail: boolean
// EnablePhone: boolean
// EnableRegister: true
// EnableUser: boolean
// EnableUserKey: boolean
// EnableWx: boolean
// Hide: boolean
// Host: string
// Icon: string
// InitRole?: null
// InitRoleID: number
// Name: string
// UUID: string
// UserCount: number
// UserKeyUrl: string
// UserRefreshUrl: string
// UserStatus: string
// Users: null
}
export enum AUStatus {
OK = 0,
Disabled = 1,
Applying = 2,
Deny = 3,
}
export interface modelsAppUser {
app_id: string
user_id: string
status: AUStatus
}
export interface modelsUser {
id: string
created: string
updated: string
delete_flag: boolean
username: string
nickname: string
email: string
phone: string
icon: string
status: number
used: number
space: number
// Index 前端缓存
// Index?: number
// Apps: modelsApp[]
// Auths: null
// CreatedAt: string
// DeletedAt: null
// ID: number
// Icon: string
// Position: string
// Roles: null
// Status: string
// UpdatedAt: string
// Username: string
// Email: string
// Nickname: string
// Phone: string
}
export interface modelsAuth {
App?: modelsApp
AppUUID: string
CreatedAt: string
DeletedAt: null
ID: number
Level: number
RID: string
RUID: string
Resource?: modelsResource
ResourceID: number
Role?: modelsRole
RoleID: number
UpdatedAt: string
User?: modelsUser
UserID?: number
}
export interface modelsRole {
App?: modelsApp
AppUUID: string
Auths: null
CreatedAt: string
DeletedAt: null
ID: number
Name: string
Tag: string
UpdatedAt: string
UserCount: number
}
export interface modelsResource {
App?: modelsApp
AppUUID: string
CreatedAt: string
DeletedAt: null
Des: string
ID: number
Name: string
UpdatedAt: string
}

@ -1,42 +1,137 @@
<template> <template>
<q-page class="row items-center justify-evenly"> <div>
<example-component <div v-if="ofApps.length > 0">
title="Example component" <div class="flex justify-between">
active <h1 class="page-h1">我的应用</h1>
:todos="todos" <div class="my-5 mr-10">
:meta="meta" <q-btn @click="new_flag = true" v-if="store.state.user.auth.Get(R.App, '').CanCreate()">
></example-component> </q-btn>
</q-page> </div>
</div>
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center">
<div v-for="(item, k) in ofApps" class="flex items-center justify-center" :key="k">
<AppCard :core="item"></AppCard>
</div>
</div>
</div>
<div class="mt-20" v-if="apps.length > 0">
<h1 class="page-h1">应用中心</h1>
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center">
<div v-for="(item, k) in apps" class="flex items-center justify-center" :key="k">
<AppCard :core="item"></AppCard>
</div>
</div>
</div>
<q-dialog v-model="new_flag">
<q-card class="w-4/5 md:w-96 rounded-2xl">
<q-card-section>
<div class="text-h6">Our Changing Planet</div>
<div class="text-subtitle2">by John Doe</div>
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<q-form @submit="create_new">
<q-input label="应用名" v-model="temp_app.name"></q-input>
<!-- <uploader url="test.ico" @success="(e) => { -->
<!-- temp_app.icon = e; -->
<!-- } -->
<!-- "> -->
<!-- <q-avatar size="large" round :src="temp_app.icon"> </q-avatar> -->
<!-- </uploader> -->
<div class="flex justify-end">
<q-btn class="mx-3" @click="new_flag = false">取消</q-btn>
<q-btn type="submit">创建</q-btn>
</div>
</q-form>
</q-card-section>
</q-card>
</q-dialog>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Todo, Meta } from 'components/models'; import { onMounted, ref } from 'vue';
import ExampleComponent from 'components/ExampleComponent.vue'; import api from 'src/boot/api';
import { ref } from 'vue'; import msg from '@veypi/msg';
import { modelsApp, modelsUser } from 'src/models';
import { useQuasar } from 'quasar';
import { useUserStore } from 'src/stores/user';
import AppCard from 'components/app.vue'
const todos = ref<Todo[]>([
{ let apps = ref<modelsApp[]>([]);
id: 1, let ofApps = ref<modelsApp[]>([]);
content: 'ct1' let $q = useQuasar()
},
function getApps() {
$q.loadingBar.start()
api.app.list().then(
(e: modelsApp[]) => {
apps.value = e;
api.app
.user("")
.list(useUserStore().id)
.then(
(e: modelsUser[]) => {
$q.loadingBar.stop();
ofApps.value = [];
console.log(e)
// for (let i in e) {
// let ai = apps.value.findIndex((a) => a.id === e[i]);
// if (ai >= 0) {
// apps.value[ai].UserStatus = e[i].Status;
// if (e[i].Status === "ok") {
// ofApps.value.push(apps.value[ai]);
// apps.value.splice(ai, 1);
// }
// }
// }
}
);
}
);
}
onMounted(() => {
getApps();
});
let new_flag = ref(false);
let temp_app = ref({
name: "",
icon: "",
});
let form_ref = ref(null);
let rules = {
name: [
{ {
id: 2, required: true,
content: 'ct2' validator(r: any, v: any) {
return (
(v && v.length >= 2 && v.length <= 16) || "长度要求2~16"
);
}, },
{ trigger: ["input", "blur"],
id: 3,
content: 'ct3'
}, },
{ ],
id: 4, };
content: 'ct4'
function create_new() {
form_ref.value.validate((e: any) => {
if (!e) {
api.app.create(temp_app.value.name, temp_app.value.icon).Start(
(e) => {
e.Status = "ok";
ofApps.value.push(e);
msg.success("创建成功");
new_flag.value = false;
}, },
{ (e) => {
id: 5, msg.warning("创建失败: " + e);
content: 'ct5' }
);
} }
]);
const meta = ref<Meta>({
totalCount: 1200
}); });
}
</script> </script>

@ -0,0 +1,107 @@
<template>
<div class="flex items-center justify-center">
<div class="px-10 pb-9 pt-28 rounded-xl w-96">
<q-form autofocus @submit="onSubmit" @reset="onReset">
<q-input v-model="data.username" label="用户名" hint="username" lazy-rules :rules="data_rules.username" />
<q-input v-model="data.password" :type="isPwd ? 'password' :
'text'" hint="password" :rules="data_rules.password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer" @click="isPwd = !isPwd" />
</template>
</q-input>
<div class="flex justify-around mt-4">
<q-btn label="注册" @click="router.push({ name: 'register' })" color="info"></q-btn>
<q-btn label="登录" type="submit" color="primary" />
<q-btn label="重置" type="reset" color="primary" flat class="q-ml-sm" />
</div>
</q-form>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import api from 'src/boot/api'
import msg from '@veypi/msg'
import util from 'src/libs/util'
import { useUserStore } from 'src/stores/user'
import { useAppStore } from 'src/stores/app'
import { modelsApp } from 'src/models'
const app = useAppStore()
const user = useUserStore()
const route = useRoute()
const router = useRouter()
let data = ref({
username: '',
password: '',
})
const data_rules = {
username: [
(v: string) => v && v.length >= 3 && v.length <= 16 || '长度要求3~16'
],
password: [
(v: string) => v && v.length >= 6 && v.length <= 16 || '长度要求6~16'
]
}
let isPwd = ref(true)
const onSubmit = () => {
console.log(data.value)
api.user.login(data.value.username,
data.value.password).then((data: any) => {
localStorage.auth_token = data.auth_token
msg.Info('登录成功')
user.fetchUserData()
let url = route.query.redirect || data.redirect || '/'
redirect(url)
console.log(data)
})
}
const onReset = () => {
data.value.password = ''
data.value.username = ''
}
let uuid = computed(() => {
return route.query.uuid
})
let ifLogOut = computed(() => {
return route.query.logout === '1'
})
function redirect(url: string) {
if (uuid.value && uuid.value !== app.id) {
api.app.get(uuid.value as string).then((app: modelsApp) => {
api.token(uuid.value as string).get().then(e => {
url = url || app.redirect
console.log(e)
// e = encodeURIComponent(e)
// url = url.replaceAll('$token', e)
window.location.href = url
})
})
} else if (util.checkLogin()) {
if (url) {
router.push(url)
} else {
router.push({ name: 'home' })
}
}
}
onMounted(() => {
if (!ifLogOut.value) {
redirect('')
}
})
</script>
<style scoped></style>

@ -0,0 +1,55 @@
<template>
<div class="flex items-center justify-center">
<div class="px-10 pb-9 pt-28 rounded-xl w-96">
<q-form @submit="register" autofocus>
<q-input v-model="data.username" label="用户名" hint="username" lazy-rules :rules="rules.username"></q-input>
<q-input label="密码" v-model="data.password" type="password" lazy-rules :rules="rules.password"></q-input>
<q-input label="密码" v-model="data.pass" type="password" lazy-rules :rules="rules.pass"></q-input>
<div class="flex justify-around mt-4">
<q-btn label="注册" type="submit" color="primary" />
</div>
</q-form>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import msg from '@veypi/msg'
import api from "src/boot/api";
const router = useRouter()
let data = ref({
username: '',
password: '',
pass: ''
})
let rules = {
username: [
(v: string) => v && v.length >= 3 && v.length <= 16 || '长度要求3~16'
],
password: [
(v: string) => v && v.length >= 6 && v.length <= 16 || '长度要求6~16'
],
pass: [
(v: string) => v && v === data.value.password || '密码不正确'
]
}
function register() {
api.user.register(data.value.username, data.value.password).then(u => {
console.log(u)
msg.Info('注册成功')
router.push({ name: 'login' })
}).catch(e => {
console.log(e)
msg.Warn('注册失败:' + e.data)
})
}
onMounted(() => {
})
</script>
<style scoped></style>

@ -1,4 +1,6 @@
import { route } from 'quasar/wrappers'; import { route } from 'quasar/wrappers';
import util from 'src/libs/util';
import { useUserStore } from 'src/stores/user';
import { import {
createMemoryHistory, createMemoryHistory,
createRouter, createRouter,
@ -31,6 +33,28 @@ export default route(function (/* { store, ssrContext } */) {
// quasar.conf.js -> build -> publicPath // quasar.conf.js -> build -> publicPath
history: createHistory(process.env.VUE_ROUTER_BASE), history: createHistory(process.env.VUE_ROUTER_BASE),
}); });
const u = useUserStore()
Router.beforeEach((to, from) => {
console.log(to.meta)
if (to.meta.requiresAuth && !util.checkLogin()) {
// 此路由需要授权,请检查是否已登录
// 如果没有,则重定向到登录页面
return {
name: 'login',
// 保存我们所在的位置,以便以后再来
query: { redirect: to.fullPath },
}
}
if (to.meta.checkAuth) {
if (!to.meta.checkAuth(u.auth, to)) {
// if (window.$msg) {
// window.$msg.warning('无权访问')
// }
return from
}
}
})
return Router; return Router;
}); });

@ -1,12 +1,43 @@
import { Auths } from 'src/models/auth';
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
declare module 'vue-router' {
interface RouteMeta {
// 是可选的
isAdmin?: boolean
title?: string
// 每个路由都必须声明
requiresAuth: boolean
checkAuth?: (a: Auths, r?: RouteLocationNormalized) => boolean
}
}
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
path: '/', path: '/',
component: () => import('layouts/MainLayout.vue'), component: () => import('layouts/MainLayout.vue'),
children: [{ path: '', component: () => import('pages/IndexPage.vue') }], meta: {
requiresAuth: true,
},
children: [
{
path: '',
component: () => import('pages/IndexPage.vue')
}
],
},
{
path: '/login/:uuid?',
name: 'login',
component: () => import('pages/login.vue'),
},
{
path: '/register/:uuid?',
name: 'register',
component: () => import('pages/register.vue'),
}, },
// Always leave this as last one, // Always leave this as last one,
// but you can also remove it // but you can also remove it
{ {

@ -0,0 +1,20 @@
/*
* app.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-09-30 17:26
* Distributed under terms of the MIT license.
*/
import { defineStore } from 'pinia';
export const useAppStore = defineStore('app', {
state: () => ({
id: '',
title: '',
}),
getters: {
},
actions: {
},
});

@ -0,0 +1,50 @@
/*
* user.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-09-22 21:05
* Distributed under terms of the MIT license.
*/
import { defineStore } from 'pinia';
import { Auths, modelsUser, NewAuths } from 'src/models';
import { useRouter } from 'vue-router';
import { Base64 } from 'js-base64'
import api from 'src/boot/api';
export const useUserStore = defineStore('user', {
state: () => ({
id: '',
local: {} as modelsUser,
auth: {} as Auths,
ready: false
}),
getters: {
},
actions: {
logout() {
this.ready = false
localStorage.removeItem('auth_token')
const r = useRouter()
r.push({ name: 'login' })
},
fetchUserData() {
let token = localStorage.getItem('auth_token')?.split('.');
if (!token || token.length !== 3) {
return false
}
let data = JSON.parse(Base64.decode(token[1]))
if (data.id) {
this.auth = NewAuths(data.Auth)
api.user.get(data.id).then((e: modelsUser) => {
console.log(e)
this.id = e.id
this.local = e
this.ready = true
}).catch((e) => {
this.logout()
})
}
}
},
});

@ -453,6 +453,11 @@
"@typescript-eslint/types" "5.62.0" "@typescript-eslint/types" "5.62.0"
eslint-visitor-keys "^3.3.0" eslint-visitor-keys "^3.3.0"
"@veypi/msg@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@veypi/msg/-/msg-0.1.0.tgz#2ebe899527a11ed11f68c2c96f468cfcc66ad3d4"
integrity sha512-58dj5nnpHsxaiK5sbPiDK5t8OF4uvN+kAmWhU0BRAgXHpkxkZNZ8rn7hXvSVybG1BbM8EuMNkq0lIxGYNKl8aw==
"@vitejs/plugin-vue@^2.2.0": "@vitejs/plugin-vue@^2.2.0":
version "2.3.4" version "2.3.4"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.3.4.tgz#966a6279060eb2d9d1a02ea1a331af071afdcf9e" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.3.4.tgz#966a6279060eb2d9d1a02ea1a331af071afdcf9e"
@ -2106,6 +2111,11 @@ jiti@^1.18.2:
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42"
integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA== integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==
js-base64@^3.7.5:
version "3.7.5"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.5.tgz#21e24cf6b886f76d6f5f165bfcd69cc55b9e3fca"
integrity sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==
js-yaml@^4.1.0: js-yaml@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"

Loading…
Cancel
Save