feat: migrate to vue 3 (#2689)

---------

Co-authored-by: Joep <jcbuhre@gmail.com>
Co-authored-by: Omar Hussein <omarmohammad1951@gmail.com>
Co-authored-by: Oleg Lobanov <oleg@lobanov.me>
This commit is contained in:
kloon15
2024-04-01 17:18:22 +02:00
committed by GitHub
parent 0e3b35b30e
commit 5100e587d7
164 changed files with 12202 additions and 8047 deletions

View File

@@ -1,92 +0,0 @@
import store from "@/store";
import router from "@/router";
import { Base64 } from "js-base64";
import { baseURL } from "@/utils/constants";
export function parseToken(token) {
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("token malformed");
}
const data = JSON.parse(Base64.decode(parts[1]));
document.cookie = `auth=${token}; path=/`;
localStorage.setItem("jwt", token);
store.commit("setJWT", token);
store.commit("setUser", data.user);
}
export async function validateLogin() {
try {
if (localStorage.getItem("jwt")) {
await renew(localStorage.getItem("jwt"));
}
} catch (_) {
console.warn("Invalid JWT token in storage"); // eslint-disable-line
}
}
export async function login(username, password, recaptcha) {
const data = { username, password, recaptcha };
const res = await fetch(`${baseURL}/api/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const body = await res.text();
if (res.status === 200) {
parseToken(body);
} else {
throw new Error(body);
}
}
export async function renew(jwt) {
const res = await fetch(`${baseURL}/api/renew`, {
method: "POST",
headers: {
"X-Auth": jwt,
},
});
const body = await res.text();
if (res.status === 200) {
parseToken(body);
} else {
throw new Error(body);
}
}
export async function signup(username, password) {
const data = { username, password };
const res = await fetch(`${baseURL}/api/signup`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (res.status !== 200) {
throw new Error(res.status);
}
}
export function logout() {
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
store.commit("setJWT", "");
store.commit("setUser", null);
localStorage.setItem("jwt", null);
router.push({ path: "/login" });
}

102
frontend/src/utils/auth.ts Normal file
View File

@@ -0,0 +1,102 @@
import { useAuthStore } from "@/stores/auth";
import router from "@/router";
import { JwtPayload, jwtDecode } from "jwt-decode";
import { baseURL } from "./constants";
import { StatusError } from "@/api/utils";
export function parseToken(token: string) {
// falsy or malformed jwt will throw InvalidTokenError
const data = jwtDecode<JwtPayload & { user: IUser }>(token);
document.cookie = `auth=${token}; Path=/; SameSite=Strict;`;
localStorage.setItem("jwt", token);
const authStore = useAuthStore();
authStore.jwt = token;
authStore.setUser(data.user);
}
export async function validateLogin() {
try {
if (localStorage.getItem("jwt")) {
await renew(<string>localStorage.getItem("jwt"));
}
} catch (error) {
console.warn("Invalid JWT token in storage"); // eslint-disable-line
throw error;
}
}
export async function login(
username: string,
password: string,
recaptcha: string
) {
const data = { username, password, recaptcha };
const res = await fetch(`${baseURL}/api/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const body = await res.text();
if (res.status === 200) {
parseToken(body);
} else {
throw new StatusError(
body || `${res.status} ${res.statusText}`,
res.status
);
}
}
export async function renew(jwt: string) {
const res = await fetch(`${baseURL}/api/renew`, {
method: "POST",
headers: {
"X-Auth": jwt,
},
});
const body = await res.text();
if (res.status === 200) {
parseToken(body);
} else {
throw new StatusError(
body || `${res.status} ${res.statusText}`,
res.status
);
}
}
export async function signup(username: string, password: string) {
const data = { username, password };
const res = await fetch(`${baseURL}/api/signup`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (res.status !== 200) {
throw new StatusError(`${res.status} ${res.statusText}`, res.status);
}
}
export function logout() {
document.cookie = "auth=; Max-Age=0; Path=/; SameSite=Strict;";
const authStore = useAuthStore();
authStore.clearUser();
localStorage.setItem("jwt", "");
router.push({ path: "/login" });
}

View File

@@ -1,70 +0,0 @@
function loading(button) {
let el = document.querySelector(`#${button}-button > i`);
if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line
return;
}
if (el.innerHTML == "autorenew" || el.innerHTML == "done") {
return;
}
el.dataset.icon = el.innerHTML;
el.style.opacity = 0;
setTimeout(() => {
el.classList.add("spin");
el.innerHTML = "autorenew";
el.style.opacity = 1;
}, 100);
}
function done(button) {
let el = document.querySelector(`#${button}-button > i`);
if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line
return;
}
el.style.opacity = 0;
setTimeout(() => {
el.classList.remove("spin");
el.innerHTML = el.dataset.icon;
el.style.opacity = 1;
}, 100);
}
function success(button) {
let el = document.querySelector(`#${button}-button > i`);
if (el === undefined || el === null) {
console.log('Error getting button ' + button) // eslint-disable-line
return;
}
el.style.opacity = 0;
setTimeout(() => {
el.classList.remove("spin");
el.innerHTML = "done";
el.style.opacity = 1;
setTimeout(() => {
el.style.opacity = 0;
setTimeout(() => {
el.innerHTML = el.dataset.icon;
el.style.opacity = 1;
}, 100);
}, 500);
}, 100);
}
export default {
loading,
done,
success,
};

