]> OzVa Git service - ozva-cloud/commitdiff
feat: ui improves the login experience (#182)
authorsigoden <sigoden@gmail.com>
Tue, 21 Feb 2023 04:42:40 +0000 (12:42 +0800)
committerGitHub <noreply@github.com>
Tue, 21 Feb 2023 04:42:40 +0000 (12:42 +0800)
close #157 #158

assets/index.css
assets/index.html
assets/index.js
src/auth.rs
src/server.rs
tests/auth.rs

index ae2efb454e3b2c6876cbbc3d3b55be2cbb3ad51a..7f89a7e16ecbe6834aa0404357557e5d42ada6f9 100644 (file)
@@ -208,9 +208,12 @@ body {
   outline: none;
 }
 
-.save-btn {
+.toolbox2 {
   margin-left: auto;
   margin-right: 2em;
+}
+
+.save-btn {
   cursor: pointer;
   user-select: none;
 }
index 6694f357ed212dfa80b56d4bafe55be7931563ee..fee290cddea7b29aa2250cb7f20ddcfb933203e3 100644 (file)
       <input id="search" name="q" type="text" maxlength="128" autocomplete="off" tabindex="1">
       <input type="submit" hidden />
     </form>
-    <div class="save-btn hidden" title="Save file">
-      <svg viewBox="0 0 1024 1024" width="24" height="24">
-        <path
-          d="M426.666667 682.666667v42.666666h170.666666v-42.666666h-170.666666z m-42.666667-85.333334h298.666667v128h42.666666V418.133333L605.866667 298.666667H298.666667v426.666666h42.666666v-128h42.666667z m260.266667-384L810.666667 379.733333V810.666667H213.333333V213.333333h430.933334zM341.333333 341.333333h85.333334v170.666667H341.333333V341.333333z"
-          fill="#444444" p-id="8311"></path>
-      </svg>
+    <div class="toolbox2">
+      <div class="login-btn hidden" title="Login">
+        <svg width="16" height="16" viewBox="0 0 16 16">
+          <path fill-rule="evenodd"
+            d="M6 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 0-1 0v2A1.5 1.5 0 0 0 6.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-8A1.5 1.5 0 0 0 5 3.5v2a.5.5 0 0 0 1 0v-2z" />
+          <path fill-rule="evenodd"
+            d="M11.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H1.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z" />
+        </svg>
+      </div>
+      <div class="user-btn hidden">
+        <svg width="16" height="16" viewBox="0 0 16 16">
+          <path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z"/>
+        </svg>
+      </div>
+      <div class="save-btn hidden" title="Save file">
+        <svg viewBox="0 0 1024 1024" width="24" height="24">
+          <path
+            d="M426.666667 682.666667v42.666666h170.666666v-42.666666h-170.666666z m-42.666667-85.333334h298.666667v128h42.666666V418.133333L605.866667 298.666667H298.666667v426.666666h42.666666v-128h42.666667z m260.266667-384L810.666667 379.733333V810.666667H213.333333V213.333333h430.933334zM341.333333 341.333333h85.333334v170.666667H341.333333V341.333333z"
+            fill="#444444" p-id="8311"></path>
+        </svg>
+      </div>
     </div>
   </div>
   <div class="main">
index 4349c38595679eec23ce3e476d129563edaf32f3..28ff54c17b809b8d833c0c95268515f2dca06a83 100644 (file)
@@ -16,6 +16,8 @@
  * @property {boolean} allow_delete
  * @property {boolean} allow_search
  * @property {boolean} allow_archive
+ * @property {boolean} auth
+ * @property {string} user
  * @property {boolean} dir_exists
  * @property {string} editable
  */
@@ -71,6 +73,10 @@ let $emptyFolder;
  * @type Element
  */
 let $editor;
+/**
+ * @type Element
+ */
+let $userBtn;
 
 function ready() {
   $pathsTable = document.querySelector(".paths-table")
@@ -79,6 +85,7 @@ function ready() {
   $uploadersTable = document.querySelector(".uploaders-table");
   $emptyFolder = document.querySelector(".empty-folder");
   $editor = document.querySelector(".editor");
+  $userBtn = document.querySelector(".user-btn");
 
   addBreadcrumb(DATA.href, DATA.uri_prefix);
 
@@ -86,6 +93,10 @@ function ready() {
     document.title = `Index of ${DATA.href} - Dufs`;
     document.querySelector(".index-page").classList.remove("hidden");
 
+    if (DATA.auth) {
+      setupAuth();
+    }
+
     if (DATA.allow_search) {
       setupSearch()
     }
@@ -106,7 +117,6 @@ function ready() {
     document.title = `Edit of ${DATA.href} - Dufs`;
     document.querySelector(".editor-page").classList.remove("hidden");;
 
-
     setupEditor();
   }
 }
@@ -203,16 +213,22 @@ Uploader.globalIdx = 0;
 
 Uploader.runnings = 0;
 
+Uploader.auth = false;
+
 /**
  * @type Uploader[]
  */
 Uploader.queues = [];
 
 
-Uploader.runQueue = () => {
+Uploader.runQueue = async () => {
   if (Uploader.runnings > 2) return;
   let uploader = Uploader.queues.shift();
   if (!uploader) return;
+  if (!Uploader.auth) {
+    Uploader.auth = true;
+    await login();
+  }
   uploader.ajax();
 }
 
@@ -365,7 +381,7 @@ function addPath(file, index) {
     ${getPathSvg(file.path_type)}
   </td>
   <td class="path cell-name">
-    <a href="${url}">${encodedName}</a>
+    <a href="${url}" target="_blank">${encodedName}</a>
   </td>
   <td class="cell-mtime">${formatMtime(file.mtime)}</td>
   <td class="cell-size">${formatSize(file.size).join(" ")}</td>
@@ -385,10 +401,11 @@ async function deletePath(index) {
   if (!confirm(`Delete \`${file.name}\`?`)) return;
 
   try {
+    await login();
     const res = await fetch(newUrl(file.name), {
       method: "DELETE",
     });
-    await assertFetch(res);
+    await assertResOK(res);
     document.getElementById(`addPath${index}`).remove();
     DATA.paths[index] = null;
     if (!DATA.paths.find(v => !!v)) {
@@ -425,14 +442,15 @@ async function movePath(index) {
   const newFileUrl = fileUrlObj.origin + prefix + newPath.split("/").map(encodeURIComponent).join("/");
 
   try {
+    await login();
     const res = await fetch(fileUrl, {
       method: "MOVE",
       headers: {
         "Destination": newFileUrl,
       }
     });
-    await assertFetch(res);
-    location.href = newFileUrl.split("/").slice(0, -1).join("/")
+    await assertResOK(res);
+    location.href = newFileUrl.split("/").slice(0, -1).join("/");
   } catch (err) {
     alert(`Cannot move \`${filePath}\` to \`${newPath}\`, ${err.message}`);
   }
@@ -445,7 +463,7 @@ function dropzone() {
       e.stopPropagation();
     });
   });
-  document.addEventListener("drop", e => {
+  document.addEventListener("drop", async e => {
     if (!e.dataTransfer.items[0].webkitGetAsEntry) {
       const files = e.dataTransfer.files.filter(v => v.size > 0);
       for (const file of files) {
@@ -462,6 +480,18 @@ function dropzone() {
   });
 }
 
+function setupAuth() {
+  if (DATA.user) {
+    $userBtn.classList.remove("hidden");
+    $userBtn.title = DATA.user;
+  } else {
+    const $loginBtn = document.querySelector(".login-btn");
+    $loginBtn.classList.remove("hidden");
+    $loginBtn.addEventListener("click", () => login(true));
+  }
+}
+
+
 /**
  * Setup searchbar
  */
@@ -491,7 +521,7 @@ function setupUpload() {
     if (name) createFolder(name);
   });
   document.querySelector(".upload-file").classList.remove("hidden");
-  document.getElementById("file").addEventListener("change", e => {
+  document.getElementById("file").addEventListener("change", async e => {
     const files = e.target.files;
     for (let file of files) {
       new Uploader(file, []).upload();
@@ -527,7 +557,7 @@ async function setupEditor() {
   $editor.classList.remove("hidden");
   try {
     const res = await fetch(baseUrl());
-    await assertFetch(res);
+    await assertResOK(res);
     const text = await res.text();
     $editor.value = text;
   } catch (err) {
@@ -549,6 +579,24 @@ async function saveChange() {
   }
 }
 
+async function login(alert = false) {
+  if (!DATA.auth) return;
+  try {
+    const res = await fetch(baseUrl() + "?auth");
+    await assertResOK(res);
+    document.querySelector(".login-btn").classList.add("hidden");
+    $userBtn.classList.remove("hidden");
+    $userBtn.title = "";
+  } catch (err) {
+    let message = `Cannot login, ${err.message}`;
+    if (alert) {
+      alert(message);
+    } else {
+      throw new Error(message);
+    }
+  }
+}
+
 /**
  * Create a folder
  * @param {string} name 
@@ -556,10 +604,11 @@ async function saveChange() {
 async function createFolder(name) {
   const url = newUrl(name);
   try {
+    await login();
     const res = await fetch(url, {
       method: "MKCOL",
     });
-    await assertFetch(res);
+    await assertResOK(res);
     location.href = url;
   } catch (err) {
     alert(`Cannot create folder \`${name}\`, ${err.message}`);
@@ -569,11 +618,12 @@ async function createFolder(name) {
 async function createFile(name) {
   const url = newUrl(name);
   try {
+    await login();
     const res = await fetch(url, {
       method: "PUT",
       body: "",
     });
-    await assertFetch(res);
+    await assertResOK(res);
     location.href = url + "?edit";
   } catch (err) {
     alert(`Cannot create file \`${name}\`, ${err.message}`);
@@ -663,7 +713,7 @@ function encodedStr(rawStr) {
   });
 }
 
-async function assertFetch(res) {
+async function assertResOK(res) {
   if (!(res.status >= 200 && res.status < 300)) {
     throw new Error(await res.text())
   }
index e19bc16063658f9ee196e4ef39cdb2c7ec6e8b9e..cf51ce639764d83d0211324f6381cadd4e68f64c 100644 (file)
@@ -72,6 +72,10 @@ impl AccessControl {
         Ok(Self { rules })
     }
 
+    pub fn valid(&self) -> bool {
+        !self.rules.is_empty()
+    }
+
     pub fn guard(
         &self,
         path: &str,
@@ -134,6 +138,9 @@ impl GuardType {
     pub fn is_reject(&self) -> bool {
         *self == GuardType::Reject
     }
+    pub fn is_readwrite(&self) -> bool {
+        *self == GuardType::ReadWrite
+    }
 }
 
 fn sanitize_path(path: &str, uri_prefix: &str) -> String {
index d5e7b09ee2aa79a23dab39cb93c42976e97e5496..fa37d7559b3d891f462c9ad07350e05d54906964 100644 (file)
@@ -144,6 +144,18 @@ impl Server {
             return Ok(res);
         }
 
+        let query = req.uri().query().unwrap_or_default();
+        let query_params: HashMap<String, String> = form_urlencoded::parse(query.as_bytes())
+            .map(|(k, v)| (k.to_string(), v.to_string()))
+            .collect();
+
+        if query_params.contains_key("auth") {
+            if !guard_type.is_readwrite() {
+                self.auth_reject(&mut res);
+            }
+            return Ok(res);
+        }
+
         let head_only = method == Method::HEAD;
 
         if self.args.path_is_file {
@@ -170,11 +182,6 @@ impl Server {
 
         let path = path.as_path();
 
-        let query = req.uri().query().unwrap_or_default();
-        let query_params: HashMap<String, String> = form_urlencoded::parse(query.as_bytes())
-            .map(|(k, v)| (k.to_string(), v.to_string()))
-            .collect();
-
         let (is_miss, is_dir, is_file, size) = match fs::metadata(path).await.ok() {
             Some(meta) => (false, meta.is_dir(), meta.is_file(), meta.len()),
             None => (true, false, false, 0),
@@ -204,21 +211,32 @@ impl Server {
                             }
                             self.handle_zip_dir(path, head_only, &mut res).await?;
                         } else if allow_search && query_params.contains_key("q") {
-                            self.handle_search_dir(path, &query_params, head_only, &mut res)
+                            let user = self.retrieve_user(authorization);
+                            self.handle_search_dir(path, &query_params, head_only, user, &mut res)
                                 .await?;
                         } else {
+                            let user = self.retrieve_user(authorization);
                             self.handle_render_index(
                                 path,
                                 &query_params,
                                 headers,
                                 head_only,
+                                user,
                                 &mut res,
                             )
                             .await?;
                         }
                     } else if render_index || render_spa {
-                        self.handle_render_index(path, &query_params, headers, head_only, &mut res)
-                            .await?;
+                        let user = self.retrieve_user(authorization);
+                        self.handle_render_index(
+                            path,
+                            &query_params,
+                            headers,
+                            head_only,
+                            user,
+                            &mut res,
+                        )
+                        .await?;
                     } else if query_params.contains_key("zip") {
                         if !allow_archive {
                             status_not_found(&mut res);
@@ -226,15 +244,19 @@ impl Server {
                         }
                         self.handle_zip_dir(path, head_only, &mut res).await?;
                     } else if allow_search && query_params.contains_key("q") {
-                        self.handle_search_dir(path, &query_params, head_only, &mut res)
+                        let user = self.retrieve_user(authorization);
+                        self.handle_search_dir(path, &query_params, head_only, user, &mut res)
                             .await?;
                     } else {
-                        self.handle_ls_dir(path, true, &query_params, head_only, &mut res)
+                        let user = self.retrieve_user(authorization);
+                        self.handle_ls_dir(path, true, &query_params, head_only, user, &mut res)
                             .await?;
                     }
                 } else if is_file {
                     if query_params.contains_key("edit") {
-                        self.handle_edit_file(path, head_only, &mut res).await?;
+                        let user = self.retrieve_user(authorization);
+                        self.handle_edit_file(path, head_only, user, &mut res)
+                            .await?;
                     } else {
                         self.handle_send_file(path, headers, head_only, &mut res)
                             .await?;
@@ -243,7 +265,8 @@ impl Server {
                     self.handle_render_spa(path, headers, head_only, &mut res)
                         .await?;
                 } else if allow_upload && req_path.ends_with('/') {
-                    self.handle_ls_dir(path, false, &query_params, head_only, &mut res)
+                    let user = self.retrieve_user(authorization);
+                    self.handle_ls_dir(path, false, &query_params, head_only, user, &mut res)
                         .await?;
                 } else {
                     status_not_found(&mut res);
@@ -382,6 +405,7 @@ impl Server {
         exist: bool,
         query_params: &HashMap<String, String>,
         head_only: bool,
+        user: Option<String>,
         res: &mut Response,
     ) -> BoxResult<()> {
         let mut paths = vec![];
@@ -394,7 +418,7 @@ impl Server {
                 }
             }
         };
-        self.send_index(path, paths, exist, query_params, head_only, res)
+        self.send_index(path, paths, exist, query_params, head_only, user, res)
     }
 
     async fn handle_search_dir(
@@ -402,6 +426,7 @@ impl Server {
         path: &Path,
         query_params: &HashMap<String, String>,
         head_only: bool,
+        user: Option<String>,
         res: &mut Response,
     ) -> BoxResult<()> {
         let mut paths: Vec<PathItem> = vec![];
@@ -452,7 +477,7 @@ impl Server {
                 }
             }
         }
-        self.send_index(path, paths, true, query_params, head_only, res)
+        self.send_index(path, paths, true, query_params, head_only, user, res)
     }
 
     async fn handle_zip_dir(
@@ -495,6 +520,7 @@ impl Server {
         query_params: &HashMap<String, String>,
         headers: &HeaderMap<HeaderValue>,
         head_only: bool,
+        user: Option<String>,
         res: &mut Response,
     ) -> BoxResult<()> {
         let index_path = path.join(INDEX_NAME);
@@ -507,7 +533,7 @@ impl Server {
             self.handle_send_file(&index_path, headers, head_only, res)
                 .await?;
         } else if self.args.render_try_index {
-            self.handle_ls_dir(path, true, query_params, head_only, res)
+            self.handle_ls_dir(path, true, query_params, head_only, user, res)
                 .await?;
         } else {
             status_not_found(res)
@@ -682,6 +708,7 @@ impl Server {
         &self,
         path: &Path,
         head_only: bool,
+        user: Option<String>,
         res: &mut Response,
     ) -> BoxResult<()> {
         let (file, meta) = tokio::join!(fs::File::open(path), fs::metadata(path),);
@@ -696,6 +723,8 @@ impl Server {
             uri_prefix: self.args.uri_prefix.clone(),
             allow_upload: self.args.allow_upload,
             allow_delete: self.args.allow_delete,
+            auth: self.args.auth.valid(),
+            user,
             editable,
         };
         res.headers_mut()
@@ -842,6 +871,7 @@ impl Server {
         Ok(())
     }
 
+    #[allow(clippy::too_many_arguments)]
     fn send_index(
         &self,
         path: &Path,
@@ -849,6 +879,7 @@ impl Server {
         exist: bool,
         query_params: &HashMap<String, String>,
         head_only: bool,
+        user: Option<String>,
         res: &mut Response,
     ) -> BoxResult<()> {
         if let Some(sort) = query_params.get("sort") {
@@ -903,6 +934,8 @@ impl Server {
             allow_search: self.args.allow_search,
             allow_archive: self.args.allow_archive,
             dir_exists: exist,
+            auth: self.args.auth.valid(),
+            user,
             paths,
         };
         let output = if query_params.contains_key("json") {
@@ -1057,6 +1090,10 @@ impl Server {
             size,
         }))
     }
+
+    fn retrieve_user(&self, authorization: Option<&HeaderValue>) -> Option<String> {
+        self.args.auth_method.get_user(authorization?)
+    }
 }
 
 #[derive(Debug, Serialize)]
@@ -1075,6 +1112,8 @@ struct IndexData {
     allow_search: bool,
     allow_archive: bool,
     dir_exists: bool,
+    auth: bool,
+    user: Option<String>,
     paths: Vec<PathItem>,
 }
 
@@ -1085,6 +1124,8 @@ struct EditData {
     uri_prefix: String,
     allow_upload: bool,
     allow_delete: bool,
+    auth: bool,
+    user: Option<String>,
     editable: bool,
 }
 
index 83b6434474d4394ca9024e7dcb6493167c2e03b9..39e5757c8849298b0408ae35fae5222e8e66cdb8 100644 (file)
@@ -45,6 +45,20 @@ fn auth_skip_on_options_method(
     Ok(())
 }
 
+#[rstest]
+fn auth_check(
+    #[with(&["--auth", "/@user:pass@user2:pass2", "-A"])] server: TestServer,
+) -> Result<(), Error> {
+    let url = format!("{}index.html?auth", server.url());
+    let resp = fetch!(b"GET", &url).send()?;
+    assert_eq!(resp.status(), 401);
+    let resp = fetch!(b"GET", &url).send_with_digest_auth("user2", "pass2")?;
+    assert_eq!(resp.status(), 401);
+    let resp = fetch!(b"GET", &url).send_with_digest_auth("user", "pass")?;
+    assert_eq!(resp.status(), 200);
+    Ok(())
+}
+
 #[rstest]
 fn auth_readonly(
     #[with(&["--auth", "/@user:pass@user2:pass2", "-A"])] server: TestServer,