cratery 1.11.1

Cratery -- a private cargo registry
<!DOCTYPE html>
<html lang="en" class="dark">

<head>
  <meta charset="UTF-8">
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/png" href="./favicon.png">
  <title>
    Cratery -- My account
  </title>
  <script src="https://cdn.tailwindcss.com"></script>
</head>

<header style="position: sticky; top: 0;">
  <nav class="bg-white border-gray-200 px-4 lg:px-6 py-2.5 dark:bg-gray-800">
      <div class="flex flex-wrap justify-between items-center mx-auto max-w-screen-xl">
          <a href="/webapp/index.html" class="flex items-center">
              <picture>
                  <source srcset="./logo-white.svg" media="(prefers-color-scheme: dark)" />
                  <source srcset="./logo-black.svg" media="(prefers-color-scheme: light)" />
                  <img src="./logo-white.svg" class="mr-3 h-6 sm:h-9" style="min-width: 200px;" alt="Cratery Logo" />
              </picture>
          </a>
          <div class="flex items-center lg:order-2">
            <a id="link-admin" href="/webapp/admin.html" style="cursor: pointer;" class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">Admin</a>
            <a id="link-account" href="/webapp/account.html" style="cursor: pointer;" class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">My Account</a>
            <a onclick="doLogout()" style="cursor: pointer;" class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">Logout</a>
          </div>
      </div>
  </nav>
</header>
<body onload="doPageLoad()" class="bg-white dark:bg-gray-800 content-center">
  <section class="bg-white dark:bg-gray-900 max-w-screen-lg mx-auto">
    <div class="py-8 lg:py-16 px-4 mx-auto max-w-screen-md">
        <form class="space-y-8">
            <div>
                <label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">Email</label>
                <input type="email" id="email" class="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light" disabled>
            </div>
            <div>
                <label for="login" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">Login (for token authentication)</label>
                <input type="text" id="login" class="block p-3 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light">
            </div>
            <div>
                <label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">Full name</label>
                <input type="text" id="name" class="block p-3 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light">
            </div>
            <div>
                <label for="roles" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">Roles</label>
                <input type="text" id="roles" class="block p-3 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light" disabled>
            </div>
        </form>
      </div>
      <div class="py-8 lg:py-16 px-4 mx-auto max-w-screen-md">
        <h2 class="mb-4 text-4xl tracking-tight font-extrabold text-center text-gray-900 dark:text-white">Tokens</h2>
        <div class="relative overflow-x-auto space-y-8">
          <button type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800" onclick="onOpenCreateToken()">Create new token</button>
          <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
              <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
                  <tr>
                      <th scope="col" class="px-6 py-3">
                        Token name
                      </th>
                      <th scope="col" class="px-6 py-3">
                        Last used
                      </th>
                      <th scope="col" class="px-6 py-3">
                        Accesses
                      </th>
                      <th scope="col" class="px-6 py-3">
                        Revoke
                      </th>
                  </tr>
              </thead>
              <tbody id="tokens">
              </tbody>
          </table>
        </div>
    </div>
  </section>
  <div id="modal-create-token" tabindex="-1" class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 w-full md:inset-0 h-modal md:h-full" style="display: none;">
    <div class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-51 w-full md:inset-0 h-modal md:h-full" style="background-color: black; opacity: 0.75;"></div>
    <div class="relative" style="margin: auto; margin-top: 10%; width: 800px;">
        <div class="relative p-4 bg-white rounded-lg shadow dark:bg-gray-800 md:p-8">
            <div class="mb-4 text-sm font-light text-gray-500 dark:text-gray-400">
              <h3 class="mb-3 text-2xl font-bold text-gray-900 dark:text-white">Create new token</h3>
            </div>
            <form class="mb-3 space-y-8">
              <div>
                <label for="new-token-name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">Token name</label>
                <input type="text" id="new-token-name" class="block p-3 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light">
              </div>
              <div>
                <label class="relative inline-flex items-center cursor-pointer">
                  <input id="new-token-can-write" type="checkbox" class="sr-only peer">
                  <div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
                  <span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Can write with this token (upload crates, etc)</span>
                </label>
              </div>
              <div>
                <label class="relative inline-flex items-center cursor-pointer">
                  <input id="new-token-can-admin" type="checkbox" class="sr-only peer">
                  <div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
                  <span class="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Can perform admin operations</span>
                </label>
              </div>
            </form>
              <div class="justify-between items-center pt-0 space-y-4 sm:flex sm:space-y-0">
                <div class="items-center space-y-4 sm:space-x-4 sm:flex sm:space-y-0">
                  <button id="modal-create-token-close" type="button"  class="py-2 px-4 w-full text-sm font-medium text-gray-500 bg-white rounded-lg border border-gray-200 sm:w-auto hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-primary-300 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">Cancel</button>
                  <button id="modal-create-token-confirm" type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Confirm</button>
                </div>
            </div>
        </div>
    </div>
  </div>
  <div id="modal-token-created" tabindex="-1" class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 w-full md:inset-0 h-modal md:h-full" style="display: none;">
    <div class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-51 w-full md:inset-0 h-modal md:h-full" style="background-color: black; opacity: 0.75;"></div>
    <div class="relative" style="margin: auto; margin-top: 10%; width: 800px;">
        <div class="relative p-4 bg-white rounded-lg shadow dark:bg-gray-800 md:p-8">
            <div class="mb-4 text-sm font-light text-gray-500 dark:text-gray-400">
              <h3 class="mb-3 text-2xl font-bold text-gray-900 dark:text-white">New created token</h3>
            </div>
            <div class="mb-3 space-y-8">
              <p class="text-base text-gray-900 dark:text-white">
                To use this token, it needs to be configured for git in <kbd>~/.git-credentials</kbd>
              </p>
              <textarea id="modal-token-created-git-cred" rows="2" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" style="font-family: monospace;" disabled></textarea>
              <p class="text-base text-gray-900 dark:text-white">
                It also needs to appear in <kbd>~/.cargo/credentials.toml</kbd>:
              </p>
              <textarea id="modal-token-created-cargo-cred" rows="4" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" style="font-family: monospace;" disabled></textarea>
              <p class="text-base text-gray-900 dark:text-white">
                Finally, the registry must be declared in <kbd>~/.cargo/config.toml</kbd>:
              </p>
              <textarea id="modal-token-created-cargo-reg" rows="5" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" style="font-family: monospace;" disabled></textarea>
            </div>
            <div class="justify-between items-center pt-0 space-y-4 sm:flex sm:space-y-0">
              <div class="items-center space-y-4 sm:space-x-4 sm:flex sm:space-y-0">
                <button id="modal-token-created-confirm" type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Ok</button>
              </div>
            </div>
        </div>
    </div>
  </div>
  <div id="modal-revoke-token" tabindex="-1" class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 w-full md:inset-0 h-modal md:h-full" style="display: none;">
    <div class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-51 w-full md:inset-0 h-modal md:h-full" style="background-color: black; opacity: 0.75;"></div>
    <div class="relative" style="margin: auto; margin-top: 10%; width: 800px;">
        <div class="relative p-4 bg-white rounded-lg shadow dark:bg-gray-800 md:p-8">
            <div class="mb-4 text-sm font-light text-gray-500 dark:text-gray-400">
              <h3 class="mb-3 text-2xl font-bold text-gray-900 dark:text-white">Revoke this token?</h3>
            </div>
            <form class="mb-3 space-y-8">
              <div>
                <label for="revoke-token-name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">Token name</label>
                <input type="text" id="revoke-token-name" class="block p-3 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 dark:shadow-sm-light" disabled>
              </div>
            </form>
            <div class="justify-between items-center pt-0 space-y-4 sm:flex sm:space-y-0">
              <div class="items-center space-y-4 sm:space-x-4 sm:flex sm:space-y-0">
                <button id="modal-revoke-token-close" type="button"  class="py-2 px-4 w-full text-sm font-medium text-gray-500 bg-white rounded-lg border border-gray-200 sm:w-auto hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-primary-300 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">Cancel</button>
                <button id="modal-revoke-token-confirm" type="button" class="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">Revoke</button>
              </div>
            </div>
        </div>
    </div>
  </div>