View File

@@ -0,0 +1,83 @@
function loading(button: string) {
const el: HTMLButtonElement | null = document.querySelector(
`#${button}-button > i`
);
if (el === undefined || el === null) {
console.log("Error getting button " + button); // eslint-disable-line
return;
}
if (el.innerHTML == "autorenew" || el.innerHTML == "done") {
return;
}
el.dataset.icon = el.innerHTML;
el.style.opacity = "0";
setTimeout(() => {
if (el) {
el.classList.add("spin");
el.innerHTML = "autorenew";
el.style.opacity = "1";
}
}, 100);
}
function done(button: string) {
const el: HTMLButtonElement | null = document.querySelector(
`#${button}-button > i`
);
if (el === undefined || el === null) {
console.log("Error getting button " + button); // eslint-disable-line
return;
}
el.style.opacity = "0";
setTimeout(() => {
if (el !== null) {
el.classList.remove("spin");
el.innerHTML = el?.dataset?.icon || "";
el.style.opacity = "1";
}
}, 100);
}
function success(button: string) {
const el: HTMLButtonElement | null = document.querySelector(
`#${button}-button > i`
);
if (el === undefined || el === null) {
console.log("Error getting button " + button); // eslint-disable-line
return;
}
el.style.opacity = "0";
setTimeout(() => {
if (el !== null) {
el.classList.remove("spin");
el.innerHTML = "done";
el.style.opacity = "1";
}
setTimeout(() => {
if (el) el.style.opacity = "0";
setTimeout(() => {
if (el !== null) {
el.innerHTML = el?.dataset?.icon || "";
el.style.opacity = "1";
}
}, 100);
}, 500);
}, 100);
}
export default {
loading,
done,
success,
};

View File

@@ -0,0 +1,66 @@
// Based on code provided by Amir Fo
// https://stackoverflow.com/a/74528564
export function copy(text: string) {
return new Promise<void>((resolve, reject) => {
if (
typeof navigator !== "undefined" &&
typeof navigator.clipboard !== "undefined" &&
// @ts-ignore
navigator.permissions !== "undefined"
) {
navigator.permissions
// @ts-ignore
.query({ name: "clipboard-write" })
.then((permission) => {
if (permission.state === "granted" || permission.state === "prompt") {
const type = "text/plain";
const blob = new Blob([text], { type });
const data = [new ClipboardItem({ [type]: blob })];
navigator.clipboard.write(data).then(resolve).catch(reject);
} else {
reject(new Error("Permission not granted!"));
}
})
.catch((e) => {
// Firefox doesnt support clipboard-write permission
if (navigator.userAgent.indexOf("Firefox") != -1) {
navigator.clipboard.writeText(text).then(resolve).catch(reject);
} else {
reject(e);
}
});
} else if (
document.queryCommandSupported &&
document.queryCommandSupported("copy")
) {
const textarea = document.createElement("textarea");
textarea.textContent = text;
textarea.setAttribute("readonly", "");
textarea.style.fontSize = "12pt";
textarea.style.position = "fixed";
textarea.style.width = "2em";
textarea.style.height = "2em";
textarea.style.padding = "0";
textarea.style.margin = "0";
textarea.style.border = "none";
textarea.style.outline = "none";
textarea.style.boxShadow = "none";
textarea.style.background = "transparent";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand("copy");
document.body.removeChild(textarea);
resolve();
} catch (e) {
document.body.removeChild(textarea);
reject(e);
}
} else {
reject(
new Error("None of copying methods are supported by this browser!")
);
}
});
}

View File

