mirror of https://github.com/veypi/OneAuth.git
oaweb quasar and oab rust update
parent
ae0ede106a
commit
5d71525ce7
@ -1,16 +1,37 @@
|
||||
module github.com/veypi/OneAuth
|
||||
|
||||
go 1.16
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/json-iterator/go v1.1.10
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/olivere/elastic/v7 v7.0.29
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
github.com/veypi/OneBD v0.4.3
|
||||
github.com/veypi/utils v0.3.1
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
|
||||
gorm.io/driver/mysql v1.0.5
|
||||
gorm.io/driver/sqlite v1.1.4
|
||||
gorm.io/gorm v1.21.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
|
||||
github.com/go-sql-driver/mysql v1.5.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/kardianos/service v1.1.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.5 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rs/zerolog v1.17.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
)
|
||||
|
||||
replace github.com/veypi/OneBD v0.4.3 => ../OceanCurrent/OneBD
|
||||
|
@ -0,0 +1,39 @@
|
||||
//
|
||||
// appuser.rs
|
||||
// Copyright (C) 2023 veypi <i@veypi.com>
|
||||
// 2023-09-30 23:11
|
||||
// Distributed under terms of the MIT license.
|
||||
//
|
||||
|
||||
use actix_web::{delete, get, post, web, Responder};
|
||||
use proc::access_read;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
|
||||
use crate::{models, Error, Result, CONFIG};
|
||||
|
||||
#[get("/app/{aid}/user/{uid}")]
|
||||
#[access_read("app")]
|
||||
pub async fn get(params: web::Path<(String, String)>) -> Result<impl Responder> {
|
||||
let (mut aid, mut uid) = params.into_inner();
|
||||
if uid == "-" {
|
||||
uid = "".to_string();
|
||||
}
|
||||
if aid == "-" {
|
||||
aid = "".to_string();
|
||||
}
|
||||
let sql = format!("select * from app_user where");
|
||||
info!("111|{}|{}|", aid, uid);
|
||||
if uid.is_empty() && aid.is_empty() {
|
||||
Err(Error::Missing("uid or aid".to_string()))
|
||||
} else {
|
||||
let s = sqlx::query_as::<_, models::AppUser>(
|
||||
"select * from app_user where app_id = ? and user_id = ?",
|
||||
)
|
||||
.bind(aid)
|
||||
.bind(uid)
|
||||
.fetch_all(CONFIG.db())
|
||||
.await?;
|
||||
Ok(web::Json(s))
|
||||
}
|
||||
}
|
@ -1,21 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title><%= productName %></title>
|
||||
<title>
|
||||
OA
|
||||
</title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="description" content="<%= productDescription %>">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="msapplication-tap-highlight" content="no">
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
|
||||
<meta name="viewport"
|
||||
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
|
||||
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
|
||||
<link rel="icon" type="image/ico" href="favicon.ico">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id='v-msg'></div>
|
||||
<!-- quasar:entry-point -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 3.6 KiB |
File diff suppressed because one or more lines are too long
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* app.ts
|
||||
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||
* 2023-09-30 17:31
|
||||
* Distributed under terms of the MIT license.
|
||||
*/
|
||||
|
||||
|
||||
import ajax from './axios'
|
||||
|
||||
export default {
|
||||
local: './app/',
|
||||
self() {
|
||||
return ajax.get(this.local, { option: 'oa' })
|
||||
},
|
||||
getKey(uuid: string) {
|
||||
return ajax.get(this.local + uuid, { option: 'key' })
|
||||
},
|
||||
create(name: string, icon: string) {
|
||||
return ajax.post(this.local, { name, icon })
|
||||
},
|
||||
get(uuid: string) {
|
||||
return ajax.get(this.local + uuid)
|
||||
},
|
||||
list() {
|
||||
return ajax.get(this.local)
|
||||
},
|
||||
update(uuid: string, props: any) {
|
||||
return ajax.patch(this.local + uuid, props)
|
||||
},
|
||||
user(uuid: string) {
|
||||
if (uuid === '') {
|
||||
uuid = '-'
|
||||
}
|
||||
return {
|
||||
local: this.local + uuid + '/user/',
|
||||
list(uid: number) {
|
||||
return ajax.get(this.local + uid)
|
||||
},
|
||||
add(uid: number) {
|
||||
return ajax.post(this.local + uid)
|
||||
},
|
||||
update(uid: number, status: string) {
|
||||
return ajax.patch(this.local + uid, { status })
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* index.ts
|
||||
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||
* 2023-09-22 20:17
|
||||
* Distributed under terms of the MIT license.
|
||||
*/
|
||||
|
||||
import app from "./app";
|
||||
import token from "./token";
|
||||
import user from "./user";
|
||||
|
||||
|
||||
|
||||
|
||||
const api = {
|
||||
user: user,
|
||||
app: app,
|
||||
token: token
|
||||
}
|
||||
|
||||
|
||||
export default api;
|
||||
|
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* token.ts
|
||||
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||
* 2023-09-30 17:37
|
||||
* Distributed under terms of the MIT license.
|
||||
*/
|
||||
|
||||
|
||||
import ajax from './axios'
|
||||
|
||||
export default (uuid: string) => {
|
||||
return {
|
||||
local: './app/' + uuid + '/token/',
|
||||
get() {
|
||||
return ajax.get(this.local)
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
|
||||
/*
|
||||
* user.ts
|
||||
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||
* 2023-09-22 20:18
|
||||
* Distributed under terms of the MIT license.
|
||||
*/
|
||||
|
||||
|
||||
import { Base64 } from 'js-base64'
|
||||
import ajax from './axios'
|
||||
|
||||
export default {
|
||||
local: './user/',
|
||||
register(username: string, password: string, prop?: any) {
|
||||
const data = Object.assign({
|
||||
username: username,
|
||||
password: Base64.encode(password),
|
||||
}, prop)
|
||||
return ajax.post(this.local, data)
|
||||
},
|
||||
login(username: string, password: string) {
|
||||
return ajax.head(this.local + username, {
|
||||
typ: 'username',
|
||||
password: Base64.encode(password),
|
||||
})
|
||||
},
|
||||
search(q: string) {
|
||||
return ajax.get(this.local, { username: q })
|
||||
},
|
||||
get(id: number) {
|
||||
return ajax.get(this.local + id)
|
||||
},
|
||||
list() {
|
||||
return ajax.get(this.local)
|
||||
},
|
||||
update(id: number, props: any) {
|
||||
return ajax.patch(this.local + id, props)
|
||||
},
|
||||
}
|
||||
|
@ -1,31 +0,0 @@
|
||||
import { boot } from 'quasar/wrappers';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
$axios: AxiosInstance;
|
||||
$api: AxiosInstance;
|
||||
}
|
||||
}
|
||||
|
||||
// Be careful when using SSR for cross-request state pollution
|
||||
// due to creating a Singleton instance here;
|
||||
// If any client changes this (global) instance, it might be a
|
||||
// good idea to move this instance creation inside of the
|
||||
// "export default () => {}" function below (which runs individually
|
||||
// for each client)
|
||||
const api = axios.create({ baseURL: 'https://api.example.com' });
|
||||
|
||||
export default boot(({ app }) => {
|
||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
||||
|
||||
app.config.globalProperties.$axios = axios;
|
||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
||||
// so you won't necessarily have to import axios in each vue file
|
||||
|
||||
app.config.globalProperties.$api = api;
|
||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
||||
// so you can easily perform requests against your app's API
|
||||
});
|
||||
|
||||
export { api };
|
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* pack.ts
|
||||
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||
* 2023-09-26 20:38
|
||||
* Distributed under terms of the MIT license.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import { boot } from 'quasar/wrappers'
|
||||
import '@veypi/msg/index.css'
|
||||
import '../assets/icon.js'
|
||||
|
||||
// "async" is optional;
|
||||
// more info on params: https://v2.quasar.dev/quasar-cli/boot-files
|
||||
export default boot(async (/* { app, router, ... } */) => {
|
||||
// something to do
|
||||
})
|
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="core rounded-2xl p-3">
|
||||
<div class="grid gap-4 grid-cols-5">
|
||||
<div class="col-span-2">
|
||||
<q-avatar style="--color: none" @click="Go" round size="xl" :icon="core.icon">
|
||||
</q-avatar>
|
||||
</div>
|
||||
<div class="col-span-3 grid grid-cols-1 items-center text-left">
|
||||
<div class="h-10 flex items-center text-2xl italic font-bold">
|
||||
{{ core.name }}
|
||||
</div>
|
||||
<span class="truncate">{{ core.des }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import msg from "@veypi/msg";
|
||||
import api from "src/boot/api";
|
||||
import { AUStatus, modelsApp, modelsAppUser } from "src/models";
|
||||
import { useUserStore } from "src/stores/user";
|
||||
import { useRouter } from "vue-router"
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
|
||||
let props = withDefaults(defineProps<{
|
||||
core: modelsApp
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
|
||||
|
||||
|
||||
function Go() {
|
||||
switch (props.core.au.status) {
|
||||
case AUStatus.OK:
|
||||
router.push({ name: "app.main", params: { uuid: props.core.UUID } });
|
||||
return;
|
||||
case AUStatus.Applying:
|
||||
msg.Info("请等待管理员审批进入");
|
||||
return;
|
||||
case AUStatus.Deny:
|
||||
msg.Warn("进入申请未通过");
|
||||
return;
|
||||
case AUStatus.Disabled:
|
||||
msg.Warn("已被禁止使用");
|
||||
return;
|
||||
}
|
||||
// api.app.user(props.core.id).add(useUserStore().id).then(e => {
|
||||
// console.log(e)
|
||||
// })
|
||||
// api.app
|
||||
// .user(props.core.UUID)
|
||||
// .add(store.state.user.id)
|
||||
// .Start(
|
||||
// (e) => {
|
||||
// bar.finish();
|
||||
// if (e.Status === "ok") {
|
||||
// router.push({ name: "app.main", params: { uuid: props.core.UUID } });
|
||||
// return;
|
||||
// }
|
||||
// props.core.UserStatus = e.Status;
|
||||
// msg.info("已发起加入申请");
|
||||
// },
|
||||
// (e) => {
|
||||
// msg.warning("加入失败: " + e);
|
||||
// bar.error();
|
||||
// }
|
||||
// );
|
||||
// return;
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.core {
|
||||
width: 256px;
|
||||
background: rgba(146, 145, 145, 0.1);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,75 @@
|
||||
import axios from 'axios'
|
||||
|
||||
function padLeftZero(str: string): string {
|
||||
return ('00' + str).substr(str.length)
|
||||
}
|
||||
|
||||
const util = {
|
||||
goto(url: string) {
|
||||
window.open(url, '_blank')
|
||||
},
|
||||
title: function (title: string) {
|
||||
window.document.title = title ? title + ' - oa' : 'veypi project'
|
||||
},
|
||||
getCookie(name: string) {
|
||||
const reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)')
|
||||
const arr = document.cookie.match(reg)
|
||||
if (arr) {
|
||||
return unescape(arr[2])
|
||||
} else return null
|
||||
},
|
||||
delCookie(name: string) {
|
||||
const exp = new Date()
|
||||
exp.setTime(exp.getTime() - 1)
|
||||
const cval = this.getCookie(name)
|
||||
if (cval !== null) {
|
||||
document.cookie = name + '=' + cval + ';expires=' + exp.toLocaleString()
|
||||
}
|
||||
},
|
||||
setCookie(name: string, value: string, time: number) {
|
||||
const exp = new Date()
|
||||
exp.setTime(exp.getTime() + time)
|
||||
document.cookie =
|
||||
name + '=' + escape(value) + ';expires=' + exp.toLocaleString()
|
||||
},
|
||||
getToken() {
|
||||
return localStorage.auth_token
|
||||
},
|
||||
addTokenOf(url: string) {
|
||||
return url + '?auth_token=' + encodeURIComponent(this.getToken())
|
||||
},
|
||||
checkLogin() {
|
||||
// return parseInt(this.getCookie('stat')) === 1
|
||||
return Boolean(localStorage.auth_token)
|
||||
},
|
||||
|
||||
formatDate(date: Date, fmt: string) {
|
||||
if (/(y+)/.test(fmt)) {
|
||||
fmt = fmt.replace(
|
||||
RegExp.$1,
|
||||
(date.getFullYear() + '').substr(4 - RegExp.$1.length),
|
||||
)
|
||||
}
|
||||
const o = {
|
||||
'M+': date.getMonth() + 1,
|
||||
'd+': date.getDate(),
|
||||
'h+': date.getHours(),
|
||||
'm+': date.getMinutes(),
|
||||
's+': date.getSeconds(),
|
||||
}
|
||||
for (const k in o) {
|
||||
if (new RegExp(`(${k})`).test(fmt)) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
const str = o[k] + ''
|
||||
fmt = fmt.replace(
|
||||
RegExp.$1,
|
||||
RegExp.$1.length === 1 ? str : padLeftZero(str),
|
||||
)
|
||||
}
|
||||
}
|
||||
return fmt
|
||||
},
|
||||
}
|
||||
|
||||
export default util
|
@ -0,0 +1,7 @@
|
||||
import { toBase64 } from "../tools/encode";
|
||||
import { AuthHeader } from "../types";
|
||||
|
||||
export function generateBasicAuthHeader(username: string, password: string): AuthHeader {
|
||||
const encoded = toBase64(`${username}:${password}`);
|
||||
return `Basic ${encoded}`;
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
import md5 from "md5";
|
||||
import { ha1Compute } from "../tools/crypto";
|
||||
import { DigestContext, Response } from "../types";
|
||||
|
||||
const NONCE_CHARS = "abcdef0123456789";
|
||||
const NONCE_SIZE = 32;
|
||||
|
||||
export function createDigestContext(username: string, password: string): DigestContext {
|
||||
return { username, password, nc: 0, algorithm: "md5", hasDigestAuth: false };
|
||||
}
|
||||
|
||||
export function generateDigestAuthHeader(options, digest: DigestContext): string {
|
||||
const url = options.url.replace("//", "");
|
||||
const uri = url.indexOf("/") == -1 ? "/" : url.slice(url.indexOf("/"));
|
||||
const method = options.method ? options.method.toUpperCase() : "GET";
|
||||
const qop = /(^|,)\s*auth\s*($|,)/.test(digest.qop) ? "auth" : false;
|
||||
const ncString = `00000000${digest.nc}`.slice(-8);
|
||||
const ha1 = ha1Compute(
|
||||
digest.algorithm,
|
||||
digest.username,
|
||||
digest.realm,
|
||||
digest.password,
|
||||
digest.nonce,
|
||||
digest.cnonce
|
||||
);
|
||||
const ha2 = md5(`${method}:${uri}`);
|
||||
const digestResponse = qop
|
||||
? md5(`${ha1}:${digest.nonce}:${ncString}:${digest.cnonce}:${qop}:${ha2}`)
|
||||
: md5(`${ha1}:${digest.nonce}:${ha2}`);
|
||||
|
||||
const authValues = {
|
||||
username: digest.username,
|
||||
realm: digest.realm,
|
||||
nonce: digest.nonce,
|
||||
uri,
|
||||
qop,
|
||||
response: digestResponse,
|
||||
nc: ncString,
|
||||
cnonce: digest.cnonce,
|
||||
algorithm: digest.algorithm,
|
||||
opaque: digest.opaque
|
||||
};
|
||||
|
||||
const authHeader = [];
|
||||
for (const k in authValues) {
|
||||
if (authValues[k]) {
|
||||
if (k === "qop" || k === "nc" || k === "algorithm") {
|
||||
authHeader.push(`${k}=${authValues[k]}`);
|
||||
} else {
|
||||
authHeader.push(`${k}="${authValues[k]}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `Digest ${authHeader.join(", ")}`;
|
||||
}
|
||||
|
||||
function makeNonce(): string {
|
||||
let uid = "";
|
||||
for (let i = 0; i < NONCE_SIZE; ++i) {
|
||||
uid = `${uid}${NONCE_CHARS[Math.floor(Math.random() * NONCE_CHARS.length)]}`;
|
||||
}
|
||||
return uid;
|
||||
}
|
||||
|
||||
export function parseDigestAuth(response: Response, _digest: DigestContext): boolean {
|
||||
const authHeader = response.headers["www-authenticate"] || "";
|
||||
if (authHeader.split(/\s/)[0].toLowerCase() !== "digest") {
|
||||
return false;
|
||||
}
|
||||
const re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi;
|
||||
for (;;) {
|
||||
const match = re.exec(authHeader);
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
_digest[match[1]] = match[2] || match[3];
|
||||
}
|
||||
_digest.nc += 1;
|
||||
_digest.cnonce = makeNonce();
|
||||
return true;
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { Layerr } from "layerr";
|
||||
import { createDigestContext } from "./digest";
|
||||
import { generateBasicAuthHeader } from "./basic";
|
||||
import { generateTokenAuthHeader } from "./oauth";
|
||||
import { AuthType, ErrorCode, OAuthToken, WebDAVClientContext } from "../types";
|
||||
|
||||
export function setupAuth(
|
||||
context: WebDAVClientContext,
|
||||
username: string,
|
||||
password: string,
|
||||
oauthToken: OAuthToken
|
||||
): void {
|
||||
switch (context.authType) {
|
||||
case AuthType.Digest:
|
||||
context.digest = createDigestContext(username, password);
|
||||
break;
|
||||
case AuthType.None:
|
||||
// Do nothing
|
||||
break;
|
||||
case AuthType.Password:
|
||||
context.headers.Authorization = generateBasicAuthHeader(username, password);
|
||||
break;
|
||||
case AuthType.Token:
|
||||
context.headers.Authorization = generateTokenAuthHeader(oauthToken);
|
||||
break;
|
||||
default:
|
||||
throw new Layerr(
|
||||
{
|
||||
info: {
|
||||
code: ErrorCode.InvalidAuthType
|
||||
}
|
||||
},
|
||||
`Invalid auth type: ${context.authType}`
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import { AuthHeader, OAuthToken } from "../types";
|
||||
|
||||
export function generateTokenAuthHeader(token: OAuthToken): AuthHeader {
|
||||
return `${token.token_type} ${token.access_token}`;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
const hasArrayBuffer = typeof ArrayBuffer === "function";
|
||||
const { toString: objToString } = Object.prototype;
|
||||
|
||||
// Taken from: https://github.com/fengyuanchen/is-array-buffer/blob/master/src/index.js
|
||||
export function isArrayBuffer(value: any): boolean {
|
||||
return (
|
||||
hasArrayBuffer &&
|
||||
(value instanceof ArrayBuffer || objToString.call(value) === "[object ArrayBuffer]")
|
||||
);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
export function isBuffer(value: any): boolean {
|
||||
return (
|
||||
value != null &&
|
||||
value.constructor != null &&
|
||||
typeof value.constructor.isBuffer === "function" &&
|
||||
value.constructor.isBuffer(value)
|
||||
);
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import HotPatcher from "hot-patcher";
|
||||
|
||||
let __patcher: HotPatcher = null;
|
||||
|
||||
export function getPatcher(): HotPatcher {
|
||||
if (!__patcher) {
|
||||
__patcher = new HotPatcher();
|
||||
}
|
||||
return __patcher;
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
import Stream from "stream";
|
||||
import { extractURLPath } from "./tools/url";
|
||||
import { setupAuth } from "./auth/index";
|
||||
import { copyFile } from "./operations/copyFile";
|
||||
import { createDirectory } from "./operations/createDirectory";
|
||||
import { createReadStream, createWriteStream } from "./operations/createStream";
|
||||
import { customRequest } from "./operations/customRequest";
|
||||
import { deleteFile } from "./operations/deleteFile";
|
||||
import { exists } from "./operations/exists";
|
||||
import { getDirectoryContents } from "./operations/directoryContents";
|
||||
import { getFileContents, getFileDownloadLink } from "./operations/getFileContents";
|
||||
import { lock, unlock } from "./operations/lock";
|
||||
import { getQuota } from "./operations/getQuota";
|
||||
import { getStat } from "./operations/stat";
|
||||
import { moveFile } from "./operations/moveFile";
|
||||
import { getFileUploadLink, putFileContents } from "./operations/putFileContents";
|
||||
import {
|
||||
AuthType,
|
||||
BufferLike,
|
||||
CreateReadStreamOptions,
|
||||
CreateWriteStreamCallback,
|
||||
CreateWriteStreamOptions,
|
||||
GetDirectoryContentsOptions,
|
||||
GetFileContentsOptions,
|
||||
GetQuotaOptions,
|
||||
Headers,
|
||||
LockOptions,
|
||||
PutFileContentsOptions,
|
||||
RequestOptionsCustom,
|
||||
StatOptions,
|
||||
WebDAVClient,
|
||||
WebDAVClientContext,
|
||||
WebDAVClientOptions,
|
||||
WebDAVMethodOptions
|
||||
} from "./types";
|
||||
|
||||
const DEFAULT_CONTACT_HREF =
|
||||
"https://github.com/perry-mitchell/webdav-client/blob/master/LOCK_CONTACT.md";
|
||||
|
||||
export function createClient(remoteURL: string, options: WebDAVClientOptions = {}): WebDAVClient {
|
||||
const {
|
||||
authType: authTypeRaw = null,
|
||||
contactHref = DEFAULT_CONTACT_HREF,
|
||||
headers = {},
|
||||
httpAgent,
|
||||
httpsAgent,
|
||||
maxBodyLength,
|
||||
maxContentLength,
|
||||
password,
|
||||
token,
|
||||
username,
|
||||
withCredentials
|
||||
} = options;
|
||||
let authType = authTypeRaw;
|
||||
if (!authType) {
|
||||
authType = username || password ? AuthType.Password : AuthType.None;
|
||||
}
|
||||
const context: WebDAVClientContext = {
|
||||
authType,
|
||||
contactHref,
|
||||
headers: Object.assign({}, headers),
|
||||
httpAgent,
|
||||
httpsAgent,
|
||||
maxBodyLength,
|
||||
maxContentLength,
|
||||
remotePath: extractURLPath(remoteURL),
|
||||
remoteURL,
|
||||
password,
|
||||
token,
|
||||
username,
|
||||
withCredentials
|
||||
};
|
||||
setupAuth(context, username, password, token);
|
||||
return {
|
||||
copyFile: (filename: string, destination: string, options?: WebDAVMethodOptions) =>
|
||||
copyFile(context, filename, destination, options),
|
||||
createDirectory: (path: string, options?: WebDAVMethodOptions) =>
|
||||
createDirectory(context, path, options),
|
||||
createReadStream: (filename: string, options?: CreateReadStreamOptions) =>
|
||||
createReadStream(context, filename, options),
|
||||
createWriteStream: (
|
||||
filename: string,
|
||||
options?: CreateWriteStreamOptions,
|
||||
callback?: CreateWriteStreamCallback
|
||||
) => createWriteStream(context, filename, options, callback),
|
||||
customRequest: (path: string, requestOptions: RequestOptionsCustom) =>
|
||||
customRequest(context, path, requestOptions),
|
||||
deleteFile: (filename: string, options?: WebDAVMethodOptions) =>
|
||||
deleteFile(context, filename, options),
|
||||
exists: (path: string, options?: WebDAVMethodOptions) => exists(context, path, options),
|
||||
getDirectoryContents: (path: string, options?: GetDirectoryContentsOptions) =>
|
||||
getDirectoryContents(context, path, options),
|
||||
getFileContents: (filename: string, options?: GetFileContentsOptions) =>
|
||||
getFileContents(context, filename, options),
|
||||
getFileDownloadLink: (filename: string) => getFileDownloadLink(context, filename),
|
||||
getFileUploadLink: (filename: string) => getFileUploadLink(context, filename),
|
||||
getHeaders: () => Object.assign({}, context.headers),
|
||||
getQuota: (options?: GetQuotaOptions) => getQuota(context, options),
|
||||
lock: (path: string, options?: LockOptions) => lock(context, path, options),
|
||||
moveFile: (filename: string, destinationFilename: string, options?: WebDAVMethodOptions) =>
|
||||
moveFile(context, filename, destinationFilename, options),
|
||||
putFileContents: (
|
||||
filename: string,
|
||||
data: string | BufferLike | Stream.Readable,
|
||||
options?: PutFileContentsOptions
|
||||
) => putFileContents(context, filename, data, options),
|
||||
setHeaders: (headers: Headers) => {
|
||||
context.headers = Object.assign({}, headers);
|
||||
},
|
||||
stat: (path: string, options?: StatOptions) => getStat(context, path, options),
|
||||
unlock: (path: string, token: string, options?: WebDAVMethodOptions) =>
|
||||
unlock(context, path, token, options)
|
||||
};
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
export { createClient } from "./factory";
|
||||
export { getPatcher } from "./compat/patcher";
|
||||
export * from "./types";
|
||||
|
||||
export { parseStat, parseXML } from "./tools/dav";
|
@ -0,0 +1,26 @@
|
||||
import { joinURL } from "../tools/url";
|
||||
import { encodePath } from "../tools/path";
|
||||
import { request, prepareRequestOptions } from "../request";
|
||||
import { handleResponseCode } from "../response";
|
||||
import { WebDAVClientContext, WebDAVMethodOptions } from "../types";
|
||||
|
||||
export async function copyFile(
|
||||
context: WebDAVClientContext,
|
||||
filename: string,
|
||||
destination: string,
|
||||
options: WebDAVMethodOptions = {}
|
||||
): Promise<void> {
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, encodePath(filename)),
|
||||
method: "COPY",
|
||||
headers: {
|
||||
Destination: joinURL(context.remoteURL, encodePath(destination))
|
||||
}
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
const response = await request(requestOptions);
|
||||
handleResponseCode(context, response);
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
import { joinURL } from "../tools/url";
|
||||
import { encodePath, getAllDirectories, normalisePath } from "../tools/path";
|
||||
import { request, prepareRequestOptions } from "../request";
|
||||
import { handleResponseCode } from "../response";
|
||||
import { getStat } from "./stat";
|
||||
import { CreateDirectoryOptions, FileStat, WebDAVClientContext, WebDAVClientError } from "../types";
|
||||
|
||||
export async function createDirectory(
|
||||
context: WebDAVClientContext,
|
||||
dirPath: string,
|
||||
options: CreateDirectoryOptions = {}
|
||||
): Promise<void> {
|
||||
if (options.recursive === true) return createDirectoryRecursively(context, dirPath, options);
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, ensureCollectionPath(encodePath(dirPath))),
|
||||
method: "MKCOL"
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
const response = await request(requestOptions);
|
||||
handleResponseCode(context, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the path is a proper "collection" path by ensuring it has a trailing "/".
|
||||
* The proper format of collection according to the specification does contain the trailing slash.
|
||||
* http://www.webdav.org/specs/rfc4918.html#rfc.section.5.2
|
||||
* @param path Path of the collection
|
||||
* @return string Path of the collection with appended trailing "/" in case the `path` does not have it.
|
||||
*/
|
||||
function ensureCollectionPath(path: string): string {
|
||||
if (!path.endsWith("/")) {
|
||||
return path + "/";
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
async function createDirectoryRecursively(
|
||||
context: WebDAVClientContext,
|
||||
dirPath: string,
|
||||
options: CreateDirectoryOptions = {}
|
||||
): Promise<void> {
|
||||
const paths = getAllDirectories(normalisePath(dirPath));
|
||||
paths.sort((a, b) => {
|
||||
if (a.length > b.length) {
|
||||
return 1;
|
||||
} else if (b.length > a.length) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
let creating: boolean = false;
|
||||
for (const testPath of paths) {
|
||||
if (creating) {
|
||||
await createDirectory(context, testPath, {
|
||||
...options,
|
||||
recursive: false
|
||||
});
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const testStat = (await getStat(context, testPath)) as FileStat;
|
||||
if (testStat.type !== "directory") {
|
||||
throw new Error(`Path includes a file: ${dirPath}`);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as WebDAVClientError;
|
||||
if (error.status === 404) {
|
||||
creating = true;
|
||||
await createDirectory(context, testPath, {
|
||||
...options,
|
||||
recursive: false
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
import Stream from "stream";
|
||||
import { joinURL } from "../tools/url";
|
||||
import { encodePath } from "../tools/path";
|
||||
import { request, prepareRequestOptions } from "../request";
|
||||
import { handleResponseCode } from "../response";
|
||||
import {
|
||||
CreateReadStreamOptions,
|
||||
CreateWriteStreamCallback,
|
||||
CreateWriteStreamOptions,
|
||||
Headers,
|
||||
WebDAVClientContext,
|
||||
WebDAVClientError
|
||||
} from "../types";
|
||||
|
||||
const NOOP = () => {};
|
||||
|
||||
export function createReadStream(
|
||||
context: WebDAVClientContext,
|
||||
filePath: string,
|
||||
options: CreateReadStreamOptions = {}
|
||||
): Stream.Readable {
|
||||
const PassThroughStream = Stream.PassThrough;
|
||||
const outStream = new PassThroughStream();
|
||||
getFileStream(context, filePath, options)
|
||||
.then(stream => {
|
||||
stream.pipe(outStream);
|
||||
})
|
||||
.catch(err => {
|
||||
outStream.emit("error", err);
|
||||
});
|
||||
return outStream;
|
||||
}
|
||||
|
||||
export function createWriteStream(
|
||||
context: WebDAVClientContext,
|
||||
filePath: string,
|
||||
options: CreateWriteStreamOptions = {},
|
||||
callback: CreateWriteStreamCallback = NOOP
|
||||
): Stream.Writable {
|
||||
const PassThroughStream = Stream.PassThrough;
|
||||
const writeStream = new PassThroughStream();
|
||||
const headers = {};
|
||||
if (options.overwrite === false) {
|
||||
headers["If-None-Match"] = "*";
|
||||
}
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, encodePath(filePath)),
|
||||
method: "PUT",
|
||||
headers,
|
||||
data: writeStream,
|
||||
maxRedirects: 0
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
request(requestOptions)
|
||||
.then(response => handleResponseCode(context, response))
|
||||
.then(response => {
|
||||
// Fire callback asynchronously to avoid errors
|
||||
setTimeout(() => {
|
||||
callback(response);
|
||||
}, 0);
|
||||
})
|
||||
.catch(err => {
|
||||
writeStream.emit("error", err);
|
||||
});
|
||||
return writeStream;
|
||||
}
|
||||
|
||||
async function getFileStream(
|
||||
context: WebDAVClientContext,
|
||||
filePath: string,
|
||||
options: CreateReadStreamOptions = {}
|
||||
): Promise<Stream.Readable> {
|
||||
const headers: Headers = {};
|
||||
if (typeof options.range === "object" && typeof options.range.start === "number") {
|
||||
let rangeHeader = `bytes=${options.range.start}-`;
|
||||
if (typeof options.range.end === "number") {
|
||||
rangeHeader = `${rangeHeader}${options.range.end}`;
|
||||
}
|
||||
headers.Range = rangeHeader;
|
||||
}
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, encodePath(filePath)),
|
||||
method: "GET",
|
||||
headers,
|
||||
responseType: "stream"
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
const response = await request(requestOptions);
|
||||
handleResponseCode(context, response);
|
||||
if (headers.Range && response.status !== 206) {
|
||||
const responseError: WebDAVClientError = new Error(
|
||||
`Invalid response code for partial request: ${response.status}`
|
||||
);
|
||||
responseError.status = response.status;
|
||||
throw responseError;
|
||||
}
|
||||
if (options.callback) {
|
||||
setTimeout(() => {
|
||||
options.callback(response);
|
||||
}, 0);
|
||||
}
|
||||
return response.data as Stream.Readable;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { joinURL } from "../tools/url";
|
||||
import { encodePath } from "../tools/path";
|
||||
import { request, prepareRequestOptions } from "../request";
|
||||
import { handleResponseCode } from "../response";
|
||||
import { RequestOptionsCustom, Response, WebDAVClientContext } from "../types";
|
||||
|
||||
export async function customRequest(
|
||||
context: WebDAVClientContext,
|
||||
remotePath: string,
|
||||
requestOptions: RequestOptionsCustom
|
||||
): Promise<Response> {
|
||||
if (!requestOptions.url) {
|
||||
requestOptions.url = joinURL(context.remoteURL, encodePath(remotePath));
|
||||
}
|
||||
const finalOptions = prepareRequestOptions(requestOptions, context, {});
|
||||
const response = await request(finalOptions);
|
||||
handleResponseCode(context, response);
|
||||
return response;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { joinURL } from "../tools/url";
|
||||
import { encodePath } from "../tools/path";
|
||||
import { request, prepareRequestOptions } from "../request";
|
||||
import { handleResponseCode } from "../response";
|
||||
import { WebDAVClientContext, WebDAVMethodOptions } from "../types";
|
||||
|
||||
export async function deleteFile(
|
||||
context: WebDAVClientContext,
|
||||
filename: string,
|
||||
options: WebDAVMethodOptions = {}
|
||||
): Promise<void> {
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, encodePath(filename)),
|
||||
method: "DELETE"
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
const response = await request(requestOptions);
|
||||
handleResponseCode(context, response);
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import pathPosix from "path-posix";
|
||||
import { joinURL, normaliseHREF } from "../tools/url";
|
||||
import { encodePath, normalisePath } from "../tools/path";
|
||||
import { parseXML, prepareFileFromProps } from "../tools/dav";
|
||||
import { request, prepareRequestOptions } from "../request";
|
||||
import { handleResponseCode, processGlobFilter, processResponsePayload } from "../response";
|
||||
import {
|
||||
DAVResult,
|
||||
FileStat,
|
||||
GetDirectoryContentsOptions,
|
||||
ResponseDataDetailed,
|
||||
WebDAVClientContext
|
||||
} from "../types";
|
||||
|
||||
export async function getDirectoryContents(
|
||||
context: WebDAVClientContext,
|
||||
remotePath: string,
|
||||
options: GetDirectoryContentsOptions = {}
|
||||
): Promise<Array<FileStat> | ResponseDataDetailed<Array<FileStat>>> {
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, encodePath(remotePath), "/"),
|
||||
method: "PROPFIND",
|
||||
headers: {
|
||||
Accept: "text/plain",
|
||||
Depth: options.deep ? "infinity" : "1"
|
||||
},
|
||||
responseType: "text"
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
const response = await request(requestOptions);
|
||||
handleResponseCode(context, response);
|
||||
const davResp = await parseXML(response.data as string);
|
||||
let files = getDirectoryFiles(davResp, context.remotePath, remotePath, options.details);
|
||||
if (options.glob) {
|
||||
files = processGlobFilter(files, options.glob);
|
||||
}
|
||||
return processResponsePayload(response, files, options.details);
|
||||
}
|
||||
|
||||
function getDirectoryFiles(
|
||||
result: DAVResult,
|
||||
serverBasePath: string,
|
||||
requestPath: string,
|
||||
isDetailed: boolean = false
|
||||
): Array<FileStat> {
|
||||
const serverBase = pathPosix.join(serverBasePath, "/");
|
||||
// Extract the response items (directory contents)
|
||||
const {
|
||||
multistatus: { response: responseItems }
|
||||
} = result;
|
||||
return (
|
||||
responseItems
|
||||
// Map all items to a consistent output structure (results)
|
||||
.map(item => {
|
||||
// HREF is the file path (in full)
|
||||
const href = normaliseHREF(item.href);
|
||||
// Each item should contain a stat object
|
||||
const {
|
||||
propstat: { prop: props }
|
||||
} = item;
|
||||
// Process the true full filename (minus the base server path)
|
||||
const filename =
|
||||
serverBase === "/"
|
||||
? decodeURIComponent(normalisePath(href))
|
||||
: decodeURIComponent(normalisePath(pathPosix.relative(serverBase, href)));
|
||||
return prepareFileFromProps(props, filename, isDetailed);
|
||||
})
|
||||
// Filter out the item pointing to the current directory (not needed)
|
||||
.filter(
|
||||
item =>
|
||||
item.basename &&
|
||||
(item.type === "file" || item.filename !== requestPath.replace(/\/$/, ""))
|
||||
)
|
||||
);
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { getStat } from "./stat";
|
||||
import { WebDAVClientContext, WebDAVMethodOptions } from "../types";
|
||||
|
||||
export async function exists(
|
||||
context: WebDAVClientContext,
|
||||
remotePath: string,
|
||||
options: WebDAVMethodOptions = {}
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await getStat(context, remotePath, options);
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
import { Layerr } from "layerr";
|
||||
import { joinURL } from "../tools/url";
|
||||
import { encodePath } from "../tools/path";
|
||||
import { fromBase64 } from "../tools/encode";
|
||||
import { request, prepareRequestOptions } from "../request";
|
||||
import { handleResponseCode, processResponsePayload } from "../response";
|
||||
import {
|
||||
AuthType,
|
||||
BufferLike,
|
||||
ErrorCode,
|
||||
GetFileContentsOptions,
|
||||
ResponseDataDetailed,
|
||||
WebDAVClientContext
|
||||
} from "../types";
|
||||
|
||||
const TRANSFORM_RETAIN_FORMAT = (v: any) => v;
|
||||
|
||||
export async function getFileContents(
|
||||
context: WebDAVClientContext,
|
||||
filePath: string,
|
||||
options: GetFileContentsOptions = {}
|
||||
): Promise<BufferLike | string | ResponseDataDetailed<BufferLike | string>> {
|
||||
const { format = "binary" } = options;
|
||||
if (format !== "binary" && format !== "text") {
|
||||
throw new Layerr(
|
||||
{
|
||||
info: {
|
||||
code: ErrorCode.InvalidOutputFormat
|
||||
}
|
||||
},
|
||||
`Invalid output format: ${format}`
|
||||
);
|
||||
}
|
||||
return format === "text"
|
||||
? getFileContentsString(context, filePath, options)
|
||||
: getFileContentsBuffer(context, filePath, options);
|
||||
}
|
||||
|
||||
async function getFileContentsBuffer(
|
||||
context: WebDAVClientContext,
|
||||
filePath: string,
|
||||
options: GetFileContentsOptions = {}
|
||||
): Promise<BufferLike | ResponseDataDetailed<BufferLike>> {
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, encodePath(filePath)),
|
||||
method: "GET",
|
||||
responseType: "arraybuffer"
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
const response = await request(requestOptions);
|
||||
handleResponseCode(context, response);
|
||||
return processResponsePayload(response, response.data as BufferLike, options.details);
|
||||
}
|
||||
|
||||
async function getFileContentsString(
|
||||
context: WebDAVClientContext,
|
||||
filePath: string,
|
||||
options: GetFileContentsOptions = {}
|
||||
): Promise<string | ResponseDataDetailed<string>> {
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, encodePath(filePath)),
|
||||
method: "GET",
|
||||
responseType: "text",
|
||||
transformResponse: [TRANSFORM_RETAIN_FORMAT]
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
const response = await request(requestOptions);
|
||||
handleResponseCode(context, response);
|
||||
return processResponsePayload(response, response.data as string, options.details);
|
||||
}
|
||||
|
||||
export function getFileDownloadLink(context: WebDAVClientContext, filePath: string): string {
|
||||
let url = joinURL(context.remoteURL, encodePath(filePath));
|
||||
const protocol = /^https:/i.test(url) ? "https" : "http";
|
||||
switch (context.authType) {
|
||||
case AuthType.None:
|
||||
// Do nothing
|
||||
break;
|
||||
case AuthType.Password: {
|
||||
const authPart = context.headers.Authorization.replace(/^Basic /i, "").trim();
|
||||
const authContents = fromBase64(authPart);
|
||||
url = url.replace(/^https?:\/\//, `${protocol}://${authContents}@`);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Layerr(
|
||||
{
|
||||
info: {
|
||||
code: ErrorCode.LinkUnsupportedAuthType
|
||||
}
|
||||
},
|
||||
`Unsupported auth type for file link: ${context.authType}`
|
||||
);
|
||||
}
|
||||
return url;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { prepareRequestOptions, request } from "../request";
|
||||
import { handleResponseCode, processResponsePayload } from "../response";
|
||||
import { parseXML } from "../tools/dav";
|
||||
import { joinURL } from "../tools/url";
|
||||
import { parseQuota } from "../tools/quota";
|
||||
import { DiskQuota, GetQuotaOptions, ResponseDataDetailed, WebDAVClientContext } from "../types";
|
||||
|
||||
export async function getQuota(
|
||||
context: WebDAVClientContext,
|
||||
options: GetQuotaOptions = {}
|
||||
): Promise<DiskQuota | null | ResponseDataDetailed<DiskQuota | null>> {
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, "/"),
|
||||
method: "PROPFIND",
|
||||
headers: {
|
||||
Accept: "text/plain",
|
||||
Depth: "0"
|
||||
},
|
||||
responseType: "text"
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
const response = await request(requestOptions);
|
||||
handleResponseCode(context, response);
|
||||
const result = await parseXML(response.data as string);
|
||||
const quota = parseQuota(result);
|
||||
return processResponsePayload(response, quota, options.details);
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import nestedProp from "nested-property";
|
||||
import { joinURL } from "../tools/url";
|
||||
import { encodePath } from "../tools/path";
|
||||
import { generateLockXML, parseGenericResponse } from "../tools/xml";
|
||||
import { request, prepareRequestOptions } from "../request";
|
||||
import { createErrorFromResponse, handleResponseCode } from "../response";
|
||||
import {
|
||||
Headers,
|
||||
LockOptions,
|
||||
LockResponse,
|
||||
WebDAVClientContext,
|
||||
WebDAVMethodOptions
|
||||
} from "../types";
|
||||
|
||||
const DEFAULT_TIMEOUT = "Infinite, Second-4100000000";
|
||||
|
||||
export async function lock(
|
||||
context: WebDAVClientContext,
|
||||
path: string,
|
||||
options: LockOptions = {}
|
||||
): Promise<LockResponse> {
|
||||
const { refreshToken, timeout = DEFAULT_TIMEOUT } = options;
|
||||
const headers: Headers = {
|
||||
Accept: "text/plain,application/xml",
|
||||
Timeout: timeout
|
||||
};
|
||||
if (refreshToken) {
|
||||
headers.If = refreshToken;
|
||||
}
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, encodePath(path)),
|
||||
method: "LOCK",
|
||||
headers,
|
||||
data: generateLockXML(context.contactHref),
|
||||
responseType: "text"
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
const response = await request(requestOptions);
|
||||
handleResponseCode(context, response);
|
||||
const lockPayload = parseGenericResponse(response.data as string);
|
||||
const token = nestedProp.get(lockPayload, "prop.lockdiscovery.activelock.locktoken.href");
|
||||
const serverTimeout = nestedProp.get(lockPayload, "prop.lockdiscovery.activelock.timeout");
|
||||
if (!token) {
|
||||
const err = createErrorFromResponse(response, "No lock token received: ");
|
||||
throw err;
|
||||
}
|
||||
return {
|
||||
token,
|
||||
serverTimeout
|
||||
};
|
||||
}
|
||||
|
||||
export async function unlock(
|
||||
context: WebDAVClientContext,
|
||||
path: string,
|
||||
token: string,
|
||||
options: WebDAVMethodOptions = {}
|
||||
): Promise<void> {
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, encodePath(path)),
|
||||
method: "UNLOCK",
|
||||
headers: {
|
||||
"Lock-Token": token
|
||||
}
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
const response = await request(requestOptions);
|
||||
handleResponseCode(context, response);
|
||||
if (response.status !== 204 && response.status !== 200) {
|
||||
const err = createErrorFromResponse(response);
|
||||
throw err;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { joinURL } from "../tools/url";
|
||||
import { encodePath } from "../tools/path";
|
||||
import { request, prepareRequestOptions } from "../request";
|
||||
import { handleResponseCode } from "../response";
|
||||
import { WebDAVClientContext, WebDAVMethodOptions } from "../types";
|
||||
|
||||
export async function moveFile(
|
||||
context: WebDAVClientContext,
|
||||
filename: string,
|
||||
destination: string,
|
||||
options: WebDAVMethodOptions = {}
|
||||
): Promise<void> {
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, encodePath(filename)),
|
||||
method: "MOVE",
|
||||
headers: {
|
||||
Destination: joinURL(context.remoteURL, encodePath(destination))
|
||||
}
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
const response = await request(requestOptions);
|
||||
handleResponseCode(context, response);
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
import { Layerr } from "layerr";
|
||||
import Stream from "stream";
|
||||
import { fromBase64 } from "../tools/encode";
|
||||
import { joinURL } from "../tools/url";
|
||||
import { encodePath } from "../tools/path";
|
||||
import { request, prepareRequestOptions } from "../request";
|
||||
import { handleResponseCode } from "../response";
|
||||
import { calculateDataLength } from "../tools/size";
|
||||
import {
|
||||
AuthType,
|
||||
BufferLike,
|
||||
ErrorCode,
|
||||
Headers,
|
||||
PutFileContentsOptions,
|
||||
WebDAVClientContext,
|
||||
WebDAVClientError
|
||||
} from "../types";
|
||||
|
||||
declare var WEB: boolean;
|
||||
|
||||
export async function putFileContents(
|
||||
context: WebDAVClientContext,
|
||||
filePath: string,
|
||||
data: string | BufferLike | Stream.Readable,
|
||||
options: PutFileContentsOptions = {}
|
||||
): Promise<boolean> {
|
||||
const { contentLength = true, overwrite = true } = options;
|
||||
const headers: Headers = {
|
||||
"Content-Type": "application/octet-stream"
|
||||
};
|
||||
if (typeof WEB === "undefined") {
|
||||
// Skip, no content-length
|
||||
} else if (contentLength === false) {
|
||||
// Skip, disabled
|
||||
} else if (typeof contentLength === "number") {
|
||||
headers["Content-Length"] = `${contentLength}`;
|
||||
} else {
|
||||
headers["Content-Length"] = `${calculateDataLength(data as string | BufferLike)}`;
|
||||
}
|
||||
if (!overwrite) {
|
||||
headers["If-None-Match"] = "*";
|
||||
}
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, encodePath(filePath)),
|
||||
method: "PUT",
|
||||
headers,
|
||||
data
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
const response = await request(requestOptions);
|
||||
try {
|
||||
handleResponseCode(context, response);
|
||||
} catch (err) {
|
||||
const error = err as WebDAVClientError;
|
||||
if (error.status === 412 && !overwrite) {
|
||||
return false;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getFileUploadLink(context: WebDAVClientContext, filePath: string): string {
|
||||
let url: string = `${joinURL(
|
||||
context.remoteURL,
|
||||
encodePath(filePath)
|
||||
)}?Content-Type=application/octet-stream`;
|
||||
const protocol = /^https:/i.test(url) ? "https" : "http";
|
||||
switch (context.authType) {
|
||||
case AuthType.None:
|
||||
// Do nothing
|
||||
break;
|
||||
case AuthType.Password: {
|
||||
const authPart = context.headers.Authorization.replace(/^Basic /i, "").trim();
|
||||
const authContents = fromBase64(authPart);
|
||||
url = url.replace(/^https?:\/\//, `${protocol}://${authContents}@`);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Layerr(
|
||||
{
|
||||
info: {
|
||||
code: ErrorCode.LinkUnsupportedAuthType
|
||||
}
|
||||
},
|
||||
`Unsupported auth type for file link: ${context.authType}`
|
||||
);
|
||||
}
|
||||
return url;
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { parseStat, parseXML } from "../tools/dav";
|
||||
import { joinURL } from "../tools/url";
|
||||
import { encodePath } from "../tools/path";
|
||||
import { request, prepareRequestOptions } from "../request";
|
||||
import { handleResponseCode, processResponsePayload } from "../response";
|
||||
import { FileStat, ResponseDataDetailed, StatOptions, WebDAVClientContext } from "../types";
|
||||
|
||||
export async function getStat(
|
||||
context: WebDAVClientContext,
|
||||
filename: string,
|
||||
options: StatOptions = {}
|
||||
): Promise<FileStat | ResponseDataDetailed<FileStat>> {
|
||||
const { details: isDetailed = false } = options;
|
||||
const requestOptions = prepareRequestOptions(
|
||||
{
|
||||
url: joinURL(context.remoteURL, encodePath(filename)),
|
||||
method: "PROPFIND",
|
||||
headers: {
|
||||
Accept: "text/plain,application/xml",
|
||||
Depth: "0"
|
||||
},
|
||||
responseType: "text"
|
||||
},
|
||||
context,
|
||||
options
|
||||
);
|
||||
const response = await request(requestOptions);
|
||||
handleResponseCode(context, response);
|
||||
const result = await parseXML(response.data as string);
|
||||
const stat = parseStat(result, filename, isDetailed);
|
||||
return processResponsePayload(response, stat, isDetailed);
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
import axios from "axios";
|
||||
import { getPatcher } from "./compat/patcher";
|
||||
import { generateDigestAuthHeader, parseDigestAuth } from "./auth/digest";
|
||||
import { cloneShallow, merge } from "./tools/merge";
|
||||
import { mergeHeaders } from "./tools/headers";
|
||||
import {
|
||||
RequestOptionsCustom,
|
||||
RequestOptionsWithState,
|
||||
RequestOptions,
|
||||
Response,
|
||||
WebDAVClientContext,
|
||||
WebDAVMethodOptions
|
||||
} from "./types";
|
||||
|
||||
function _request(requestOptions: RequestOptions) {
|
||||
return getPatcher().patchInline(
|
||||
"request",
|
||||
(options: RequestOptions) => axios(options as any),
|
||||
requestOptions
|
||||
);
|
||||
}
|
||||
|
||||
export function prepareRequestOptions(
|
||||
requestOptions: RequestOptionsCustom | RequestOptionsWithState,
|
||||
context: WebDAVClientContext,
|
||||
userOptions: WebDAVMethodOptions
|
||||
): RequestOptionsWithState {
|
||||
const finalOptions = cloneShallow(requestOptions) as RequestOptionsWithState;
|
||||
finalOptions.headers = mergeHeaders(
|
||||
context.headers,
|
||||
finalOptions.headers || {},
|
||||
userOptions.headers || {}
|
||||
);
|
||||
if (typeof userOptions.data !== "undefined") {
|
||||
finalOptions.data = userOptions.data;
|
||||
}
|
||||
if (context.httpAgent) {
|
||||
finalOptions.httpAgent = context.httpAgent;
|
||||
}
|
||||
if (context.httpsAgent) {
|
||||
finalOptions.httpsAgent = context.httpsAgent;
|
||||
}
|
||||
if (context.digest) {
|
||||
finalOptions._digest = context.digest;
|
||||
}
|
||||
if (typeof context.withCredentials === "boolean") {
|
||||
finalOptions.withCredentials = context.withCredentials;
|
||||
}
|
||||
if (context.maxContentLength) {
|
||||
finalOptions.maxContentLength = context.maxContentLength;
|
||||
}
|
||||
if (context.maxBodyLength) {
|
||||
finalOptions.maxBodyLength = context.maxBodyLength;
|
||||
}
|
||||
if (userOptions.hasOwnProperty("onUploadProgress")) {
|
||||
finalOptions.onUploadProgress = userOptions["onUploadProgress"];
|
||||
}
|
||||
// Take full control of all response status codes
|
||||
finalOptions.validateStatus = () => true;
|
||||
return finalOptions;
|
||||
}
|
||||
|
||||
export function request(requestOptions: RequestOptionsWithState): Promise<Response> {
|
||||
// Client not configured for digest authentication
|
||||
if (!requestOptions._digest) {
|
||||
return _request(requestOptions);
|
||||
}
|
||||
|
||||
// Remove client's digest authentication object from request options
|
||||
const _digest = requestOptions._digest;
|
||||
delete requestOptions._digest;
|
||||
|
||||
// If client is already using digest authentication, include the digest authorization header
|
||||
if (_digest.hasDigestAuth) {
|
||||
requestOptions = merge(requestOptions, {
|
||||
headers: {
|
||||
Authorization: generateDigestAuthHeader(requestOptions, _digest)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Perform the request and handle digest authentication
|
||||
return _request(requestOptions).then(function(response: Response) {
|
||||
if (response.status == 401) {
|
||||
_digest.hasDigestAuth = parseDigestAuth(response, _digest);
|
||||
|
||||
if (_digest.hasDigestAuth) {
|
||||
requestOptions = merge(requestOptions, {
|
||||
headers: {
|
||||
Authorization: generateDigestAuthHeader(requestOptions, _digest)
|
||||
}
|
||||
});
|
||||
|
||||
return _request(requestOptions).then(function(response2: Response) {
|
||||
if (response2.status == 401) {
|
||||
_digest.hasDigestAuth = false;
|
||||
} else {
|
||||
_digest.nc++;
|
||||
}
|
||||
return response2;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
_digest.nc++;
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import minimatch from "minimatch";
|
||||
import {
|
||||
FileStat,
|
||||
Response,
|
||||
ResponseDataDetailed,
|
||||
WebDAVClientContext,
|
||||
WebDAVClientError
|
||||
} from "./types";
|
||||
|
||||
export function createErrorFromResponse(response: Response, prefix: string = ""): Error {
|
||||
const err: WebDAVClientError = new Error(
|
||||
`${prefix}Invalid response: ${response.status} ${response.statusText}`
|
||||
) as WebDAVClientError;
|
||||
err.status = response.status;
|
||||
err.response = response;
|
||||
return err;
|
||||
}
|
||||
|
||||
export function handleResponseCode(context: WebDAVClientContext, response: Response): Response {
|
||||
const { status } = response;
|
||||
if (status === 401 && context.digest) return response;
|
||||
if (status >= 400) {
|
||||
const err = createErrorFromResponse(response);
|
||||
throw err;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
export function processGlobFilter(files: Array<FileStat>, glob: string): Array<FileStat> {
|
||||
return files.filter(file => minimatch(file.filename, glob, { matchBase: true }));
|
||||
}
|
||||
|
||||
export function processResponsePayload<T>(
|
||||
response: Response,
|
||||
data: T,
|
||||
isDetailed: boolean = false
|
||||
): ResponseDataDetailed<T> | T {
|
||||
return isDetailed
|
||||
? {
|
||||
data,
|
||||
headers: response.headers || {},
|
||||
status: response.status,
|
||||
statusText: response.statusText
|
||||
}
|
||||
: data;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import md5 from "md5";
|
||||
|
||||
export function ha1Compute(
|
||||
algorithm: string,
|
||||
user: string,
|
||||
realm: string,
|
||||
pass: string,
|
||||
nonce: string,
|
||||
cnonce: string
|
||||
): string {
|
||||
const ha1 = md5(`${user}:${realm}:${pass}`) as string;
|
||||
if (algorithm && algorithm.toLowerCase() === "md5-sess") {
|
||||
return md5(`${ha1}:${nonce}:${cnonce}`) as string;
|
||||
}
|
||||
return ha1;
|
||||
}
|
@ -0,0 +1,171 @@
|
||||
import path from "path-posix";
|
||||
import xmlParser from "fast-xml-parser";
|
||||
import nestedProp from "nested-property";
|
||||
import { decodeHTMLEntities } from "./encode";
|
||||
import { normalisePath } from "./path";
|
||||
import {
|
||||
DAVResult,
|
||||
DAVResultRaw,
|
||||
DAVResultResponse,
|
||||
DAVResultResponseProps,
|
||||
DiskQuotaAvailable,
|
||||
FileStat,
|
||||
WebDAVClientError
|
||||
} from "../types";
|
||||
|
||||
enum PropertyType {
|
||||
Array = "array",
|
||||
Object = "object",
|
||||
Original = "original"
|
||||
}
|
||||
|
||||
function getPropertyOfType(
|
||||
obj: Object,
|
||||
prop: string,
|
||||
type: PropertyType = PropertyType.Original
|
||||
): any {
|
||||
const val = nestedProp.get(obj, prop);
|
||||
if (type === "array" && Array.isArray(val) === false) {
|
||||
return [val];
|
||||
} else if (type === "object" && Array.isArray(val)) {
|
||||
return val[0];
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
function normaliseResponse(response: any): DAVResultResponse {
|
||||
const output = Object.assign({}, response);
|
||||
nestedProp.set(output, "propstat", getPropertyOfType(output, "propstat", PropertyType.Object));
|
||||
nestedProp.set(
|
||||
output,
|
||||
"propstat.prop",
|
||||
getPropertyOfType(output, "propstat.prop", PropertyType.Object)
|
||||
);
|
||||
return output;
|
||||
}
|
||||
|
||||
function normaliseResult(result: DAVResultRaw): DAVResult {
|
||||
const { multistatus } = result;
|
||||
if (multistatus === "") {
|
||||
return {
|
||||
multistatus: {
|
||||
response: []
|
||||
}
|
||||
};
|
||||
}
|
||||
if (!multistatus) {
|
||||
throw new Error("Invalid response: No root multistatus found");
|
||||
}
|
||||
const output: any = {
|
||||
multistatus: Array.isArray(multistatus) ? multistatus[0] : multistatus
|
||||
};
|
||||
nestedProp.set(
|
||||
output,
|
||||
"multistatus.response",
|
||||
getPropertyOfType(output, "multistatus.response", PropertyType.Array)
|
||||
);
|
||||
nestedProp.set(
|
||||
output,
|
||||
"multistatus.response",
|
||||
nestedProp.get(output, "multistatus.response").map(response => normaliseResponse(response))
|
||||
);
|
||||
return output as DAVResult;
|
||||
}
|
||||
|
||||
export function parseXML(xml: string): Promise<DAVResult> {
|
||||
return new Promise(resolve => {
|
||||
const result = xmlParser.parse(xml, {
|
||||
arrayMode: false,
|
||||
ignoreNameSpace: true
|
||||
// // We don't use the processors here as decoding is done manually
|
||||
// // later on - decoding early would break some path checks.
|
||||
// attrValueProcessor: val => decodeHTMLEntities(decodeURIComponent(val)),
|
||||
// tagValueProcessor: val => decodeHTMLEntities(decodeURIComponent(val))
|
||||
});
|
||||
resolve(normaliseResult(result));
|
||||
});
|
||||
}
|
||||
|
||||
export function prepareFileFromProps(
|
||||
props: DAVResultResponseProps,
|
||||
rawFilename: string,
|
||||
isDetailed: boolean = false
|
||||
): FileStat {
|
||||
// Last modified time, raw size, item type and mime
|
||||
const {
|
||||
getlastmodified: lastMod = null,
|
||||
getcontentlength: rawSize = "0",
|
||||
resourcetype: resourceType = null,
|
||||
getcontenttype: mimeType = null,
|
||||
getetag: etag = null
|
||||
} = props;
|
||||
const type =
|
||||
resourceType &&
|
||||
typeof resourceType === "object" &&
|
||||
typeof resourceType.collection !== "undefined"
|
||||
? "directory"
|
||||
: "file";
|
||||
const filename = decodeHTMLEntities(rawFilename);
|
||||
const stat: FileStat = {
|
||||
filename,
|
||||
basename: path.basename(filename),
|
||||
lastmod: lastMod,
|
||||
size: parseInt(rawSize, 10),
|
||||
type,
|
||||
etag: typeof etag === "string" ? etag.replace(/"/g, "") : null
|
||||
};
|
||||
if (type === "file") {
|
||||
stat.mime = mimeType && typeof mimeType === "string" ? mimeType.split(";")[0] : "";
|
||||
}
|
||||
if (isDetailed) {
|
||||
stat.props = props;
|
||||
}
|
||||
return stat;
|
||||
}
|
||||
|
||||
export function parseStat(
|
||||
result: DAVResult,
|
||||
filename: string,
|
||||
isDetailed: boolean = false
|
||||
): FileStat {
|
||||
let responseItem: DAVResultResponse = null;
|
||||
try {
|
||||
responseItem = result.multistatus.response[0];
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
if (!responseItem) {
|
||||
throw new Error("Failed getting item stat: bad response");
|
||||
}
|
||||
const {
|
||||
propstat: { prop: props, status: statusLine }
|
||||
} = responseItem;
|
||||
|
||||
// As defined in https://tools.ietf.org/html/rfc2068#section-6.1
|
||||
const [_, statusCodeStr, statusText] = statusLine.split(" ", 3);
|
||||
const statusCode = parseInt(statusCodeStr, 10);
|
||||
if (statusCode >= 400) {
|
||||
const err: WebDAVClientError = new Error(
|
||||
`Invalid response: ${statusCode} ${statusText}`
|
||||
) as WebDAVClientError;
|
||||
err.status = statusCode;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const filePath = normalisePath(filename);
|
||||
return prepareFileFromProps(props, filePath, isDetailed);
|
||||
}
|
||||
|
||||
export function translateDiskSpace(value: string | number): DiskQuotaAvailable {
|
||||
switch (value.toString()) {
|
||||
case "-3":
|
||||
return "unlimited";
|
||||
case "-2":
|
||||
/* falls-through */
|
||||
case "-1":
|
||||
// -1 is non-computed
|
||||
return "unknown";
|
||||
default:
|
||||
return parseInt(value as string, 10);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { decode, encode } from "base-64";
|
||||
|
||||
declare var WEB: boolean;
|
||||
|
||||
export function decodeHTMLEntities(text: string): string {
|
||||
if (typeof WEB === "undefined") {
|
||||
// Node
|
||||
const he = require("he");
|
||||
return he.decode(text);
|
||||
} else {
|
||||
// Nasty browser way
|
||||
const txt = document.createElement("textarea");
|
||||
txt.innerHTML = text;
|
||||
return txt.value;
|
||||
}
|
||||
}
|
||||
|
||||
export function fromBase64(text: string): string {
|
||||
return decode(text);
|
||||
}
|
||||
|
||||
export function toBase64(text: string): string {
|
||||
return encode(text);
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { Headers } from "../types";
|
||||
|
||||
export function mergeHeaders(...headerPayloads: Headers[]): Headers {
|
||||
if (headerPayloads.length === 0) return {};
|
||||
const headerKeys = {};
|
||||
return headerPayloads.reduce((output: Headers, headers: Headers) => {
|
||||
Object.keys(headers).forEach(header => {
|
||||
const lowerHeader = header.toLowerCase();
|
||||
if (headerKeys.hasOwnProperty(lowerHeader)) {
|
||||
output[headerKeys[lowerHeader]] = headers[header];
|
||||
} else {
|
||||
headerKeys[lowerHeader] = header;
|
||||
output[header] = headers[header];
|
||||
}
|
||||
});
|
||||
return output;
|
||||
}, {});
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
export function cloneShallow<T extends Object>(obj: T): T {
|
||||
return isPlainObject(obj)
|
||||
? Object.assign({}, obj)
|
||||
: Object.setPrototypeOf(Object.assign({}, obj), Object.getPrototypeOf(obj));
|
||||
}
|
||||
|
||||
function isPlainObject(obj: Object | any): boolean {
|
||||
if (
|
||||
typeof obj !== "object" ||
|
||||
obj === null ||
|
||||
Object.prototype.toString.call(obj) != "[object Object]"
|
||||
) {
|
||||
// Not an object
|
||||
return false;
|
||||
}
|
||||
if (Object.getPrototypeOf(obj) === null) {
|
||||
return true;
|
||||
}
|
||||
let proto = obj;
|
||||
// Find the prototype
|
||||
while (Object.getPrototypeOf(proto) !== null) {
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
return Object.getPrototypeOf(obj) === proto;
|
||||
}
|
||||
|
||||
export function merge(...args: Object[]) {
|
||||
let output = null,
|
||||
items = [...args];
|
||||
while (items.length > 0) {
|
||||
const nextItem = items.shift();
|
||||
if (!output) {
|
||||
output = cloneShallow(nextItem);
|
||||
} else {
|
||||
output = mergeObjects(output, nextItem);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function mergeObjects(obj1: Object, obj2: Object): Object {
|
||||
const output = cloneShallow(obj1);
|
||||
Object.keys(obj2).forEach(key => {
|
||||
if (!output.hasOwnProperty(key)) {
|
||||
output[key] = obj2[key];
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(obj2[key])) {
|
||||
output[key] = Array.isArray(output[key])
|
||||
? [...output[key], ...obj2[key]]
|
||||
: [...obj2[key]];
|
||||
} else if (typeof obj2[key] === "object" && !!obj2[key]) {
|
||||
output[key] =
|
||||
typeof output[key] === "object" && !!output[key]
|
||||
? mergeObjects(output[key], obj2[key])
|
||||
: cloneShallow(obj2[key]);
|
||||
} else {
|
||||
output[key] = obj2[key];
|
||||
}
|
||||
});
|
||||
return output;
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { dirname } from "path-posix";
|
||||
|
||||
const SEP_PATH_POSIX = "__PATH_SEPARATOR_POSIX__";
|
||||
const SEP_PATH_WINDOWS = "__PATH_SEPARATOR_WINDOWS__";
|
||||
|
||||
export function encodePath(path) {
|
||||
const replaced = path.replace(/\//g, SEP_PATH_POSIX).replace(/\\\\/g, SEP_PATH_WINDOWS);
|
||||
const formatted = encodeURIComponent(replaced);
|
||||
return formatted
|
||||
.split(SEP_PATH_WINDOWS)
|
||||
.join("\\\\")
|
||||
.split(SEP_PATH_POSIX)
|
||||
.join("/");
|
||||
}
|
||||
|
||||
export function getAllDirectories(path: string): Array<string> {
|
||||
if (!path || path === "/") return [];
|
||||
let currentPath = path;
|
||||
const output: Array<string> = [];
|
||||
do {
|
||||
output.push(currentPath);
|
||||
currentPath = dirname(currentPath);
|
||||
} while (currentPath && currentPath !== "/");
|
||||
return output;
|
||||
}
|
||||
|
||||
export function normalisePath(pathStr: string): string {
|
||||
let normalisedPath = pathStr;
|
||||
if (normalisedPath[0] !== "/") {
|
||||
normalisedPath = "/" + normalisedPath;
|
||||
}
|
||||
if (/^.+\/$/.test(normalisedPath)) {
|
||||
normalisedPath = normalisedPath.substr(0, normalisedPath.length - 1);
|
||||
}
|
||||
return normalisedPath;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { translateDiskSpace } from "./dav";
|
||||
import { DAVResult, DiskQuota } from "../types";
|
||||
|
||||
export function parseQuota(result: DAVResult): DiskQuota | null {
|
||||
try {
|
||||
const [responseItem] = result.multistatus.response;
|
||||
const {
|
||||
propstat: {
|
||||
prop: { "quota-used-bytes": quotaUsed, "quota-available-bytes": quotaAvail }
|
||||
}
|
||||
} = responseItem;
|
||||
return typeof quotaUsed !== "undefined" && typeof quotaAvail !== "undefined"
|
||||
? {
|
||||
used: parseInt(quotaUsed, 10),
|
||||
available: translateDiskSpace(quotaAvail)
|
||||
}
|
||||
: null;
|
||||
} catch (err) {
|
||||
/* ignore */
|
||||
}
|
||||
return null;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { Layerr } from "layerr";
|
||||
import { isArrayBuffer } from "../compat/arrayBuffer";
|
||||
import { isBuffer } from "../compat/buffer";
|
||||
import { BufferLike, ErrorCode } from "../types";
|
||||
|
||||
export function calculateDataLength(data: string | BufferLike): number {
|
||||
if (isArrayBuffer(data)) {
|
||||
return (<ArrayBuffer>data).byteLength;
|
||||
} else if (isBuffer(data)) {
|
||||
return (<Buffer>data).length;
|
||||
} else if (typeof data === "string") {
|
||||
return (<string>data).length;
|
||||
}
|
||||
throw new Layerr(
|
||||
{
|
||||
info: {
|
||||
code: ErrorCode.DataTypeNoLength
|
||||
}
|
||||
},
|
||||
"Cannot calculate data length: Invalid type"
|
||||
);
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import URL from "url-parse";
|
||||
import _joinURL from "url-join";
|
||||
import { normalisePath } from "./path";
|
||||
|
||||
export function extractURLPath(fullURL: string): string {
|
||||
const url = new URL(fullURL);
|
||||
let urlPath = url.pathname;
|
||||
if (urlPath.length <= 0) {
|
||||
urlPath = "/";
|
||||
}
|
||||
return normalisePath(urlPath);
|
||||
}
|
||||
|
||||
export function joinURL(...parts: Array<string>): string {
|
||||
return _joinURL(
|
||||
parts.reduce((output, nextPart, partIndex) => {
|
||||
if (
|
||||
partIndex === 0 ||
|
||||
nextPart !== "/" ||
|
||||
(nextPart === "/" && output[output.length - 1] !== "/")
|
||||
) {
|
||||
output.push(nextPart);
|
||||
}
|
||||
return output;
|
||||
}, [])
|
||||
);
|
||||
}
|
||||
|
||||
export function normaliseHREF(href: string): string {
|
||||
const normalisedHref = href.replace(/^https?:\/\/[^\/]+/, "");
|
||||
return normalisedHref;
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import xmlParser, { j2xParser as XMLParser } from "fast-xml-parser";
|
||||
|
||||
export function generateLockXML(ownerHREF: string): string {
|
||||
return getParser().parse(
|
||||
namespace(
|
||||
{
|
||||
lockinfo: {
|
||||
"@_xmlns:d": "DAV:",
|
||||
lockscope: {
|
||||
exclusive: {}
|
||||
},
|
||||
locktype: {
|
||||
write: {}
|
||||
},
|
||||
owner: {
|
||||
href: ownerHREF
|
||||
}
|
||||
}
|
||||
},
|
||||
"d"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getParser(): XMLParser {
|
||||
return new XMLParser({
|
||||
attributeNamePrefix: "@_",
|
||||
format: true,
|
||||
ignoreAttributes: false,
|
||||
supressEmptyNode: true
|
||||
});
|
||||
}
|
||||
|
||||
function namespace<T extends Object>(obj: T, ns: string): T {
|
||||
const copy = { ...obj };
|
||||
for (const key in copy) {
|
||||
if (copy[key] && typeof copy[key] === "object" && key.indexOf(":") === -1) {
|
||||
copy[`${ns}:${key}`] = namespace(copy[key], ns);
|
||||
delete copy[key];
|
||||
} else if (/^@_/.test(key) === false) {
|
||||
copy[`${ns}:${key}`] = copy[key];
|
||||
delete copy[key];
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
export function parseGenericResponse(xml: string): Object {
|
||||
return xmlParser.parse(xml, {
|
||||
arrayMode: false,
|
||||
ignoreNameSpace: true,
|
||||
parseAttributeValue: true,
|
||||
parseNodeValue: true
|
||||
});
|
||||
}
|
@ -0,0 +1,288 @@
|
||||
import Stream from "stream";
|
||||
|
||||
export type AuthHeader = string;
|
||||
|
||||
export enum AuthType {
|
||||
Digest = "digest",
|
||||
None = "none",
|
||||
Password = "password",
|
||||
Token = "token"
|
||||
}
|
||||
|
||||
export type BufferLike = Buffer | ArrayBuffer;
|
||||
|
||||
export interface CreateDirectoryOptions extends WebDAVMethodOptions {
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateReadStreamOptions extends WebDAVMethodOptions {
|
||||
callback?: (response: Response) => void;
|
||||
range?: {
|
||||
start: number;
|
||||
end?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type CreateWriteStreamCallback = (response: Response) => void;
|
||||
|
||||
export interface CreateWriteStreamOptions extends WebDAVMethodOptions {
|
||||
overwrite?: boolean;
|
||||
}
|
||||
|
||||
export interface DAVResultResponse {
|
||||
href: string;
|
||||
propstat: {
|
||||
prop: DAVResultResponseProps;
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DAVResultResponseProps {
|
||||
displayname: string;
|
||||
resourcetype: {
|
||||
collection?: boolean;
|
||||
};
|
||||
getlastmodified?: string;
|
||||
getetag?: string;
|
||||
getcontentlength?: string;
|
||||
getcontenttype?: string;
|
||||
"quota-available-bytes"?: any;
|
||||
"quota-used-bytes"?: string;
|
||||
}
|
||||
|
||||
export interface DAVResult {
|
||||
multistatus: {
|
||||
response: Array<DAVResultResponse>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DAVResultRawMultistatus {
|
||||
response: DAVResultResponse | [DAVResultResponse];
|
||||
}
|
||||
|
||||
export interface DAVResultRaw {
|
||||
multistatus: "" | DAVResultRawMultistatus | [DAVResultRawMultistatus];
|
||||
}
|
||||
|
||||
export interface DigestContext {
|
||||
username: string;
|
||||
password: string;
|
||||
nc: number;
|
||||
algorithm: string;
|
||||
hasDigestAuth: boolean;
|
||||
cnonce?: string;
|
||||
nonce?: string;
|
||||
realm?: string;
|
||||
qop?: string;
|
||||
opaque?: string;
|
||||
}
|
||||
|
||||
export interface DiskQuota {
|
||||
used: number;
|
||||
available: DiskQuotaAvailable;
|
||||
}
|
||||
|
||||
export type DiskQuotaAvailable = "unknown" | "unlimited" | number;
|
||||
|
||||
export enum ErrorCode {
|
||||
DataTypeNoLength = "data-type-no-length",
|
||||
InvalidAuthType = "invalid-auth-type",
|
||||
InvalidOutputFormat = "invalid-output-format",
|
||||
LinkUnsupportedAuthType = "link-unsupported-auth"
|
||||
}
|
||||
|
||||
export interface FileStat {
|
||||
filename: string;
|
||||
basename: string;
|
||||
lastmod: string;
|
||||
size: number;
|
||||
type: "file" | "directory";
|
||||
etag: string | null;
|
||||
mime?: string;
|
||||
props?: DAVResultResponseProps;
|
||||
}
|
||||
|
||||
export interface GetDirectoryContentsOptions extends WebDAVMethodOptions {
|
||||
deep?: boolean;
|
||||
details?: boolean;
|
||||
glob?: string;
|
||||
}
|
||||
|
||||
export interface GetFileContentsOptions extends WebDAVMethodOptions {
|
||||
details?: boolean;
|
||||
format?: "binary" | "text";
|
||||
}
|
||||
|
||||
export interface GetQuotaOptions extends WebDAVMethodOptions {
|
||||
details?: boolean;
|
||||
}
|
||||
|
||||
export interface Headers {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface LockOptions extends WebDAVMethodOptions {
|
||||
refreshToken?: string;
|
||||
timeout?: string;
|
||||
}
|
||||
|
||||
export interface LockResponse {
|
||||
serverTimeout: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface OAuthToken {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
refresh_token?: string;
|
||||
}
|
||||
|
||||
export interface PutFileContentsOptions extends WebDAVMethodOptions {
|
||||
contentLength?: boolean | number;
|
||||
overwrite?: boolean;
|
||||
onUploadProgress?: UploadProgressCallback;
|
||||
}
|
||||
|
||||
export type RequestDataPayload = string | Buffer | ArrayBuffer | { [key: string]: any };
|
||||
|
||||
interface RequestOptionsBase {
|
||||
data?: RequestDataPayload;
|
||||
headers?: Headers;
|
||||
httpAgent?: any;
|
||||
httpsAgent?: any;
|
||||
maxBodyLength?: number;
|
||||
maxContentLength?: number;
|
||||
maxRedirects?: number;
|
||||
method: string;
|
||||
onUploadProgress?: UploadProgressCallback;
|
||||
responseType?: string;
|
||||
transformResponse?: Array<(value: any) => any>;
|
||||
url?: string;
|
||||
validateStatus?: (status: number) => boolean;
|
||||
withCredentials?: boolean;
|
||||
}
|
||||
|
||||
export interface RequestOptionsCustom extends RequestOptionsBase {}
|
||||
|
||||
export interface RequestOptions extends RequestOptionsBase {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface RequestOptionsWithState extends RequestOptions {
|
||||
_digest?: DigestContext;
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
data: ResponseData;
|
||||
status: number;
|
||||
headers: Headers;
|
||||
statusText: string;
|
||||
}
|
||||
|
||||
export type ResponseData = string | Buffer | ArrayBuffer | Object | Array<any>;
|
||||
|
||||
export interface ResponseDataDetailed<T> {
|
||||
data: T;
|
||||
headers: Headers;
|
||||
status: number;
|
||||
statusText: string;
|
||||
}
|
||||
|
||||
export interface ResponseStatusValidator {
|
||||
(status: number): boolean;
|
||||
}
|
||||
|
||||
export interface StatOptions extends WebDAVMethodOptions {
|
||||
details?: boolean;
|
||||
}
|
||||
|
||||
export interface UploadProgress {
|
||||
loaded: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface UploadProgressCallback {
|
||||
(progress: UploadProgress): void;
|
||||
}
|
||||
|
||||
export interface WebDAVClient {
|
||||
copyFile: (filename: string, destination: string) => Promise<void>;
|
||||
createDirectory: (path: string, options?: CreateDirectoryOptions) => Promise<void>;
|
||||
createReadStream: (filename: string, options?: CreateReadStreamOptions) => Stream.Readable;
|
||||
createWriteStream: (
|
||||
filename: string,
|
||||
options?: CreateWriteStreamOptions,
|
||||
callback?: CreateWriteStreamCallback
|
||||
) => Stream.Writable;
|
||||
customRequest: (path: string, requestOptions: RequestOptionsCustom) => Promise<Response>;
|
||||
deleteFile: (filename: string) => Promise<void>;
|
||||
exists: (path: string) => Promise<boolean>;
|
||||
getDirectoryContents: (
|
||||
path: string,
|
||||
options?: GetDirectoryContentsOptions
|
||||
) => Promise<Array<FileStat> | ResponseDataDetailed<Array<FileStat>>>;
|
||||
getFileContents: (
|
||||
filename: string,
|
||||
options?: GetFileContentsOptions
|
||||
) => Promise<BufferLike | string | ResponseDataDetailed<BufferLike | string>>;
|
||||
getFileDownloadLink: (filename: string) => string;
|
||||
getFileUploadLink: (filename: string) => string;
|
||||
getHeaders: () => Headers;
|
||||
getQuota: (
|
||||
options?: GetQuotaOptions
|
||||
) => Promise<DiskQuota | null | ResponseDataDetailed<DiskQuota | null>>;
|
||||
lock: (path: string, options?: LockOptions) => Promise<LockResponse>;
|
||||
moveFile: (filename: string, destinationFilename: string) => Promise<void>;
|
||||
putFileContents: (
|
||||
filename: string,
|
||||
data: string | BufferLike | Stream.Readable,
|
||||
options?: PutFileContentsOptions
|
||||
) => Promise<boolean>;
|
||||
setHeaders: (headers: Headers) => void;
|
||||
stat: (
|
||||
path: string,
|
||||
options?: StatOptions
|
||||
) => Promise<FileStat | ResponseDataDetailed<FileStat>>;
|
||||
unlock: (path: string, token: string, options?: WebDAVMethodOptions) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface WebDAVClientContext {
|
||||
authType: AuthType;
|
||||
contactHref: string;
|
||||
digest?: DigestContext;
|
||||
headers: Headers;
|
||||
httpAgent?: any;
|
||||
httpsAgent?: any;
|
||||
maxBodyLength?: number;
|
||||
maxContentLength?: number;
|
||||
password?: string;
|
||||
remotePath: string;
|
||||
remoteURL: string;
|
||||
token?: OAuthToken;
|
||||
username?: string;
|
||||
withCredentials?: boolean;
|
||||
}
|
||||
|
||||
export interface WebDAVClientError extends Error {
|
||||
status?: number;
|
||||
response?: Response;
|
||||
}
|
||||
|
||||
export interface WebDAVClientOptions {
|
||||
authType?: AuthType;
|
||||
contactHref?: string;
|
||||
headers?: Headers;
|
||||
httpAgent?: any;
|
||||
httpsAgent?: any;
|
||||
maxBodyLength?: number;
|
||||
maxContentLength?: number;
|
||||
password?: string;
|
||||
token?: OAuthToken;
|
||||
username?: string;
|
||||
withCredentials?: boolean;
|
||||
}
|
||||
|
||||
export interface WebDAVMethodOptions {
|
||||
data?: RequestDataPayload;
|
||||
headers?: Headers;
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
!function(a,b,c){function d(c){var d=b.createElement("iframe"),e="https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid="+c.appid+"&agentid="+c.agentid+"&redirect_uri="+c.redirect_uri+"&state="+c.state+"&login_type=jssdk";e+=c.style?"&style="+c.style:"",e+=c.href?"&href="+c.href:"",d.src=e,d.frameBorder="0",d.allowTransparency="true",d.scrolling="no",d.width="300px",d.height="400px";var f=b.getElementById(c.id);f.innerHTML="",f.appendChild(d),d.onload=function(){d.contentWindow.postMessage&&a.addEventListener&&(a.addEventListener("message",function(b){
|
||||
b.data&&b.origin.indexOf("work.weixin.qq.com")>-1&&(a.location.href=b.data)}),d.contentWindow.postMessage("ask_usePostMessage","*"))}}a.WwLogin=d}(window,document);
|
@ -0,0 +1,86 @@
|
||||
|
||||
export interface modelsSimpleAuth {
|
||||
level: number
|
||||
name: string
|
||||
rid: string
|
||||
// RID: string
|
||||
// RUID: string
|
||||
}
|
||||
|
||||
|
||||
export const R = {
|
||||
// 应用管理配置权限
|
||||
App: 'app',
|
||||
// 用户管理和绑定应用权限
|
||||
User: 'user',
|
||||
// 权限资源定义权限
|
||||
Resource: 'resource',
|
||||
// 角色管理和绑定用户权限
|
||||
Role: 'role',
|
||||
// 权限管理和绑定角色权限
|
||||
Auth: 'auth',
|
||||
}
|
||||
|
||||
const level = {
|
||||
None: 0,
|
||||
Do: 1,
|
||||
Part: 1,
|
||||
Read: 2,
|
||||
Create: 3,
|
||||
Update: 4,
|
||||
Delete: 5,
|
||||
All: 6
|
||||
}
|
||||
|
||||
class authLevel {
|
||||
level = level.None
|
||||
constructor(level: number) {
|
||||
this.level = level
|
||||
}
|
||||
CanDo(): boolean {
|
||||
return this.level >= level.Do
|
||||
}
|
||||
CanRead(): boolean {
|
||||
return this.level >= level.Read
|
||||
}
|
||||
CanCreate(): boolean {
|
||||
return this.level >= level.Create
|
||||
}
|
||||
CanUpdate(): boolean {
|
||||
return this.level >= level.Update
|
||||
}
|
||||
CanDelete(): boolean {
|
||||
return this.level >= level.Delete
|
||||
}
|
||||
CanDoAny(): boolean {
|
||||
return this.level >= level.All
|
||||
}
|
||||
}
|
||||
|
||||
export class auths {
|
||||
private readonly list: modelsSimpleAuth[]
|
||||
|
||||
constructor(auths: modelsSimpleAuth[]) {
|
||||
this.list = auths
|
||||
}
|
||||
|
||||
Get(name: string, rid: string): authLevel {
|
||||
let l = level.None
|
||||
for (let i of this.list) {
|
||||
if (i.name == name && (i.rid === '' || i.rid === rid) && i.level > l) {
|
||||
l = i.level
|
||||
}
|
||||
}
|
||||
return new authLevel(l)
|
||||
}
|
||||
}
|
||||
|
||||
export interface Auths {
|
||||
Get(name: string, rid: string): authLevel
|
||||
}
|
||||
|
||||
|
||||
export function NewAuths(a: modelsSimpleAuth[]): Auths {
|
||||
return new auths(a)
|
||||
}
|
||||
|
@ -1,42 +1,137 @@
|
||||
<template>
|
||||
<q-page class="row items-center justify-evenly">
|
||||
<example-component
|
||||
title="Example component"
|
||||
active
|
||||
:todos="todos"
|
||||
:meta="meta"
|
||||
></example-component>
|
||||
</q-page>
|
||||
<div>
|
||||
<div v-if="ofApps.length > 0">
|
||||
<div class="flex justify-between">
|
||||
<h1 class="page-h1">我的应用</h1>
|
||||
<div class="my-5 mr-10">
|
||||
<q-btn @click="new_flag = true" v-if="store.state.user.auth.Get(R.App, '').CanCreate()">创建应用
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center">
|
||||
<div v-for="(item, k) in ofApps" class="flex items-center justify-center" :key="k">
|
||||
<AppCard :core="item"></AppCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-20" v-if="apps.length > 0">
|
||||
<h1 class="page-h1">应用中心</h1>
|
||||
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center">
|
||||
<div v-for="(item, k) in apps" class="flex items-center justify-center" :key="k">
|
||||
<AppCard :core="item"></AppCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-dialog v-model="new_flag">
|
||||
<q-card class="w-4/5 md:w-96 rounded-2xl">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Our Changing Planet</div>
|
||||
<div class="text-subtitle2">by John Doe</div>
|
||||
</q-card-section>
|
||||
<q-separator></q-separator>
|
||||
<q-card-section>
|
||||
<q-form @submit="create_new">
|
||||
<q-input label="应用名" v-model="temp_app.name"></q-input>
|
||||
<!-- <uploader url="test.ico" @success="(e) => { -->
|
||||
<!-- temp_app.icon = e; -->
|
||||
<!-- } -->
|
||||
<!-- "> -->
|
||||
<!-- <q-avatar size="large" round :src="temp_app.icon"> </q-avatar> -->
|
||||
<!-- </uploader> -->
|
||||
<div class="flex justify-end">
|
||||
<q-btn class="mx-3" @click="new_flag = false">取消</q-btn>
|
||||
<q-btn type="submit">创建</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Todo, Meta } from 'components/models';
|
||||
import ExampleComponent from 'components/ExampleComponent.vue';
|
||||
import { ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import api from 'src/boot/api';
|
||||
import msg from '@veypi/msg';
|
||||
import { modelsApp, modelsUser } from 'src/models';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { useUserStore } from 'src/stores/user';
|
||||
import AppCard from 'components/app.vue'
|
||||
|
||||
const todos = ref<Todo[]>([
|
||||
{
|
||||
id: 1,
|
||||
content: 'ct1'
|
||||
},
|
||||
|
||||
let apps = ref<modelsApp[]>([]);
|
||||
let ofApps = ref<modelsApp[]>([]);
|
||||
let $q = useQuasar()
|
||||
|
||||
function getApps() {
|
||||
$q.loadingBar.start()
|
||||
api.app.list().then(
|
||||
(e: modelsApp[]) => {
|
||||
apps.value = e;
|
||||
api.app
|
||||
.user("")
|
||||
.list(useUserStore().id)
|
||||
.then(
|
||||
(e: modelsUser[]) => {
|
||||
$q.loadingBar.stop();
|
||||
ofApps.value = [];
|
||||
console.log(e)
|
||||
// for (let i in e) {
|
||||
// let ai = apps.value.findIndex((a) => a.id === e[i]);
|
||||
// if (ai >= 0) {
|
||||
// apps.value[ai].UserStatus = e[i].Status;
|
||||
// if (e[i].Status === "ok") {
|
||||
// ofApps.value.push(apps.value[ai]);
|
||||
// apps.value.splice(ai, 1);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getApps();
|
||||
});
|
||||
|
||||
let new_flag = ref(false);
|
||||
let temp_app = ref({
|
||||
name: "",
|
||||
icon: "",
|
||||
});
|
||||
let form_ref = ref(null);
|
||||
let rules = {
|
||||
name: [
|
||||
{
|
||||
id: 2,
|
||||
content: 'ct2'
|
||||
required: true,
|
||||
validator(r: any, v: any) {
|
||||
return (
|
||||
(v && v.length >= 2 && v.length <= 16) || "长度要求2~16"
|
||||
);
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: 'ct3'
|
||||
trigger: ["input", "blur"],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
content: 'ct4'
|
||||
],
|
||||
};
|
||||
|
||||
function create_new() {
|
||||
form_ref.value.validate((e: any) => {
|
||||
if (!e) {
|
||||
api.app.create(temp_app.value.name, temp_app.value.icon).Start(
|
||||
(e) => {
|
||||
e.Status = "ok";
|
||||
ofApps.value.push(e);
|
||||
msg.success("创建成功");
|
||||
new_flag.value = false;
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
content: 'ct5'
|
||||
(e) => {
|
||||
msg.warning("创建失败: " + e);
|
||||
}
|
||||
);
|
||||
}
|
||||
]);
|
||||
const meta = ref<Meta>({
|
||||
totalCount: 1200
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="px-10 pb-9 pt-28 rounded-xl w-96">
|
||||
<q-form autofocus @submit="onSubmit" @reset="onReset">
|
||||
<q-input v-model="data.username" label="用户名" hint="username" lazy-rules :rules="data_rules.username" />
|
||||
<q-input v-model="data.password" :type="isPwd ? 'password' :
|
||||
'text'" hint="password" :rules="data_rules.password">
|
||||
<template v-slot:append>
|
||||
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer" @click="isPwd = !isPwd" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="flex justify-around mt-4">
|
||||
<q-btn label="注册" @click="router.push({ name: 'register' })" color="info"></q-btn>
|
||||
<q-btn label="登录" type="submit" color="primary" />
|
||||
<q-btn label="重置" type="reset" color="primary" flat class="q-ml-sm" />
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import api from 'src/boot/api'
|
||||
import msg from '@veypi/msg'
|
||||
import util from 'src/libs/util'
|
||||
import { useUserStore } from 'src/stores/user'
|
||||
import { useAppStore } from 'src/stores/app'
|
||||
import { modelsApp } from 'src/models'
|
||||
|
||||
|
||||
const app = useAppStore()
|
||||
const user = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
|
||||
let data = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
const data_rules = {
|
||||
username: [
|
||||
(v: string) => v && v.length >= 3 && v.length <= 16 || '长度要求3~16'
|
||||
],
|
||||
password: [
|
||||
(v: string) => v && v.length >= 6 && v.length <= 16 || '长度要求6~16'
|
||||
]
|
||||
}
|
||||
let isPwd = ref(true)
|
||||
|
||||
const onSubmit = () => {
|
||||
console.log(data.value)
|
||||
api.user.login(data.value.username,
|
||||
data.value.password).then((data: any) => {
|
||||
localStorage.auth_token = data.auth_token
|
||||
msg.Info('登录成功')
|
||||
user.fetchUserData()
|
||||
let url = route.query.redirect || data.redirect || '/'
|
||||
redirect(url)
|
||||
console.log(data)
|
||||
})
|
||||
}
|
||||
const onReset = () => {
|
||||
data.value.password = ''
|
||||
data.value.username = ''
|
||||
}
|
||||
|
||||
let uuid = computed(() => {
|
||||
return route.query.uuid
|
||||
})
|
||||
let ifLogOut = computed(() => {
|
||||
return route.query.logout === '1'
|
||||
})
|
||||
|
||||
|
||||
function redirect(url: string) {
|
||||
if (uuid.value && uuid.value !== app.id) {
|
||||
|
||||
api.app.get(uuid.value as string).then((app: modelsApp) => {
|
||||
api.token(uuid.value as string).get().then(e => {
|
||||
url = url || app.redirect
|
||||
console.log(e)
|
||||
// e = encodeURIComponent(e)
|
||||
// url = url.replaceAll('$token', e)
|
||||
window.location.href = url
|
||||
|
||||
})
|
||||
})
|
||||
} else if (util.checkLogin()) {
|
||||
if (url) {
|
||||
router.push(url)
|
||||
} else {
|
||||
router.push({ name: 'home' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!ifLogOut.value) {
|
||||
redirect('')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="px-10 pb-9 pt-28 rounded-xl w-96">
|
||||
<q-form @submit="register" autofocus>
|
||||
<q-input v-model="data.username" label="用户名" hint="username" lazy-rules :rules="rules.username"></q-input>
|
||||
<q-input label="密码" v-model="data.password" type="password" lazy-rules :rules="rules.password"></q-input>
|
||||
<q-input label="密码" v-model="data.pass" type="password" lazy-rules :rules="rules.pass"></q-input>
|
||||
<div class="flex justify-around mt-4">
|
||||
<q-btn label="注册" type="submit" color="primary" />
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import msg from '@veypi/msg'
|
||||
import api from "src/boot/api";
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
let data = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
pass: ''
|
||||
})
|
||||
let rules = {
|
||||
username: [
|
||||
(v: string) => v && v.length >= 3 && v.length <= 16 || '长度要求3~16'
|
||||
],
|
||||
password: [
|
||||
(v: string) => v && v.length >= 6 && v.length <= 16 || '长度要求6~16'
|
||||
],
|
||||
pass: [
|
||||
(v: string) => v && v === data.value.password || '密码不正确'
|
||||
]
|
||||
}
|
||||
|
||||
function register() {
|
||||
api.user.register(data.value.username, data.value.password).then(u => {
|
||||
console.log(u)
|
||||
msg.Info('注册成功')
|
||||
router.push({ name: 'login' })
|
||||
}).catch(e => {
|
||||
console.log(e)
|
||||
msg.Warn('注册失败:' + e.data)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* app.ts
|
||||
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||
* 2023-09-30 17:26
|
||||
* Distributed under terms of the MIT license.
|
||||
*/
|
||||
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
id: '',
|
||||
title: '',
|
||||
}),
|
||||
getters: {
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
});
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* user.ts
|
||||
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||
* 2023-09-22 21:05
|
||||
* Distributed under terms of the MIT license.
|
||||
*/
|
||||
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
import { Auths, modelsUser, NewAuths } from 'src/models';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Base64 } from 'js-base64'
|
||||
import api from 'src/boot/api';
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
id: '',
|
||||
local: {} as modelsUser,
|
||||
auth: {} as Auths,
|
||||
ready: false
|
||||
}),
|
||||
getters: {
|
||||
},
|
||||
actions: {
|
||||
logout() {
|
||||
this.ready = false
|
||||
localStorage.removeItem('auth_token')
|
||||
const r = useRouter()
|
||||
r.push({ name: 'login' })
|
||||
},
|
||||
fetchUserData() {
|
||||
let token = localStorage.getItem('auth_token')?.split('.');
|
||||
if (!token || token.length !== 3) {
|
||||
return false
|
||||
}
|
||||
let data = JSON.parse(Base64.decode(token[1]))
|
||||
if (data.id) {
|
||||
this.auth = NewAuths(data.Auth)
|
||||
api.user.get(data.id).then((e: modelsUser) => {
|
||||
console.log(e)
|
||||
this.id = e.id
|
||||
this.local = e
|
||||
this.ready = true
|
||||
}).catch((e) => {
|
||||
this.logout()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
Loading…
Reference in New Issue