</body>
<footer class="p-4 bg-white md:p-8 lg:p-10 dark:bg-gray-800">
  <div class="mx-auto max-w-screen-xl text-center">
      <span class="text-sm text-gray-500 sm:text-center dark:text-gray-400">Version <span id="version"></span>, Copyright © <span id="year"></span> <a href="https://cenotelie.fr/" target="_blank" class="hover:underline">Cénotélie</a>. All Rights Reserved.</span>
  </div>
</footer>

<link href="/webapp/index.css" rel="stylesheet" />
<script src="/webapp/api.js"></script>
<script src="/webapp/index.js"></script>
<script>
    function doPageLoad() {
      onPageLoad().then((user) => {
        document.getElementById("email").value = user.email;
        document.getElementById("login").value = user.login;
        document.getElementById("name").value = user.name;
        document.getElementById("roles").value = user.roles;
        setupOnChange(document.getElementById("login"), (login) => {
          const user = JSON.parse(window.localStorage.getItem("cratery-user"));
          return apiUpdateUser({...user, login}).then(user => {
            window.localStorage.setItem("cratery-user", JSON.stringify(user));
            return user;
          });
        });

        setupOnChange(document.getElementById("name"), (name) => {
          const user = JSON.parse(window.localStorage.getItem("cratery-user"));
          return apiUpdateUser({...user, name}).then(user => {
            window.localStorage.setItem("cratery-user", JSON.stringify(user));
            return user;
          });
        });
        apiGetUserTokens().then((tokens) => {
          const table = document.getElementById("tokens");
          for (const token of tokens) {
            table.appendChild(renderToken(token));
          }
        });

        apiGetRegistryInformation().then((registryInformation) => {
          window.localStorage.setItem("cratery-registry-name", registryInformation.registryName);
        })
      });
    }

    function renderTokenAccess(token) {
      if (!token.canWrite && !token.canAdmin) {
        return document.createTextNode("read-only");
      }
      let access = "read";
      if (token.canWrite) {
        access = access + ", write"
      }
      if (token.canAdmin) {
        access = access + ", admin"
      }
      return document.createTextNode(access);
    }

    function renderToken(token) {
      const row = document.createElement("tr");
      row.className = "bg-white border-b dark:bg-gray-800 dark:border-gray-700";
      const cell1 = document.createElement("th");
      cell1.setAttribute("scope", "row");
      cell1.className = "px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white";
      cell1.appendChild(document.createTextNode(token.name));
      const cell2 = document.createElement("td");
      cell2.className = "px-6 py-4";
      cell2.appendChild(document.createTextNode(serializeDateTime(token.lastUsed)));
      const cell3 = document.createElement("td");
      cell3.className = "px-6 py-4";
      cell3.appendChild(renderTokenAccess(token));
      const cell4 = document.createElement("td");
      cell4.className = "px-6 py-4";
      const button = document.createElement("button");
      button.className = "focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900";
      button.type = "button";
      button.appendChild(document.createTextNode("Revoke"));
      button.addEventListener("click", () => {
        openRevokeTokenModal(token);
      });

      cell4.appendChild(button);
      row.appendChild(cell1);
      row.appendChild(cell2);
      row.appendChild(cell3);
      row.appendChild(cell4);
      return row;
    }

    function onOpenCreateToken() {
      const modalEl = document.getElementById('modal-create-token');
      modalEl.style.display = "unset";
      const newTokenNameEl = document.getElementById("new-token-name");
      newTokenNameEl.value = "";
      newTokenNameEl.focus();
      const newTokenCanWriteEl = document.getElementById("new-token-can-write");
      newTokenCanWriteEl.checked = false;
      const newTokenCanAdminEl = document.getElementById("new-token-can-admin");
      newTokenCanAdminEl.checked = false;

      const closeEl = document.getElementById('modal-create-token-close');
      closeEl.addEventListener('click', function() {
        modalEl.style.display = "none";
      });

      const confirmEl = document.getElementById('modal-create-token-confirm');
      confirmEl.addEventListener('click', function() {
        const name = newTokenNameEl.value;
        if (name.length === 0) {
          return;
        }
        const canWrite = newTokenCanWriteEl.checked;
        const canAdmin = newTokenCanAdminEl.checked;
        newTokenNameEl.disabled = true;
        closeEl.disabled = true;
        confirmEl.disabled = true;
        apiCreateUserToken(name, canWrite, canAdmin).then((tokenData) => {
          modalEl.style.display = "none";
          openTokenCreatedModal(tokenData.secret);
        }).finally(() => {
          newTokenNameEl.disabled = false;
          newTokenNameEl.value = "";
          closeEl.disabled = false;
          confirmEl.disabled = false;
        });
      });
    }

    function openTokenCreatedModal(secret) {
      const user = JSON.parse(window.localStorage.getItem("cratery-user"));
      const regName = window.localStorage.getItem("cratery-registry-name");
      const gitCredEl = document.getElementById("modal-token-created-git-cred");
      gitCredEl.value = `${window.location.protocol}//${user.login}:${secret}@${window.location.hostname}`;
      const cargoCredEl = document.getElementById("modal-token-created-cargo-cred");
      cargoCredEl.value = `[registries.${regName}]\ntoken = "Basic ${btoa(`${user.login}:${secret}`)}"`;
      const cargoRegEl = document.getElementById("modal-token-created-cargo-reg");
      cargoRegEl.value = `[registries]\n${regName} = { index = "sparse+${window.location.protocol}//${window.location.hostname}/" }\n\n[registry]\nglobal-credential-providers = ["cargo:token"]`;
      const modalEl = document.getElementById('modal-token-created');
      modalEl.style.display = "unset";

      const confirmEl = document.getElementById('modal-token-created-confirm');
      confirmEl.addEventListener('click', function() {
        modalEl.style.display = "none";
        window.location.reload();
      });
    }

    function openRevokeTokenModal(token) {
      const modalEl = document.getElementById('modal-revoke-token');
      modalEl.style.display = "unset";
      const tokenNameEl = document.getElementById("revoke-token-name");
      tokenNameEl.value = token.name;
      const closeEl = document.getElementById('modal-revoke-token-close');
      closeEl.addEventListener('click', function() {
        modalEl.style.display = "none";
      });
      const confirmEl = document.getElementById('modal-revoke-token-confirm');
      confirmEl.addEventListener('click', function() {
        closeEl.disabled = true;
        confirmEl.disabled = true;
        apiRevokeUserToken(token.id).then((_) => {
          window.location.reload();
        }).finally(() => {
          closeEl.disabled = false;
          confirmEl.disabled = false;
        });
      });
    }
</script>
</html>