@@ -1,42 +0,0 @@
const name = window.FileBrowser.Name || "File Browser";
const disableExternal = window.FileBrowser.DisableExternal;
const disableUsedPercentage = window.FileBrowser.DisableUsedPercentage;
const baseURL = window.FileBrowser.BaseURL;
const staticURL = window.FileBrowser.StaticURL;
const recaptcha = window.FileBrowser.ReCaptcha;
const recaptchaKey = window.FileBrowser.ReCaptchaKey;
const signup = window.FileBrowser.Signup;
const version = window.FileBrowser.Version;
const logoURL = `${staticURL}/img/logo.svg`;
const noAuth = window.FileBrowser.NoAuth;
const authMethod = window.FileBrowser.AuthMethod;
const loginPage = window.FileBrowser.LoginPage;
const theme = window.FileBrowser.Theme;
const enableThumbs = window.FileBrowser.EnableThumbs;
const resizePreview = window.FileBrowser.ResizePreview;
const enableExec = window.FileBrowser.EnableExec;
const tusSettings = window.FileBrowser.TusSettings;
const origin = window.location.origin;
const tusEndpoint = `/api/tus`;
export {
name,
disableExternal,
disableUsedPercentage,
baseURL,
logoURL,
recaptcha,
recaptchaKey,
signup,
version,
noAuth,
authMethod,
loginPage,
theme,
enableThumbs,
resizePreview,
enableExec,
tusSettings,
origin,
tusEndpoint,
};

View File

@@ -0,0 +1,42 @@
const name: string = window.FileBrowser.Name || "File Browser";
const disableExternal: boolean = window.FileBrowser.DisableExternal;
const disableUsedPercentage: boolean = window.FileBrowser.DisableUsedPercentage;
const baseURL: string = window.FileBrowser.BaseURL;
const staticURL: string = window.FileBrowser.StaticURL;
const recaptcha: string = window.FileBrowser.ReCaptcha;
const recaptchaKey: string = window.FileBrowser.ReCaptchaKey;
const signup: boolean = window.FileBrowser.Signup;
const version: string = window.FileBrowser.Version;
const logoURL = `${staticURL}/img/logo.svg`;
const noAuth: boolean = window.FileBrowser.NoAuth;
const authMethod = window.FileBrowser.AuthMethod;
const loginPage: boolean = window.FileBrowser.LoginPage;
const theme: UserTheme = window.FileBrowser.Theme;
const enableThumbs: boolean = window.FileBrowser.EnableThumbs;
const resizePreview: boolean = window.FileBrowser.ResizePreview;
const enableExec: boolean = window.FileBrowser.EnableExec;
const tusSettings = window.FileBrowser.TusSettings;
const origin = window.location.origin;
const tusEndpoint = `/api/tus`;
export {
name,
disableExternal,
disableUsedPercentage,
baseURL,
logoURL,
recaptcha,
recaptchaKey,
signup,
version,
noAuth,
authMethod,
loginPage,
theme,
enableThumbs,
resizePreview,
enableExec,
tusSettings,
origin,
tusEndpoint,
};

View File

