master
veypi 1 year ago
parent ad4f9d4b6a
commit 664bf9c922

232
oab/Cargo.lock generated

@ -91,6 +91,44 @@ dependencies = [
"syn 2.0.37",
]
[[package]]
name = "actix-multipart"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b960e2aea75f49c8f069108063d12a48d329fc8b60b786dfc7552a9d5918d2d"
dependencies = [
"actix-multipart-derive",
"actix-utils",
"actix-web",
"bytes",
"derive_more",
"futures-core",
"futures-util",
"httparse",
"local-waker",
"log",
"memchr",
"mime",
"serde",
"serde_json",
"serde_plain",
"tempfile",
"tokio",
]
[[package]]
name = "actix-multipart-derive"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d"
dependencies = [
"darling",
"parse-size",
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]]
name = "actix-router"
version = "0.5.1"
@ -822,6 +860,73 @@ dependencies = [
"cipher",
]
[[package]]
name = "darling"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.37",
]
[[package]]
name = "darling_macro"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core",
"quote",
"syn 2.0.37",
]
[[package]]
name = "dav-server"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c1b3c57ea8b45dc1303de4653b12e3a2742ff023a469083716feea98abfdbe"
dependencies = [
"actix-web",
"bytes",
"futures-channel",
"futures-util",
"headers",
"htmlescape",
"http",
"http-body",
"lazy_static",
"libc",
"log",
"lru",
"mime_guess",
"parking_lot 0.12.1",
"percent-encoding",
"pin-project",
"pin-utils",
"regex",
"time",
"tokio",
"url",
"uuid",
"xml-rs",
"xmltree",
]
[[package]]
name = "der"
version = "0.5.1"
@ -927,9 +1032,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.3"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480"
dependencies = [
"errno-dragonfly",
"libc",
@ -1238,6 +1343,30 @@ dependencies = [
"hashbrown 0.14.1",
]
[[package]]
name = "headers"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270"
dependencies = [
"base64 0.21.4",
"bytes",
"headers-core",
"http",
"httpdate",
"mime",
"sha1",
]
[[package]]
name = "headers-core"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
dependencies = [
"http",
]
[[package]]
name = "heck"
version = "0.4.1"
@ -1295,6 +1424,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "htmlescape"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
[[package]]
name = "http"
version = "0.2.9"
@ -1306,6 +1441,26 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-auth-basic"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd2e17aacf7f4a2428def798e2ff4f4f883c0987bdaf47dd5c8bc027bc9f1ebc"
dependencies = [
"base64 0.13.1",
]
[[package]]
name = "http-body"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
"bytes",
"http",
"pin-project-lite",
]
[[package]]
name = "http-range"
version = "0.1.5"
@ -1347,6 +1502,12 @@ dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.4.0"
@ -1549,6 +1710,15 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "lru"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21"
dependencies = [
"hashbrown 0.14.1",
]
[[package]]
name = "md-5"
version = "0.10.6"
@ -1561,9 +1731,9 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.6.3"
version = "2.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
[[package]]
name = "mime"
@ -1713,14 +1883,18 @@ name = "oab"
version = "0.1.0"
dependencies = [
"actix-files",
"actix-multipart",
"actix-web",
"aes-gcm",
"base64 0.13.1",
"block-padding",
"chrono",
"clap",
"dav-server",
"futures-util",
"generic-array",
"http",
"http-auth-basic",
"include_dir",
"jsonwebtoken",
"lazy_static",
@ -1854,6 +2028,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "parse-size"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae"
[[package]]
name = "paste"
version = "1.0.14"
@ -1893,6 +2073,26 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "pin-project"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
@ -2488,6 +2688,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_plain"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
dependencies = [
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.16"
@ -3631,6 +3840,21 @@ dependencies = [
"tap",
]
[[package]]
name = "xml-rs"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a"
[[package]]
name = "xmltree"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb"
dependencies = [
"xml-rs",
]
[[package]]
name = "zeroize"
version = "1.6.0"

@ -41,4 +41,9 @@ uuid = { version = "1.1", features = ["v3","v4", "fast-rng", "macro-diagnostics"
serde_repr = "0.1.8"
proc = {path="proc"}
# dav-server = {version = "0.5.7", features = ["default","actix-compat"], path = "../../../test/dav-server-rs/" }
dav-server = {version = "0.5.7", features = ["default","actix-compat"]}
http = "0.2.9"
http-auth-basic = "0.3.3"
actix-multipart = "0.6.1"

@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize};
use tracing::info;
use crate::{
libs,
models::{self, access, app, app_user, rand_str, AUStatus, AccessLevel, Token},
AppState, Error, Result,
};
@ -101,13 +102,7 @@ pub async fn create(
..Default::default()
};
let ac: access::Model = ac.insert(&db).await?;
let au = app_user::ActiveModel {
app_id: sea_orm::ActiveValue::Set(obj.id.clone()),
user_id: sea_orm::ActiveValue::Set(t.id.clone()),
status: sea_orm::ActiveValue::Set(AUStatus::OK as i32),
..Default::default()
};
let au: app_user::Model = au.insert(&db).await?;
libs::user::connect_to_app(t.id.clone(), obj.id.clone(), &db, Some(obj.clone())).await?;
db.commit().await?;
Ok(web::Json(obj))
}

@ -11,21 +11,12 @@ mod app;
mod appuser;
mod resource;
mod role;
mod upload;
mod user;
use crate::{Error, Result};
use actix_web::{get, web};
#[get("/hello/{name}")]
async fn greet(name: web::Path<u32>) -> Result<String> {
let n = name.into_inner();
if n > 0 {
Ok(format!("Hello {n}!"))
} else {
Err(Error::Unknown)
}
}
use actix_web::web;
pub fn routes(cfg: &mut web::ServiceConfig) {
cfg.service(upload::save_files);
cfg.service(user::get)
.service(user::list)
.service(user::register)
@ -37,6 +28,4 @@ pub fn routes(cfg: &mut web::ServiceConfig) {
.service(app::del);
cfg.service(appuser::get);
cfg.service(greet);
}

@ -0,0 +1,92 @@
//
// upload.rs
// Copyright (C) 2023 veypi <i@veypi.com>
// 2023-10-03 21:50
// Distributed under terms of the MIT license.
//
//
use actix_multipart::form::{tempfile::TempFile, MultipartForm};
use actix_web::{post, web, Responder};
use proc::access_read;
use tracing::{info, warn};
use crate::{AppState, Error, Result};
#[derive(Debug, MultipartForm)]
struct UploadForm {
files: Vec<TempFile>,
}
#[post("/upload/")]
#[access_read("app")]
async fn save_files(
MultipartForm(form): MultipartForm<UploadForm>,
stat: web::Data<AppState>,
) -> Result<impl Responder> {
let l = form.files.len();
let mut res: Vec<String> = Vec::new();
info!("!|||||||||||_{}_|", l);
for f in form.files {
info!("saving to {:#?}", f);
let fname = f.file_name.unwrap();
let path = format!("{}tmp/{}", stat.media_path, fname);
info!("saving to {path}");
match f.file.persist(path) {
Ok(t) => {
info!("{:#?}", t);
res.push(format!("/media/tmp/{}", fname))
}
Err(e) => {
warn!("{}", e);
return Err(Error::InternalServerError);
}
};
}
Ok(web::Json(res))
}
// #[actix_web::main]
// async fn main() -> std::io::Result<()> {
// HttpServer::new(|| {
// App::new()
// .wrap(middleware::Logger::default())
// .app_data(TempFileConfig::default().directory("./tmp"))
// .service(
// web::resource("/")
// .route(web::get().to(index))
// .route(web::post().to(save_files)),
// )
// })
// .bind(("127.0.0.1", 8080))?
// .workers(2)
// .run()
// .await
// }
// /// Example of the old manual way of processing multipart forms.
// #[allow(unused)]
// async fn save_file_manual(mut payload: Multipart) -> Result<HttpResponse, Error> {
// // iterate over multipart stream
// while let Some(mut field) = payload.try_next().await? {
// // A multipart/form-data stream has to contain `content_disposition`
// let content_disposition = field.content_disposition();
// let filename = content_disposition
// .get_filename()
// .map_or_else(|| Uuid::new_v4().to_string(), sanitize_filename::sanitize);
// let filepath = format!("./tmp/{filename}");
// // File::create is blocking operation, use threadpool
// let mut f = web::block(|| std::fs::File::create(filepath)).await??;
// // Field in turn is stream of *Bytes* object
// while let Some(chunk) = field.try_next().await? {
// // filesystem operations are blocking, we have to use threadpool
// f = web::block(move || f.write_all(&chunk).map(|_| f)).await??;
// }
// }
// Ok(HttpResponse::Ok().into())
// }

@ -95,7 +95,7 @@ pub async fn login(
// 未绑定应用时进行绑定操作
let aid = stat.uuid.clone();
let db = stat.db().begin().await?;
let s = libs::user::connect_to_app(u.clone(), aid, &db).await?;
let s = libs::user::connect_to_app(u.id.clone(), aid, &db, None).await?;
db.commit().await?;
s
}
@ -165,7 +165,7 @@ pub async fn register(
let u = u.insert(&db).await?;
// 关联应用
libs::user::connect_to_app(u.clone(), stat.uuid.clone(), &db).await?;
libs::user::connect_to_app(u.id.clone(), stat.uuid.clone(), &db, None).await?;
db.commit().await?;

@ -18,6 +18,7 @@ use clap::{Args, Parser, Subcommand};
use lazy_static::lazy_static;
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use sqlx::{mysql::MySqlPoolOptions, Pool};
use tracing::Level;
use crate::Result;
@ -76,6 +77,7 @@ pub struct AppState {
pub db_pass: String,
pub db_name: String,
pub log_dir: Option<String>,
pub fs_root: String,
/// "100MB" 日志分割尺寸-单位KB,MB,GB
pub log_temp_size: Option<String>,
pub log_pack_compress: Option<String>,
@ -119,7 +121,6 @@ impl AppState {
key: "AMpjwQHwVjGsb1WC4WG6".to_string(),
debug: true,
server_url: "127.0.0.1:4001".to_string(),
media_path: "/Users/veypi/test/media/".to_string(),
db_url: "localhost:3306".to_string(),
db_user: "root".to_string(),
db_pass: "123456".to_string(),
@ -127,6 +128,8 @@ impl AppState {
log_dir: None,
log_temp_size: None,
log_pack_compress: None,
media_path: "/Users/veypi/test/media/".to_string(),
fs_root: "/Users/veypi/test/media/".to_string(),
log_level: None,
jwt_secret: None,
_sqlx: None,
@ -192,5 +195,8 @@ pub fn init_log() {
tracing_subscriber::fmt()
.with_line_number(true)
.with_timer(FormatTime {})
.with_max_level(Level::DEBUG)
.with_target(false)
.with_file(true)
.init();
}

@ -0,0 +1,121 @@
//
// fs.rs
// Copyright (C) 2023 veypi <i@veypi.com>
// 2023-10-02 22:51
// Distributed under terms of the MIT license.
//
//
use std::{fs, path::Path};
use actix_web::web;
use dav_server::{
actix::{DavRequest, DavResponse},
body::Body,
fakels::FakeLs,
localfs::LocalFs,
DavConfig, DavHandler,
};
use http::Response;
use http_auth_basic::Credentials;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use tracing::{info, warn};
use crate::{
models::{self, UserPlugin},
AppState, Error, Result,
};
pub fn core() -> DavHandler {
DavHandler::builder()
.locksystem(FakeLs::new())
.strip_prefix("/file/")
.build_handler()
}
pub async fn dav_handler(
req: DavRequest,
davhandler: web::Data<DavHandler>,
stat: web::Data<AppState>,
) -> DavResponse {
let root = stat.fs_root.clone();
match handle_file(req, stat).await {
Ok((p, req)) => {
let p = Path::new(&root).join(p);
if !p.exists() {
match fs::create_dir_all(p.clone()) {
Ok(_) => {}
Err(e) => {
warn!("{}", e);
}
}
}
info!("mount {}", p.to_str().unwrap());
let config = DavConfig::new().filesystem(LocalFs::new(p, false, false, true));
davhandler.handle_with(config, req.request).await.into()
}
Err(e) => {
warn!("handle file failed: {}", e);
Response::builder()
.status(401)
.header("WWW-Authenticate", "Basic realm=\"file\"")
.body(Body::from("please auth".to_string()))
.unwrap()
.into()
}
}
}
async fn handle_file(req: DavRequest, stat: web::Data<AppState>) -> Result<(String, DavRequest)> {
let p = req.request.uri();
let headers = req.request.headers();
let m = req.request.method();
// handle_authorization(req.request.headers());
info!("access {} to {}", m, p);
let auth_token = headers.get("auth_token");
let authorization = headers.get("authorization");
let app_id = match headers.get("app_id") {
Some(i) => i.to_str().unwrap_or(""),
None => "",
};
match auth_token {
Some(t) => match models::Token::from(t.to_str().unwrap_or("")) {
Ok(t) => {
if t.is_valid() {
if app_id != "" {
// 只有秘钥才能访问app数据
if t.can_read("app", app_id) {
return Ok((format!("app/{}/", app_id), req));
}
} else {
return Ok((format!("user/{}/", t.id), req));
}
}
}
Err(_) => {}
},
None => {}
}
match authorization {
Some(au) => {
let credentials =
Credentials::from_header(au.to_str().unwrap_or("").to_string()).unwrap();
info!("{}|{}", credentials.user_id, credentials.password);
match models::user::Entity::find()
.filter(models::user::Column::Username.eq(credentials.user_id))
.one(stat.db())
.await?
{
Some(u) => {
u.check_pass(&credentials.password)?;
return Ok((format!("user/{}/", u.id), req));
}
None => {}
}
}
None => {}
}
Err(Error::NotAuthed)
}

@ -7,6 +7,7 @@
pub mod auth;
pub mod user;
pub mod fs;
use std::future::{ready, Ready};

@ -6,21 +6,24 @@
//
use crate::{
models::{self, app, app_user, user, user_role},
models::{self, app, app_user, user_role},
Error, Result,
};
use sea_orm::{ActiveModelTrait, ConnectionTrait, DatabaseTransaction, EntityTrait};
// 尝试绑定应用
pub async fn connect_to_app(
u: user::Model,
uid: String,
aid: String,
db: &DatabaseTransaction,
app_obj: Option<app::Model>,
) -> Result<app_user::Model> {
let app_obj: Option<app::Model> = app::Entity::find_by_id(&aid).one(db).await?;
let app_obj = match app_obj {
Some(o) => o,
None => return Err(Error::NotFound(aid.clone())),
None => match app::Entity::find_by_id(&aid).one(db).await? {
Some(o) => o,
None => return Err(Error::NotFound(aid.clone())),
},
};
let m = match app_obj.join_method.into() {
models::AppJoin::Disabled => return Err(Error::AppDisabledRegister),
@ -29,26 +32,26 @@ pub async fn connect_to_app(
};
let au = app_user::ActiveModel {
app_id: sea_orm::ActiveValue::Set(aid.clone()),
user_id: sea_orm::ActiveValue::Set(u.id.clone()),
user_id: sea_orm::ActiveValue::Set(uid.clone()),
status: sea_orm::ActiveValue::Set(m.clone() as i32),
..Default::default()
};
let au = au.insert(db).await?;
if m == models::AUStatus::OK {
after_connected_to_app(u, app_obj, db).await?;
after_connected_to_app(uid, app_obj, db).await?;
}
Ok(au)
}
// 成功绑定应用后操作
pub async fn after_connected_to_app(
u: user::Model,
uid: String,
obj: app::Model,
db: &DatabaseTransaction,
) -> Result<()> {
if obj.role_id.is_some() {
user_role::ActiveModel {
user_id: sea_orm::ActiveValue::Set(u.id.clone()),
user_id: sea_orm::ActiveValue::Set(uid.clone()),
role_id: sea_orm::ActiveValue::Set(obj.role_id.unwrap().clone()),
..Default::default()
}
@ -56,7 +59,7 @@ pub async fn after_connected_to_app(
.await?;
};
let sql = format!(
"update app set user_count = user_count + 1 where app_id = {}",
"update app set user_count = user_count + 1 where id = '{}'",
obj.id
);
db.execute(sea_orm::Statement::from_string(

@ -38,6 +38,7 @@ async fn main() -> Result<()> {
}
async fn web(data: AppState) -> Result<()> {
let url = data.server_url.clone();
let dav = libs::fs::core();
let serv = HttpServer::new(move || {
let logger = middleware::Logger::default();
let json_config = web::JsonConfig::default()
@ -55,10 +56,10 @@ async fn web(data: AppState) -> Result<()> {
let app = App::new();
app.wrap(logger)
.wrap(middleware::Compress::default())
.app_data(web::Data::new(data.clone()))
.service(fs::Files::new("/media", data.media_path.clone()).show_files_listing())
.service(
web::scope("api")
.app_data(web::Data::new(data.clone()))
.wrap(
ErrorHandlers::new()
.handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header),
@ -67,6 +68,11 @@ async fn web(data: AppState) -> Result<()> {
.app_data(json_config)
.configure(api::routes),
)
.service(
web::scope("file")
.app_data(web::Data::new(dav.clone()))
.service(web::resource("/{tail:.*}").to(libs::fs::dav_handler)),
)
});
info!("listen to {}", url);
serv.bind(url)?.run().await?;

@ -12,7 +12,6 @@ pub struct Model {
pub id: i32,
pub created: Option<DateTime>,
pub updated: Option<DateTime>,
pub delete_flag: i8,
pub app_id: String,
pub name: String,
pub role_id: Option<String>,

@ -12,7 +12,6 @@ pub struct Model {
pub id: String,
pub created: Option<DateTime>,
pub updated: Option<DateTime>,
pub delete_flag: i8,
pub key: String,
pub name: String,
pub icon: Option<String>,

@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize};
pub struct Model {
pub created: Option<DateTime>,
pub updated: Option<DateTime>,
pub delete_flag: i8,
#[sea_orm(primary_key, auto_increment = false)]
pub app_id: String,
#[sea_orm(primary_key, auto_increment = false)]

@ -12,7 +12,6 @@ pub struct Model {
pub id: String,
pub created: Option<DateTime>,
pub updated: Option<DateTime>,
pub delete_flag: i8,
pub app_id: String,
pub name: String,
pub des: Option<String>,

@ -12,7 +12,6 @@ pub struct Model {
pub id: String,
pub created: Option<DateTime>,
pub updated: Option<DateTime>,
pub delete_flag: i8,
#[sea_orm(unique)]
pub username: String,
pub nickname: Option<String>,

@ -168,6 +168,7 @@ impl From<Box<dyn std::fmt::Display>> for Error {
}
}
impl actix_web::Responder for Error {
type Body = actix_web::body::BoxBody;
fn respond_to(self, _req: &actix_web::HttpRequest) -> HttpResponse<Self::Body> {

@ -15,8 +15,10 @@
"dependencies": {
"@quasar/extras": "^1.16.4",
"@veypi/msg": "^0.1.0",
"@veypi/one-icon": "2",
"axios": "^1.2.1",
"js-base64": "^3.7.5",
"mitt": "^3.0.1",
"pinia": "^2.0.11",
"quasar": "^2.6.0",
"vue": "^3.0.0",

@ -145,7 +145,8 @@ module.exports = configure(function(/* ctx */) {
// Quasar plugins
plugins: [
'LoadingBar'
'LoadingBar',
'AppFullscreen',
]
},

@ -30,3 +30,19 @@ onBeforeMount(() => {
})
</script>
<style>
html,
body,
#q-app {
@apply font-mono h-full w-full select-none;
}
.page-h1 {
font-size: 1.5rem;
line-height: 2rem;
margin-left: 2.5rem;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
}
</style>

@ -1,34 +1,22 @@
<template>
<q-item
clickable
tag="a"
target="_blank"
:href="link"
>
<q-item-section
v-if="icon"
avatar
>
<q-item v-ripple clickable tag="a" :href="link" :to="to">
<q-item-section v-if="icon" avatar>
<q-icon :name="icon" />
</q-item-section>
<q-item-section>
<q-item-label>{{ title }}</q-item-label>
<q-item-label caption>{{ caption }}</q-item-label>
<!-- <q-item-label caption>{{ caption }}</q-item-label> -->
</q-item-section>
</q-item>
</template>
<script setup lang="ts">
export interface EssentialLinkProps {
title: string;
caption?: string;
link?: string;
icon?: string;
}
withDefaults(defineProps<EssentialLinkProps>(), {
import { MenuLink } from 'src/models'
withDefaults(defineProps<MenuLink>(), {
caption: '',
link: '#',
icon: '',
});
</script>

@ -2,7 +2,8 @@
<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 class="cursor-pointer" style="--color: none" @click="Go" round size="5rem">
<img :src="core.icon">
</q-avatar>
</div>
<div class="col-span-3 grid grid-cols-1 items-center text-left">
@ -36,7 +37,7 @@ let props = withDefaults(defineProps<{
function Go() {
switch (props.core.au.status) {
case AUStatus.OK:
router.push({ name: "app.main", params: { uuid: props.core.UUID } });
router.push({ name: "app.home", params: { id: props.core.id } });
return;
case AUStatus.Applying:
msg.Info("请等待管理员审批进入");

@ -0,0 +1,30 @@
<!--
* main.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-04 10:56
* Distributed under terms of the MIT license.
-->
<template>
<q-list>
<!-- <q-item-label header> -->
<!-- Essential Links -->
<!-- </q-item-label> -->
<EssentialLink v-for="link in menu.list" :key="link.title" v-bind="link" />
</q-list>
</template>
<script lang="ts" setup>
import EssentialLink from 'src/components/EssentialLink.vue';
import { useMenuStore } from 'src/stores/menu';
import { onMounted } from 'vue';
let menu = useMenuStore()
onMounted(() => {
console.log('loading main menu')
})
</script>
<style scoped></style>

@ -1,56 +1,87 @@
<template>
<div @click="click">
<input ref="file" type="file" hidden @change="upload">
<input enctype="multipart/form-data" ref="file" name="files" multiple type="file" hidden @change="upload">
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import { createClient } from "webdav";
// import { createClient } from "webdav";
import { ref } from 'vue';
import { useAppStore } from "src/stores/app";
import axios from "axios";
let file = ref<HTMLInputElement>()
let app = useAppStore();
let emits = defineEmits<{
(e: 'success', v: string): void
(e: 'failed'): void
}>()
let props = withDefaults(defineProps<{
url: string
multiple?: boolean,
}>(), {
url: '',
multiple: false
})
function click() {
file.value?.dispatchEvent(new MouseEvent('click'))
}
let prefix = '/file/public/app/' + app.id + '/'
let client = createClient(prefix,
{ headers: { auth_token: localStorage.getItem('auth_token') || '' } })
async function upload() {
let list = file.value?.files || []
if (list.length) {
let reader = new FileReader()
reader.onload = function (event) {
var res = event.target?.result
// let data = new Blob([res])
let url = props.url.replaceAll('.', '.' + new Date().getTime().toString() + '.')
client.putFileContents(url, res).then(e => {
if (e) {
emits('success', prefix + url)
} else {
emits('failed')
}
})
}
reader.readAsArrayBuffer(list[0])
} else {
const upload = (evt: Event) => {
evt.preventDefault()
let f = (evt.target as HTMLInputElement).files as any
var data = new FormData();
console.log(f)
for (let i of f) {
console.log(i)
data.append('files', i, i.data)
}
axios.post("/api/upload/", data, {
headers: {
"Content-Type": 'multipart/form-data',
'auth_token': localStorage.getItem('auth_token')
}
}).then(e => {
console.log(e.data)
emits('success', props.multiple ? e.data : e.data[0])
})
// var token = sessionStorage.getItem('token')
// const config = {
// headers: {
// 'Content-Type': 'multipart/form-data'
// }
// }
// window.API.post('https://110.10.56.10:8000/images/?token=' + token, data, config)
// .then(response => this.$router.push('/listImage'))
// .catch((error) => {
// console.log(JSON.stringify(error))
// })
}
// async function dav_upload() {
// let prefix = '/file/public/app/' + app.id + '/'
// let client = createClient(prefix,
// { headers: { auth_token: localStorage.getItem('auth_token') || '' } })
// let list = file.value?.files || []
// if (list.length) {
// let reader = new FileReader()
// reader.onload = function (event) {
// var res = event.target?.result
// // let data = new Blob([res])
// let url = props.url.replaceAll('.', '.' + new Date().getTime().toString() + '.')
// client.putFileContents(url, res).then(e => {
// if (e) {
// emits('success', prefix + url)
// } else {
// emits('failed')
// }
// })
// }
// reader.readAsArrayBuffer(list[0])
// } else {
// }
// }
</script>
<style scoped></style>

@ -0,0 +1,53 @@
<!--
* AppLayout.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-04 10:46
* Distributed under terms of the MIT license.
-->
<template>
<div>
<h1>{{ app.name }}</h1>
<router-view :data="{ a: 1 }" />
</div>
</template>
<script lang="ts" setup>
import msg from '@veypi/msg';
import api from 'src/boot/api';
import { modelsApp } from 'src/models';
import { useMenuStore } from 'src/stores/menu';
import { computed, watch, ref, onMounted, provide, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router';
let route = useRoute();
let menu = useMenuStore()
let id = computed(() => route.params.id)
let app = ref({} as modelsApp)
provide('app', app)
const sync_app = () => {
api.app.get(id.value as string).then((e: modelsApp) => {
app.value = e
}).catch(e => {
msg.Warn('sync app data failed: ' + e)
})
}
watch(id, () => {
sync_app()
})
onMounted(() => {
sync_app()
menu.set([
])
})
onBeforeUnmount(() => {
menu.load_default()
})
</script>
<style scoped></style>

@ -1,38 +1,42 @@
<template>
<q-layout view="hHh LpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar>
<q-icon size="xl" color="aqua" name='svguse:#icon-glassdoor'></q-icon>
<q-toolbar class="h-16 pl-0">
<q-toolbar-title>
统一认证系统
<q-toolbar-title class="flex items-center cursor-pointer" @click="router.push({ name: 'home' })">
<q-icon size="3.5rem" color="aqua" name='svguse:#icon-glassdoor' style="color: aqua;"></q-icon>
<q-separator dark vertical inset />
<span class="ml-3">
统一认证系统
</span>
</q-toolbar-title>
<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-icon class="mx-2" size="2rem" @click="$q.fullscreen.toggle()"
:name="$q.fullscreen.isActive ? 'fullscreen_exit' : 'fullscreen'" />
<q-icon class="mx-2" size="1.5rem" @click="$q.dark.toggle"
:name="$q.dark.mode ? 'light_mode' : 'dark_mode'"></q-icon>
<OAer @logout="user.logout" :is-dark="$q.dark.mode"></OAer>
</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-header>
<q-drawer v-model="leftDrawerOpen" side="left" bordered>
<q-list>
<q-item-label header>
Essential Links
</q-item-label>
<EssentialLink v-for="link in essentialLinks" :key="link.title" v-bind="link" />
</q-list>
<q-drawer show-if-above :mini="miniState" @mouseover="miniState = false" @mouseout="miniState = true" mini-to-overlay
:width="200" :breakpoint="500" bordered v-model="leftDrawerOpen" side="left" class="pt-4">
<Menu></Menu>
</q-drawer>
<q-page-container>
<router-view />
<q-page-container class="flex">
<q-page class="w-full">
<router-view />
</q-page>
</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>
@ -47,25 +51,25 @@
<script setup lang="ts">
import { ref } from 'vue';
import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue';
import { util } from 'src/libs';
import { useRouter } from 'vue-router';
import Menu from 'src/components/menu.vue'
import { useAppStore } from 'src/stores/app';
import { useUserStore } from 'src/stores/user';
import { OAer, Cfg } from "src/oaer";
Cfg.token.value = util.getToken();
const app = useAppStore()
const user = useUserStore()
const router = useRouter()
const essentialLinks: EssentialLinkProps[] = [
{
title: 'Docs',
caption: 'quasar.dev',
icon: 'school',
link: 'https://quasar.dev'
},
{
title: 'Github',
caption: 'github.com/quasarframework',
icon: 'code',
link: 'https://github.com/quasarframework'
},
]
const leftDrawerOpen = ref(false)
const miniState = ref(true)
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value

@ -1,75 +1,80 @@
import axios from 'axios'
function padLeftZero(str: string): string {
return ('00' + str).substr(str.length)
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)
},
randomNum(minNum: number, maxNum: number) {
return Math.floor(Math.random() * maxNum) + minNum
},
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
},
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

@ -5,10 +5,18 @@
* @descriptionindex
*/
import { RouteLocationRaw } from 'vue-router';
export { type Auths, type modelsSimpleAuth, NewAuths, R } from './auth'
export interface MenuLink {
title: string;
caption?: string;
to?: RouteLocationRaw;
link?: string;
icon?: string;
}
export interface modelsBread {
Index: number

@ -0,0 +1,28 @@
{
"name": "@veypi/oaer",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"mitt": "^3.0.0",
"vue": "^3.2.20"
},
"devDependencies": {
"@veypi/one-icon": "2.0.6",
"@vitejs/plugin-vue": "^1.9.3",
"autoprefixer": "^9.8.8",
"axios": "^0.24.0",
"js-base64": "^3.7.2",
"naive-ui": "^2.19.11",
"postcss": "^7.0.39",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@2.1.0",
"typescript": "^4.4.3",
"vite": "^2.6.4",
"vue-tsc": "^0.3.0"
},
"author": "veypi",
"license": "MIT"
}

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

@ -0,0 +1,86 @@
import axios from 'axios'
import evt from '../evt'
import {Cfg} from './setting'
function getQueryVariable(variable: string) {
let query = window.location.search.substring(1)
let vars = query.split('&')
for (let i = 0; i < vars.length; i++) {
let pair = vars[i].split('=')
if (pair[0] == variable) {
return pair[1]
}
}
return ''
}
function baseRequests(url: string, method: any = 'GET', query: any, data: any, success: any, fail?: Function, header?: any) {
let headers = {
auth_token: Cfg.token.value || decodeURIComponent(getQueryVariable('token') as string),
uuid: Cfg.uuid.value,
}
if (header) {
headers = Object.assign(headers, header)
}
return axios({
url: url,
params: query,
data: data,
method: method,
headers: headers,
}).then((res: any) => {
if ('auth_token' in res.headers) {
Cfg.token.value = res.headers.auth_token
}
if ('redirect_url' in res.headers) {
window.location.href = res.headers.redirect_url
return
}
if (method === 'HEAD') {
success(res.headers)
} else {
success(res.data)
}
})
.catch((e: any) => {
if (e.response && e.response.status === 401) {
evt.emit('logout')
return
}
console.log(e)
if (e.response && e.response.status === 500) {
return
}
if (typeof fail === 'function') {
fail(e.response)
} else if (e.response && e.response.status === 400) {
console.log(400)
} else {
console.log(e.request)
}
})
}
const ajax = {
get(url: '', data = {}, success = {}, fail?: Function, header?: any) {
return baseRequests(url, 'GET', data, {}, success, fail, header)
},
head(url: '', data = {}, success = {}, fail?: Function, header?: any) {
return baseRequests(url, 'HEAD', data, {}, success, fail, header)
},
delete(url: '', data = {}, success = {}, fail?: Function, header?: any) {
return baseRequests(url, 'DELETE', data, {}, success, fail, header)
},
post(url: '', data = {}, success = {}, fail?: Function, header?: any) {
return baseRequests(url, 'POST', {}, data, success, fail, header)
},
put(url: '', data = {}, success = {}, fail?: Function, header?: any) {
return baseRequests(url, 'PUT', {}, data, success, fail, header)
},
patch(url: '', data = {}, success = {}, fail?: Function, header?: any) {
return baseRequests(url, 'PATCH', {}, data, success, fail, header)
},
}
export default ajax

@ -0,0 +1,31 @@
/*
* @name: app
* @author: veypi <i@veypi.com>
* @date: 2021-11-17 14:44
* @descriptionap
* @update: 2021-11-17 14:44
*/
import {Interface} from './interface'
import ajax from './ajax'
import {Cfg} from './setting'
export default {
local: () => Cfg.BaseUrl() + 'app/',
get(uuid: string) {
return new Interface(ajax.get, this.local() + uuid)
},
list() {
return new Interface(ajax.get, this.local())
},
user(uuid: string) {
if (uuid === '') {
uuid = '-'
}
return {
local: () => this.local() + uuid + '/user/',
list(uid: number) {
return new Interface(ajax.get, this.local() + uid)
},
}
},
}

@ -0,0 +1,18 @@
/*
* Copyright (C) 2019 light <veypi@light-laptop>
*
* Distributed under terms of the MIT license.
*/
import user from './user'
import app from './app'
import {Cfg} from './setting'
const api = {
user: user,
app: app,
}
export {api, Cfg}
export default api

@ -0,0 +1,65 @@
import evt from '../evt'
export type SuccessFunction<T> = (e: any) => void;
export type FailedFunction<T> = (e: any) => void;
const Code = {
42011: '无操作权限',
22031: '资源不存在 或 您无权操作该资源',
}
export class Interface {
private readonly method: Function
private readonly api: string
private readonly data: any
private readonly header: any
constructor(method: Function, api: string, data?: any, headers?: any) {
this.method = method
this.api = api
this.data = data
this.header = headers
}
Start(success?: SuccessFunction<any>, fail?: FailedFunction<any>) {
const newFail = function (data: any) {
if (data) {
if (data.code === 40001) {
// no login
evt.emit('logout')
return
// @ts-ignore
} else if (data.code === 42011 && window.$msg) {
// @ts-ignore
window.$msg.warning('无权限')
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
if (data && data.code && Code[data.code]) {
}
if (fail) {
fail(data.err)
// @ts-ignore
} else if (window.$msg) {
// @ts-ignore
window.$msg.warning(data.err)
}
}
const newSuccess = function (data: any) {
if (Number(data.status) === 1) {
if (success) {
success(data.content)
// @ts-ignore
} else if (window.$msg) {
// @ts-ignore
window.$msg.warning('ok')
}
} else {
newFail(data)
}
}
this.method(this.api, this.data, newSuccess, newFail, this.header)
}
}

@ -0,0 +1,22 @@
/*
* @name: setting
* @author: veypi <i@veypi.com>
* @date: 2021-11-17 15:45
* @descriptionsetting
* @update: 2021-11-17 15:45
*/
import {ref} from 'vue'
export let Cfg = {
token: ref(''),
uuid: ref(''),
host: ref(''),
prefix: '/api/',
BaseUrl() {
return this.host.value + this.prefix
},
userFileUrl() {
return (this.host.value || window.location.href) + '/file/usr/'
},
}

@ -0,0 +1,33 @@
import {Base64} from 'js-base64'
import {Interface} from './interface'
import ajax from './ajax'
import {Cfg} from './setting'
export default {
local: () => Cfg.BaseUrl() + 'user/',
register(username: string, password: string, prop?: any) {
const data = Object.assign({
username: username,
password: Base64.encode(password),
}, prop)
return new Interface(ajax.post, this.local(), data)
},
login(username: string, password: string) {
return new Interface(ajax.head, this.local() + username, {
UidType: 'username',
password: Base64.encode(password),
})
},
search(q: string) {
return new Interface(ajax.get, this.local(), {username: q})
},
get(id: number) {
return new Interface(ajax.get, this.local() + id)
},
list() {
return new Interface(ajax.get, this.local())
},
update(id: number, props: any) {
return new Interface(ajax.patch, this.local() + id, props)
},
}

@ -0,0 +1 @@
!function(t){var e,n,o,i,c,d='<svg><symbol id="icon-close" viewBox="0 0 1024 1024"><path d="M176.661601 817.172881C168.472798 825.644055 168.701706 839.149636 177.172881 847.338438 185.644056 855.527241 199.149636 855.298332 207.338438 846.827157L826.005105 206.827157C834.193907 198.355983 833.964998 184.850403 825.493824 176.661601 817.02265 168.472798 803.517069 168.701706 795.328267 177.172881L176.661601 817.172881Z" ></path><path d="M795.328267 846.827157C803.517069 855.298332 817.02265 855.527241 825.493824 847.338438 833.964998 839.149636 834.193907 825.644055 826.005105 817.172881L207.338438 177.172881C199.149636 168.701706 185.644056 168.472798 177.172881 176.661601 168.701706 184.850403 168.472798 198.355983 176.661601 206.827157L795.328267 846.827157Z" ></path></symbol><symbol id="icon-logout" viewBox="0 0 1024 1024"><path d="M856.8 389.8c-18.9-44.7-45.9-84.8-80.4-119.2-18.9-18.9-39.5-35.6-61.7-49.9-10-6.5-23.3 0.6-23.3 12.6 0 5.1 2.6 9.9 6.9 12.6 95 61.5 158 168.5 158 289.8 0 190.3-154.8 345-345 345s-345-154.8-345-345c0-122.4 64.1-230.2 160.5-291.4 4.4-2.8 7-7.6 7-12.7 0-11.8-13.1-19.1-23.1-12.8-23.2 14.7-44.8 32-64.6 51.8-34.4 34.4-61.5 74.5-80.4 119.2-19.6 46.2-29.5 95.3-29.5 146s9.9 99.7 29.5 146c18.9 44.7 45.9 84.8 80.4 119.2 34.4 34.4 74.5 61.5 119.2 80.4 46.2 19.6 95.3 29.5 146 29.5 50.6 0 99.7-9.9 146-29.5 44.7-18.9 84.8-45.9 119.2-80.4s61.5-74.5 80.4-119.2c19.6-46.2 29.5-95.3 29.5-146s-10-99.8-29.6-146z" fill="" ></path><path d="M512 431.1c-8.8 0-16-7.2-16-16V98.2c0-8.8 7.2-16 16-16s16 7.2 16 16V415c0 8.9-7.2 16.1-16 16.1z" fill="" ></path></symbol></svg>',s=(s=document.getElementsByTagName("script"))[s.length-1].getAttribute("data-injectcss"),l=function(t,e){e.parentNode.insertBefore(t,e)};if(s&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>")}catch(t){console&&console.log(t)}}function a(){c||(c=!0,o())}function r(){try{i.documentElement.doScroll("left")}catch(t){return void setTimeout(r,50)}a()}e=function(){var t,e;(e=document.createElement("div")).innerHTML=d,d=null,(t=e.getElementsByTagName("svg")[0])&&(t.setAttribute("aria-hidden","true"),t.style.position="absolute",t.style.width=0,t.style.height=0,t.style.overflow="hidden",e=t,(t=document.body).firstChild?l(e,t.firstChild):t.appendChild(e))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(e,0):(n=function(){document.removeEventListener("DOMContentLoaded",n,!1),e()},document.addEventListener("DOMContentLoaded",n,!1)):document.attachEvent&&(o=e,i=t.document,c=!1,r(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,a())})}(window);

@ -0,0 +1,36 @@
<template>
<div class="w-full px-3">
<div class="h-16 flex justify-between items-center">
<span style="color: #777">我的应用</span>
<span @click="Cfg.host.value?goto(Cfg.host.value):redirect('/')" class="cursor-pointer"
style="color:#f36828">应用中心</span>
</div>
<div class="grid grid-cols-5">
<template v-for="(ap,ai) of apps"
:key="ai">
<div class="mx-2" @click="redirect(ap.Host)" v-if="ap.UUID !== Cfg.uuid.value">
<n-avatar v-if="ap.Icon" size="2" :src="Cfg.host.value+ ap.Icon" round></n-avatar>
</div>
</template>
</div>
<hr class="mt-10" style="border:none;border-top:1px solid #777;">
</div>
</template>
<script lang="ts" setup>
import {modelsApp} from '../models'
import {Cfg} from '../api'
let props = withDefaults(defineProps<{
apps: modelsApp[]
}>(), {})
function goto(url: string) {
window.open(url, '_blank')
}
function redirect(url: string) {
window.location.href = url
}
</script>
<style scoped>
</style>

@ -0,0 +1,47 @@
<template>
<div class="w-full px-3">
<div class="h-16 flex justify-between items-center">
<span style="color: #777">
我的云盘
</span>
<span class="cursor-pointer" style="color:#f36828">文件中心</span>
</div>
<div class="">
{{ usr.Used }} KB / {{ usr.Space }} GB
<n-progress type="line" color="#0f0" rail-color="#fff" :percentage="1" indicator-text-color="#f00" />
</div>
<div class="flex justify-center">
<n-button @click="showModal = true" type="primary">获取挂载链接</n-button>
</div>
<n-modal v-model:show="showModal">
<n-card style="width: 600px;" title="云盘挂载地址" :bordered="false" size="huge">
<template #header-extra>复制</template>
{{ Cfg.userFileUrl() }}
<template #footer> 挂载说明</template>
</n-card>
</n-modal>
<hr class="mt-10" style="border:none;border-top:1px solid #777;">
</div>
</template>
<script lang="ts" setup>
import { createClient } from 'webdav'
import { modelsUser } from '../models'
import { onMounted, ref } from 'vue'
import { Cfg } from '../api'
let showModal = ref(false)
let props = withDefaults(defineProps<{
usr: modelsUser
}>(), {})
let client = createClient('http://127.0.0.1:4001/file/usr/',
{ headers: { auth_token: localStorage.getItem('auth_token') as string } })
onMounted(() => {
client.stat('').then((e) => {
console.log(e)
}).catch((e) => {
console.log(e)
})
})
</script>
<style scoped></style>

@ -0,0 +1,7 @@
<template>
<div></div>
</template>
<script lang="js" setup>
</script>
<style scoped>
</style>

@ -0,0 +1,12 @@
/*
* @name: index
* @author: veypi <i@veypi.com>
* @date: 2021-12-18 14:24
* @descriptionindex
*/
import mitt from 'mitt'
const emitter = mitt()
export default emitter

@ -0,0 +1,91 @@
<template>
<div>
<div @click="setValue(true)">
<slot>
</slot>
</div>
<div @click.self="setValue(false)" class="core" style="height: 100vh;width: 100vw;" v-if="props.modelValue">
<div style="height: 100%; width: 300px" class="core-right">
<transition appear enter-active-class="animate__slideInRight">
<div class="flex right-title animate__animated animate__faster px-3">
<div class="flex-grow text-left" style="font-size: 1.2rem">
<slot name="title"></slot>
</div>
<div class="flex-grow-0 flex items-center h-full">
<OneIcon @click="setValue(false)" color="#fff" style="font-size: 24px">close</OneIcon>
</div>
</div>
</transition>
<div class="right-main">
<transition appear enter-active-class="animate__slideInDown">
<div class="right-main-core animate__animated animate__faster"
:style="{'background': backgound}">
<slot name="main"></slot>
</div>
</transition>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import {OneIcon} from '@veypi/one-icon'
import {computed, watch} from 'vue'
let emits = defineEmits<{
(e: 'update:modelValue', v: boolean): void
}>()
let props = withDefaults(defineProps<{
isDark?: boolean,
modelValue?: boolean
}>(), {})
let backgound = computed(() => {
return props.isDark ? '#222' : '#eee'
})
watch(props, () => {
})
function setValue(b: boolean) {
emits('update:modelValue', b)
}
</script>
<style scoped>
.core {
position: fixed;
left: 0;
top: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 100;
}
.core-right {
position: absolute;
right: 0;
top: 0;
}
.right-main {
width: 100%;
height: calc(100% - 50px);
overflow: hidden;
}
.right-main-core {
height: 100%;
width: 100%;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
--animate-duration: 400ms;
}
.right-title {
width: 100%;
height: 50px;
line-height: 50px;
background: linear-gradient(90deg, #f74d22, #fa9243);
}
</style>

@ -0,0 +1,24 @@
/*
* @name: index
* @author: veypi <i@veypi.com>
* @date: 2021-12-18 13:16
* @descriptionindex
*/
import { App } from 'vue'
import OAer from './main.vue'
import './assets/icon.js'
import { Cfg, api } from './api'
export { OAer, Cfg, api }
export default {
installed: false,
install(vue: App, options?: any): void {
if (this.installed) {
return
}
this.installed = true
vue.component('OAer', OAer)
},
}

@ -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,121 @@
<template>
<BaseFrame v-model="shown" :is-dark="isDark">
<template #title>
{{ self.Name }}
</template>
<div class="flex justify-center items-center">
<n-avatar style="--color: none" :src="Cfg.host.value + usr.Icon"
round></n-avatar>
</div>
<template #main>
<div style="height: 100%">
<div style="height: calc(100% - 50px)">
<div class="w-full px-3">
<div class="h-16 flex justify-between items-center">
<span style="color: #777">我的账户</span>
<span @click="$router.push({name: 'user_setting'});shown=false" class="cursor-pointer"
style="color:#f36828">账户中心</span>
</div>
<div class="grid grid-cols-4 gap-4 h-20">
<div class="flex items-center justify-center">
<n-avatar size="50" :src="Cfg.host.value+ usr.Icon" round></n-avatar>
</div>
<div class="col-span-2 text-xs grid grid-cols-1 items-center text-left" style="">
<span>昵称: &ensp;&ensp; {{ usr.Nickname }}</span>
<span>账户: &ensp;&ensp; {{ usr.Username }}</span>
<span>邮箱: &ensp;&ensp; {{ usr.Email }}</span>
</div>
<div class="">123</div>
</div>
<hr class="mt-10" style="border:none;border-top:1px solid #777;">
</div>
<File :usr="usr"></File>
<Apps :apps="ofApps"></Apps>
</div>
<hr style="border:none;border-top:2px solid #777;">
<div style="height: 48px">
<div @click="evt.emit('logout')"
class="w-full h-full flex justify-center items-center cursor-pointer transition duration-500 ease-in-out transform hover:scale-125">
<OneIcon :color="isDark?'#eee': '#333'" class="inline-block" style="font-size: 24px;">
logout
</OneIcon>
<div>
退出登录
</div>
</div>
</div>
</div>
</template>
</BaseFrame>
</template>
<script lang="ts" setup>
import BaseFrame from './frame.vue'
import Apps from './components/app.vue'
import File from './components/file.vue'
import {OneIcon} from '@veypi/one-icon'
import {computed, onMounted, ref, watch} from 'vue'
import {decode} from 'js-base64'
import {api, Cfg} from './api'
import evt from './evt'
import {modelsApp, modelsUser} from './models'
let shown = ref(false)
let emits = defineEmits<{
(e: 'logout'): void
(e: 'load', u: modelsUser): void
}>()
let props = withDefaults(defineProps<{
isDark?: boolean
}>(), {
isDark: false,
})
onMounted(() => {
fetchUserData()
})
let usr = ref<modelsUser>({} as modelsUser)
let ofApps = ref<modelsApp[]>([])
let self = ref<modelsApp>({} as modelsApp)
let token = computed(() => Cfg.token.value)
watch(token, () => {
fetchUserData()
})
function fetchUserData() {
let token = Cfg.token.value?.split('.')
if (!token || token.length !== 3) {
return false
}
let data = JSON.parse(decode(token[1]))
if (data.ID > 0) {
api.user.get(data.ID).Start(e => {
usr.value = e
ofApps.value = []
for (let v of e.Apps) {
if (v.Status === 'ok') {
ofApps.value.push(v.App)
}
if (v.App.UUID === Cfg.uuid.value) {
self.value = v.App
}
}
emits('load', e)
}, e => {
console.log(e)
evt.emit('logout')
})
} else {
evt.emit('logout')
}
}
evt.on('logout', () => {
emits('logout')
})
</script>
<style scoped>
</style>

@ -0,0 +1,126 @@
/*
* @name: index
* @author: veypi <i@veypi.com>
* @date: 2021-11-18 17:36
* @descriptionindex
*/
export interface modelsBread {
Index: number
Name: string
Type?: string
RName: string
RParams?: any
RQuery?: any
}
export enum AppJoin {
Auto = 0,
Disabled = 1,
Applying = 2,
}
export interface App {
created: string
updated: string
delete_flag: boolean
des: string
hide: boolean
icon: string
id: string
join_method: AppJoin
name: string
redirect: string
role_id: string
status: number
user_count: number
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 interface modelsUser {
// Index 前端缓存
Index?: number
Apps: App[]
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 modelsSimpleAuth {
Level: number
RID: string
RUID: string
}
export interface modelsAuth {
App?: App
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?: App
AppUUID: string
Auths: null
CreatedAt: string
DeletedAt: null
ID: number
Name: string
Tag: string
UpdatedAt: string
UserCount: number
}
export interface modelsResource {
App?: App
AppUUID: string
CreatedAt: string
DeletedAt: null
Des: string
ID: number
Name: string
UpdatedAt: string
}

@ -0,0 +1,72 @@
import {darkTheme} from 'naive-ui/lib/themes'
import {BuiltInGlobalTheme} from 'naive-ui/lib/themes/interface'
import {lightTheme} from 'naive-ui/lib/themes/light'
import {ref} from 'vue'
import {useOsTheme, GlobalThemeOverrides} from 'naive-ui'
interface builtIn extends BuiltInGlobalTheme {
overrides: GlobalThemeOverrides
me: {
lightBox: string,
lightBoxShadow: string
}
}
let light = lightTheme as builtIn
let dark = darkTheme as builtIn
let intputNone = {
color: 'url(0) no-repeat',
colorFocus: 'url(0) no-repeat',
colorFocusWarning: 'url(0) no-repeat',
colorFocusError: 'url(0) no-repeat',
}
light.overrides = {
Input: Object.assign({}, intputNone),
}
dark.overrides = {
Input: Object.assign({
border: '1px solid #aaa',
}, intputNone),
}
light.common.cardColor = '#f4f4f4'
light.common.bodyColor = '#eee'
dark.common.bodyColor = '#2e2e2e'
light.me = {
lightBox: '#f4f4f4',
lightBoxShadow: '18px 18px 36px #c6c6c6, -18px -18px 36px #fff',
}
dark.me = {
lightBox: '#2e2e2e',
lightBoxShadow: '21px 21px 42px #272727, -21px -21px 42px #353535',
}
export const OsThemeRef = useOsTheme()
let theme = 'light'
export let Theme = ref(light)
export let IsDark = ref(false)
function change(t: string) {
if (t === 'dark') {
theme = 'dark'
Theme.value = dark
} else {
theme = 'light'
Theme.value = light
}
IsDark.value = theme === 'dark'
}
export function ChangeTheme() {
if (IsDark.value) {
change('light')
} else {
change('dark')
}
}
if (OsThemeRef.value === 'dark') {
change('dark')
}

@ -0,0 +1,11 @@
module.exports = {
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()]
})

File diff suppressed because it is too large Load Diff

@ -0,0 +1,37 @@
<template>
<div class="flex justify-center items-center w-full h-full">
<div class="text-center text-xl">
<q-icon style="font-size: 200px" name="svguse:#icon-404"></q-icon>
<div>
路径失效啦! {{ count }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter, useRoute } from 'vue-router'
import { onBeforeUnmount, onMounted, ref } from "vue";
const route = useRoute()
const router = useRouter()
let count = ref(5)
let timer = ref()
onMounted(() => {
timer.value = setInterval(() => {
count.value--
if (count.value === 0) {
router.back()
// router.push('/')
clearInterval(timer.value)
}
}, 1000)
})
onBeforeUnmount(() => {
if (timer.value) {
clearInterval(timer.value)
}
})
</script>
<style scoped></style>

@ -0,0 +1,23 @@
<!--
* AppHome.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-04 15:34
* Distributed under terms of the MIT license.
-->
<template>
<div>
{{ app }}
</div>
</template>
<script lang="ts" setup>
import { inject } from 'vue';
import { modelsApp } from 'src/models';
let app = inject('app') as modelsApp
</script>
<style scoped></style>

@ -4,7 +4,7 @@
<div class="flex justify-between">
<h1 class="page-h1">我的应用</h1>
<div class="my-5 mr-10">
<q-btn @click="new_flag = true" v-if="user.auth.Get(R.App, '').CanCreate()">
<q-btn outline @click="new_flag = true" v-if="user.auth.Get(R.App, '').CanCreate()">
</q-btn>
</div>
</div>
@ -31,18 +31,16 @@
<q-card-section>
<q-form @submit="create_new">
<q-input label="应用名" v-model="temp_app.name" :rules="rules.name"></q-input>
<q-field label="icon" stack-label>
<template v-slot:control>
<uploader url="test.ico" @success="(e) => {
temp_app.icon = e;
}
">
<q-avatar size="xl" round>
<img :src="temp_app.icon">
</q-avatar>
</uploader>
</template>
</q-field>
<div class="flex justify-center my-4 items-center" label='icon'>
<uploader @success="temp_app.icon = $event">
<q-avatar>
<img :src="temp_app.icon">
</q-avatar>
</uploader>
<q-icon class="ml-2" size="1rem" name='autorenew' @click="temp_app.icon = rand_icon()"></q-icon>
</div>
<q-separator></q-separator>
<div class="flex justify-end mt-8">
<q-btn class="mx-3" @click="new_flag = false">取消</q-btn>
@ -64,6 +62,7 @@ import AppCard from 'components/app.vue'
import { useUserStore } from 'src/stores/user';
import { R } from 'src/models';
import uploader from 'components/uploader'
import { util } from 'src/libs';
let user = useUserStore()
@ -91,14 +90,14 @@ function getApps() {
);
}
onMounted(() => {
getApps();
});
const rand_icon = () => {
return "/media/icon/sign/scenery-" + util.randomNum(1, 20) + ".png"
}
let new_flag = ref(false);
let temp_app = ref({
name: "",
icon: "",
icon: rand_icon()
});
let rules = {
name: [
@ -110,13 +109,15 @@ function create_new() {
api.app.create(temp_app.value.name, temp_app.value.icon).then((e:
modelsApp) => {
console.log(e)
// e.Status = "ok";
// ofApps.value.push(e);
ofApps.value.push(e);
msg.Info("创建成功");
new_flag.value = false;
}).catch(e => {
msg.Warn("创建失败: " + e);
})
}
onMounted(() => {
getApps();
});
</script>

@ -11,6 +11,17 @@ declare module 'vue-router' {
checkAuth?: (a: Auths, r?: RouteLocationNormalized) => boolean
}
}
function loadcomponents(path: string, name: string, main: string) {
return {
path: path,
name: name,
components: {
default: () => import("../pages/" + main + ".vue"),
}
}
}
const routes: RouteRecordRaw[] = [
{
@ -19,22 +30,31 @@ const routes: RouteRecordRaw[] = [
meta: {
requiresAuth: true,
},
redirect: 'home',
children: [
loadcomponents('home', 'home', 'IndexPage'),
loadcomponents('user', 'user', '404'),
loadcomponents('settings', 'settings', '404'),
{
path: '',
component: () => import('pages/IndexPage.vue')
path: 'app/:id?',
component: () => import("../layouts/AppLayout.vue"),
redirect: { name: 'app.home' },
children: [
loadcomponents('home', 'app.home', 'IndexPage'),
loadcomponents('user', 'app.user', 'AppHome'),
]
}
],
},
{
path: '/login/:uuid?',
name: 'login',
component: () => import('pages/login.vue'),
component: () => import('../pages/login.vue'),
},
{
path: '/register/:uuid?',
name: 'register',
component: () => import('pages/register.vue'),
component: () => import('../pages/register.vue'),
},
@ -42,7 +62,7 @@ const routes: RouteRecordRaw[] = [
// but you can also remove it
{
path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue'),
component: () => import('../pages/404.vue'),
},
];

@ -7,14 +7,19 @@
import { defineStore } from 'pinia';
import { useQuasar } from 'quasar'
export const useAppStore = defineStore('app', {
state: () => ({
id: '',
is_dark: false,
title: '',
}),
getters: {
},
actions: {
toggle_mode() {
}
},
});

@ -0,0 +1,47 @@
/*
* menu.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-04 21:51
* Distributed under terms of the MIT license.
*/
import { defineStore } from 'pinia';
import { MenuLink } from 'src/models';
const defaultLinks: MenuLink[] = [
{
title: '应用中心',
caption: '',
icon: 'apps',
to: { name: 'home' }
},
{
title: '用户管理',
caption: 'oa.veypi.com',
icon: 'person',
to: { name: 'user' }
},
{
title: '设置',
caption: '',
icon: 'settings',
to: { name: 'settings' }
},
]
export const useMenuStore = defineStore('menu', {
state: () => ({
list: defaultLinks as MenuLink[],
}),
getters: {
},
actions: {
set(links: MenuLink[]) {
this.list = links
},
load_default() {
this.list = defaultLinks
}
},
});

@ -465,6 +465,13 @@
resolved "https://registry.yarnpkg.com/@veypi/msg/-/msg-0.1.0.tgz#2ebe899527a11ed11f68c2c96f468cfcc66ad3d4"
integrity sha512-58dj5nnpHsxaiK5sbPiDK5t8OF4uvN+kAmWhU0BRAgXHpkxkZNZ8rn7hXvSVybG1BbM8EuMNkq0lIxGYNKl8aw==
"@veypi/one-icon@2":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@veypi/one-icon/-/one-icon-2.0.6.tgz#158c692971848524cd59db1a61d88805d2e45646"
integrity sha512-ldfRE8vDSqZEFk+94wqieWP4s1Mz1EDG1VhXmckWI0cat2RT/Kk9hcICImkLhsOmhNRX7nwxSU4UbUiJVix/Jw==
dependencies:
vue "^3.2.20"
"@vitejs/plugin-vue@^2.2.0":
version "2.3.4"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.3.4.tgz#966a6279060eb2d9d1a02ea1a331af071afdcf9e"
@ -2433,6 +2440,11 @@ minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
mitt@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@ -3447,7 +3459,7 @@ vue-router@^4.0.0:
dependencies:
"@vue/devtools-api" "^6.5.0"
vue@^3.0.0:
vue@^3.0.0, vue@^3.2.20:
version "3.3.4"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.3.4.tgz#8ed945d3873667df1d0fcf3b2463ada028f88bd6"
integrity sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==

Loading…
Cancel
Save