@@ -1,5 +1,5 @@
export default function (name) {
let re = new RegExp(
export default function (name: string) {
const re = new RegExp(
"(?:(?:^|.*;\\s*)" + name + "\\s*\\=\\s*([^;]*).*$)|^.*$"
);
return document.cookie.replace(re, "$1");

View File

@@ -1,10 +1,10 @@
export default function getRule(rules) {
export default function getRule(rules: string[]) {
for (let i = 0; i < rules.length; i++) {
rules[i] = rules[i].toLowerCase();
}
let result = null;
let find = Array.prototype.find;
const find = Array.prototype.find;
find.call(document.styleSheets, (styleSheet) => {
result = find.call(styleSheet.cssRules, (cssRule) => {

View File

@@ -0,0 +1,34 @@
import { theme } from "./constants";
export const getTheme = (): UserTheme => {
return (document.documentElement.className as UserTheme) || theme;
};
export const setTheme = (theme: UserTheme) => {
const html = document.documentElement;
if (!theme) {
html.className = getMediaPreference();
} else {
html.className = theme;
}
};
export const toggleTheme = (): void => {
const activeTheme = getTheme();
if (activeTheme === "light") {
setTheme("dark");
} else {
setTheme("light");
}
};
export const getMediaPreference = (): UserTheme => {
const hasDarkPreference = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
if (hasDarkPreference) {
return "dark";
} else {
return "light";
}
};

View File

@@ -1,138 +0,0 @@
import store from "@/store";
import url from "@/utils/url";
export function checkConflict(files, items) {
if (typeof items === "undefined" || items === null) {
items = [];
}
let folder_upload = files[0].fullPath !== undefined;
let conflict = false;
for (let i = 0; i < files.length; i++) {
let file = files[i];
let name = file.name;
if (folder_upload) {
let dirs = file.fullPath.split("/");
if (dirs.length > 1) {
name = dirs[0];
}
}
let res = items.findIndex(function hasConflict(element) {
return element.name === this;
}, name);
if (res >= 0) {
conflict = true;
break;
}
}
return conflict;
}
export function scanFiles(dt) {
return new Promise((resolve) => {
let reading = 0;
const contents = [];
if (dt.items !== undefined) {
for (let item of dt.items) {
if (
item.kind === "file" &&
typeof item.webkitGetAsEntry === "function"
) {
const entry = item.webkitGetAsEntry();
readEntry(entry);
}
}
} else {
resolve(dt.files);
}
function readEntry(entry, directory = "") {
if (entry.isFile) {
reading++;
entry.file((file) => {
reading--;
file.fullPath = `${directory}${file.name}`;
contents.push(file);
if (reading === 0) {
resolve(contents);
}
});
} else if (entry.isDirectory) {
const dir = {
isDir: true,
size: 0,
fullPath: `${directory}${entry.name}`,
name: entry.name,
};
contents.push(dir);
readReaderContent(entry.createReader(), `${directory}${entry.name}`);
}
}
function readReaderContent(reader, directory) {
reading++;
reader.readEntries(function (entries) {
reading--;
if (entries.length > 0) {
for (const entry of entries) {
readEntry(entry, `${directory}/`);
}
readReaderContent(reader, `${directory}/`);
}
if (reading === 0) {
resolve(contents);
}
});
}
});
}
function detectType(mimetype) {
if (mimetype.startsWith("video")) return "video";
if (mimetype.startsWith("audio")) return "audio";
if (mimetype.startsWith("image")) return "image";
if (mimetype.startsWith("pdf")) return "pdf";
if (mimetype.startsWith("text")) return "text";
return "blob";
}
export function handleFiles(files, base, overwrite = false) {
for (let i = 0; i < files.length; i++) {
let id = store.state.upload.id;
let path = base;
let file = files[i];
if (file.fullPath !== undefined) {
path += url.encodePath(file.fullPath);
} else {
path += url.encodeRFC5987ValueChars(file.name);
}
if (file.isDir) {
path += "/";
}
const item = {
id,
path,
file,
overwrite,
...(!file.isDir && { type: detectType(file.type) }),
};
store.dispatch("upload/upload", item);
}
}

View File

@@ -0,0 +1,154 @@
import { useUploadStore } from "@/stores/upload";
import url from "@/utils/url";
export function checkConflict(
files: UploadList,
dest: ResourceItem[]
): boolean {
if (typeof dest === "undefined" || dest === null) {
dest = [];
}
const folder_upload = files[0].fullPath !== undefined;
const names = new Set<string>();
for (let i = 0; i < files.length; i++) {
const file = files[i];
let name = file.name;
if (folder_upload) {
const dirs = file.fullPath?.split("/");
if (dirs && dirs.length > 1) {
name = dirs[0];
}
}
names.add(name);
}
return dest.some((d) => names.has(d.name));
}
export function scanFiles(dt: DataTransfer): Promise<UploadList | FileList> {
return new Promise((resolve) => {
let reading = 0;
const contents: UploadList = [];
if (dt.items) {
// ts didnt like the for of loop even tho
// it is the official example on MDN
// for (const item of dt.items) {
for (let i = 0; i < dt.items.length; i++) {
const item = dt.items[i];
if (
item.kind === "file" &&
typeof item.webkitGetAsEntry === "function"
) {
const entry = item.webkitGetAsEntry();
entry && readEntry(entry);
}
}
} else {
resolve(dt.files);
}
function readEntry(entry: FileSystemEntry, directory = ""): void {
if (entry.isFile) {
reading++;
(entry as FileSystemFileEntry).file((file) => {
reading--;
contents.push({
file,
name: file.name,
size: file.size,
isDir: false,
fullPath: `${directory}${file.name}`,
});
if (reading === 0) {
resolve(contents);
}
});
} else if (entry.isDirectory) {
const dir = {
isDir: true,
size: 0,
fullPath: `${directory}${entry.name}`,
name: entry.name,
};
contents.push(dir);
readReaderContent(
(entry as FileSystemDirectoryEntry).createReader(),
`${directory}${entry.name}`
);
}
}
function readReaderContent(
reader: FileSystemDirectoryReader,
directory: string
): void {
reading++;
reader.readEntries((entries) => {
reading--;
if (entries.length > 0) {
for (const entry of entries) {
readEntry(entry, `${directory}/`);
}
readReaderContent(reader, `${directory}/`);
}
if (reading === 0) {
resolve(contents);
}
});
}
});
}
function detectType(mimetype: string): ResourceType {
if (mimetype.startsWith("video")) return "video";
if (mimetype.startsWith("audio")) return "audio";
if (mimetype.startsWith("image")) return "image";
if (mimetype.startsWith("pdf")) return "pdf";
if (mimetype.startsWith("text")) return "text";
return "blob";
}
export function handleFiles(
files: UploadList,
base: string,
overwrite = false
) {
const uploadStore = useUploadStore();
for (const file of files) {
const id = uploadStore.id;
let path = base;
if (file.fullPath !== undefined) {
path += url.encodePath(file.fullPath);
} else {
path += url.encodeRFC5987ValueChars(file.name);
}
if (file.isDir) {
path += "/";
}
const item: UploadItem = {
id,
path,
file,
overwrite,
...(!file.isDir && { type: detectType((file.file as File).type) }),
};
uploadStore.upload(item);
}
}

View File

@@ -1,36 +0,0 @@
export function removeLastDir(url) {
var arr = url.split("/");
if (arr.pop() === "") {
arr.pop();
}
return arr.join("/");
}
// this code borrow from mozilla
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Examples
export function encodeRFC5987ValueChars(str) {
return (
encodeURIComponent(str)
// Note that although RFC3986 reserves "!", RFC5987 does not,
// so we do not need to escape it
.replace(/['()]/g, escape) // i.e., %27 %28 %29
.replace(/\*/g, "%2A")
// The following are not required for percent-encoding per RFC5987,
// so we can allow for a little better readability over the wire: |`^
.replace(/%(?:7C|60|5E)/g, unescape)
);
}
export function encodePath(str) {
return str
.split("/")
.map((v) => encodeURIComponent(v))
.join("/");
}
export default {
encodeRFC5987ValueChars,
removeLastDir,
encodePath,
};

42
frontend/src/utils/url.ts Normal file
View File

@@ -0,0 +1,42 @@
export function removeLastDir(url: string) {
const arr = url.split("/");
if (arr.pop() === "") {
arr.pop();
}
return arr.join("/");
}
// this function is taken from mozilla
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Examples
export function encodeRFC5987ValueChars(str: string) {
return (
encodeURIComponent(str)
// The following creates the sequences %27 %28 %29 %2A (Note that
// the valid encoding of "*" is %2A, which necessitates calling
// toUpperCase() to properly encode). Although RFC3986 reserves "!",
// RFC5987 does not, so we do not need to escape it.
.replace(
/['()*]/g,
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
)
// The following are not required for percent-encoding per RFC5987,
// so we can allow for a little better readability over the wire: |`^
.replace(/%(7C|60|5E)/g, (str, hex) =>
String.fromCharCode(parseInt(hex, 16))
)
);
}
export function encodePath(str: string) {
return str
.split("/")
.map((v) => encodeURIComponent(v))
.join("/");
}
export default {
encodeRFC5987ValueChars,
removeLastDir,
encodePath,
};

View File

@@ -1,68 +0,0 @@
import Vue from "vue";
import Noty from "noty";
import VueLazyload from "vue-lazyload";
import i18n from "@/i18n";
import { disableExternal } from "@/utils/constants";
import AsyncComputed from "vue-async-computed";
Vue.use(VueLazyload);
Vue.use(AsyncComputed);
Vue.config.productionTip = true;
const notyDefault = {
type: "info",
layout: "bottomRight",
timeout: 1000,
progressBar: true,
};
Vue.prototype.$noty = (opts) => {
new Noty(Object.assign({}, notyDefault, opts)).show();
};
Vue.prototype.$showSuccess = (message) => {
new Noty(
Object.assign({}, notyDefault, {
text: message,
type: "success",
})
).show();
};
Vue.prototype.$showError = (error, displayReport = true) => {
let btns = [
Noty.button(i18n.t("buttons.close"), "", function () {
n.close();
}),
];
if (!disableExternal && displayReport) {
btns.unshift(
Noty.button(i18n.t("buttons.reportIssue"), "", function () {
window.open(
"https://github.com/filebrowser/filebrowser/issues/new/choose"
);
})
);
}
let n = new Noty(
Object.assign({}, notyDefault, {
text: error.message || error,
type: "error",
timeout: null,
buttons: btns,
})
);
n.show();
};
Vue.directive("focus", {
inserted: function (el) {
el.focus();
},
});
export default Vue;