@ethicdevs/gitfoss | Show object: 44976c9e17b474292a236316215899fe21dd884f ∙ GitFOSS
feat(ssh,keys,command): compute ssh keys fingerprint on add using ss-keygen
+ 204
- 128
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1778839401647,
+  "_generatedAtUnix": 1779047887440,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "assets": {

...
@@ -40,7 +40,7 @@
       "pathSourceMap": "./public/.islands/PullRequestSourceSelect.bundle.js.map"
     },
     "RepositoriesList": {
-      "hash": "648218c692146b6bff49bf45eb44b2d650a6a2b1",
+      "hash": "42a9c5e680868fed97e448c96287f3fa70935911",
       "pathSource": "./app/islands/RepositoriesList.tsx",
       "pathBundle": "./public/.islands/RepositoriesList.bundle.js",
       "pathSourceMap": "./public/.islands/RepositoriesList.bundle.js.map"

app/controllers/user/getUserDetailsView.ts
@@ -1,6 +1,6 @@
 // 3rd-party
 import type { ReqHandler } from "@ethicdevs/react-monolith";
-import { User } from "@prisma/client";
+import { ResourceVisibility, User } from "@prisma/client";
 // app
 import { AppRoute, AppRouteParams } from "../../routes.defs";
 import { makeUsersService } from "../../services/user";

...
@@ -34,7 +34,14 @@ const getUserDetailsView: ReqHandler<
     title: `@${username}`,
     currentUser,
     user,
-    repositories: await usersService.getUserRepositories(user),
+    repositories: (await usersService.getUserRepositories(user)).filter(
+      (repo) => {
+        if (user.id === curr_user_uid) {
+          return true;
+        }
+        return repo.visibility === ResourceVisibility.PUBLIC;
+      },
+    ),
   });
 };
 

app/islands/RepositoriesList.tsx
@@ -8,6 +8,7 @@ import type { Organization, Repository } from "@prisma/client";
 import type { WithThemeSchemeProp } from "../types";
 import { AppRoute } from "../routes.defs";
 import { Card } from "../components/Card.styled";
+import { Chip } from "../components/Chip";
 import { Grid } from "../components/Grid";
 import { buildRouteLink } from "../utils/shared";
 import { breakpoints, NamedColors } from "../utils/style";

...
@@ -19,23 +20,74 @@ export interface RepositoriesListProps {
 const RepositoriesList: ReactIsland<
   RepositoriesListProps & WithThemeSchemeProp
 > = ({ repositories, themeScheme }) => {
-  const selector = ".container";
+  // const selector = ".container";
   return (
     <>
       <style>{`
-        .container {
-          container-type: size;
-          container-name: repositories;
-          min-block-size: 210px;
-          height: max-content;
+        .cards {
+          display: grid;
+          grid-template-columns: 1fr; /* mobile default (1 column) */
+
+          width: 100%;
+          max-width: 1200px; /* prevents cards becoming too wide when no sidebar */
+
+          margin: 0 auto;
+          box-sizing: border-box;
+          gap: 16px;
+        }
+
+        /* Tablet fallback using container width (if no container queries) */
+        @media (min-width: 600px) {
+          .cards {
+            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+          }
+        }
+
+        /* Desktop: force 3 columns */
+        @media (min-width: 1100px) {
+          .cards {
+            grid-template-columns: repeat(3, 1fr);
+          }
+        }
+
+        /* Container query (preferred) — adapts based on .cards container width, not viewport */
+        @container (min-width: 600px) {
+          .cards {
+            display: grid;
+            grid-template-columns: repeat(2, minmax(300px, 1fr)); // auto-fit
+          }
+          .cards > .card {
+            min-width: 300px;
+            width: 100%;
+          }
+        }
+        @container (min-width: 928px) {
+          .cards {
+            display: flex;
+            justify-content: stretch;
+            align-items: flex-start;
+          }
+          .cards > .card {
+            flex: .33;
+            min-width: 33%;
+          }
+        }
+        @container (min-width: 1100px) {
+          .cards {
+            grid-template-columns: repeat(3, 1fr);
+          }
+        }
+
+        /* Required: enable container for .cards */
+        .cards {
+          container-type: inline-size;
         }
 
         .card {
           min-width: 300px;
-          max-width: calc(33% - 8px);
           width: 100%;
           flex: 0 0 33%;
-          max-height: 200px;
+          max-height: 320px;
           min-height: 200px;
           gap: 8px;
         }

...
@@ -61,82 +113,13 @@ const RepositoriesList: ReactIsland<
           }
         }
       `}</style>
-      <script>{`
-        // quick js to set @container height based on number of cards
-        function resize() {
-          const gap = 4;
-          const cardWidth = 300; // match your min-width
-          const cardHeight = 200; // use computed or measured height
-          const container = document.querySelector(\`${selector}\`);
-          const cards = document.querySelectorAll(\`${selector} .card\`);
-          if (!container || cards.length === 0) return;
-          const containerWidth = container.clientWidth;
-          const cols = Math.floor((containerWidth + gap) / (cardWidth + gap));
-          const rows = Math.ceil(cards.length / cols);
-          let height = (cols * cardHeight);
-          let i = 0;
-          if (cols === 1) {
-            height = rows * cardHeight;
-            height += cardHeight;
-            i += rows;
-          } else {
-            height = cols * cardHeight;
-            i += 1;
-            if (cols == 2) {
-              if (cards.length % cols !== 0) {
-                height += cardHeight * 2;
-                i += 1;
-              }
-            } else if (cols == 3) {
-              if (cards.length % rows !== 0) {
-                height += cardHeight;
-                height -= cardHeight / 1.2; // why 1.2?
-              }
-              i += 1;
-            } else if (cols == 4) {
-              if (cards.length % cols !== 0) {
-                height -= cardHeight;
-                i += 1;
-              }
-            }
-          }
-          height -= ((rows * gap) * i);
-          // container.style.height = \`\${height}px\`;
-          container.style.minBlockSize = \`\${height}px\`;
-          cards.forEach(function (card, index) {
-            card.style.height = cardHeight;
-            card.style.maxHeight = cardHeight;
-            card.style.minHeight = cardHeight;
-          });
-          console.log({
-            containerWidth,
-            cols,
-            rows,
-            height,
-          })
-        }
-        window.addEventListener(\`load\`, function(event) {
-          resize();
-        });
-        window.addEventListener(\`resize\`, function(event) {
-          // function debounce(func, wait) {
-          //   let timeout;
-          //   return function() {
-          //     clearTimeout(timeout);
-          //     timeout = setTimeout(func, wait);
-          //   };
-          // }
-          // debounce(resize, 200);
-          resize();
-        });
-      `}</script>
       <Grid.Row
         fluid
         gap={4}
         alignItems={"stretch"}
         justifyContent={"stretch"}
         style={{ marginTop: 16 }}
-        className="container"
+        className="cards"
       >
         {repositories.map((repo) => (
           <Card key={repo.id} themeScheme={themeScheme} className="card">

...
@@ -151,10 +134,10 @@ const RepositoriesList: ReactIsland<
                   {repo.parentOrg.displayName || repo.parentOrg.slug}
                   {" / "}
                   {repo.displayName || repo.slug}
-                  {" ∙ "}
+                  {/*{" ∙ "}
                   <span style={{ textTransform: "capitalize" }}>
                     ({repo.visibility.toLowerCase()})
-                  </span>
+                  </span>*/}
                 </a>
               </h1>
               {repo.isFork && (

...
@@ -164,7 +147,7 @@ const RepositoriesList: ReactIsland<
               )}
             </Grid.Row>
             <Grid.Col fluid gap={8}>
-              <p style={{ margin: 0 }}>{repo.shortDescription}</p>
+              <p style={{ flex: 1, margin: 0 }}>{repo.shortDescription}</p>
               {repo.lastPushedAt != null && (
                 <p
                   style={{

...
@@ -176,6 +159,9 @@ const RepositoriesList: ReactIsland<
                   Last push: {new Date(repo.lastPushedAt).toLocaleString()}
                 </p>
               )}
+              <Chip themeScheme={themeScheme}>
+                {repo.visibility.toLowerCase()}
+              </Chip>
             </Grid.Col>
           </Card>
         ))}

app/services/gitServer/authorizationResolver.ts
@@ -13,9 +13,16 @@ const makeAuthorizationResolver: ServiceMethodFactory<
   return async (repoPath, { username, password }) => {
     const [orgSlug, repoSlugUnsafe] = repoPath.split("/");
 
-    // password contains the publicKey instead of regular password
-    const isPubKeyAuth = repoSlugUnsafe.endsWith(".pub");
+    if (orgSlug == null || orgSlug.trim() === "") {
+      return false;
+    }
 
+    if (repoSlugUnsafe == null || repoSlugUnsafe.trim() === "") {
+      return false;
+    }
+
+    // if password contains the publicKey instead of regular password
+    const isPubKeyAuth = repoSlugUnsafe.endsWith(".pub");
     const repoSlug = repoSlugUnsafe.replace(/\.(git|pub)$/, "");
 
     console.log("AuthorizationResolver called with:", {

...
@@ -65,45 +72,43 @@ const makeAuthorizationResolver: ServiceMethodFactory<
     const repoMembershipsHasReqUser =
       org.memberships.find((m) => m.id === user.id) != null;
 
+    // allow read-only for unlisted users without auth, but write still behind auth.
     if (repo.visibility === ResourceVisibility.PUBLIC) {
       return true;
-    } else {
-      // TODO: allow read-only for unlisted users without auth, but write behind auth.
-      if (
-        repoOrgOwnerIsReqUser === false &&
-        repoMembershipsHasReqUser === false
-      ) {
-        return false;
-      }
-
-      if (isPubKeyAuth) {
-        const matchingPk = await request.prisma.userSSHKey.findFirst({
-          where: {
-            // password contains the publicKey instead of regular password
-            key: password,
-            revoked: false,
-          },
-          include: {
-            user: true,
-          },
-        });
-
-        return (
-          matchingPk != null &&
-          matchingPk.user.id === user.id &&
-          matchingPk.user.username === username
-        );
-      }
-
-      const hashedPassword = cryptoService.computeHash(password);
-      const authed = hashedPassword === user.hashedPassword;
-
-      console.log("git.authorizationResolver:", authed);
-
-      console.log("git.authorizationResolver:", authed, username);
-
-      return authed;
     }
+
+    if (
+      repoOrgOwnerIsReqUser === false &&
+      repoMembershipsHasReqUser === false
+    ) {
+      return false;
+    }
+
+    if (isPubKeyAuth) {
+      const matchingPk = await request.prisma.userSSHKey.findFirst({
+        where: {
+          // password contains the publicKey instead of regular password
+          key: password,
+          revoked: false,
+        },
+        include: {
+          user: true,
+        },
+      });
+
+      return (
+        matchingPk != null &&
+        matchingPk.user.id === user.id &&
+        matchingPk.user.username === username
+      );
+    }
+
+    const hashedPassword = cryptoService.computeHash(password);
+    const authed = hashedPassword === user.hashedPassword;
+
+    console.log("git.authorizationResolver:", authed, username);
+
+    return authed;
   };
 };
 

app/services/repository/getRepositoryHead.ts
@@ -44,6 +44,35 @@ const makeGetRepositoryHead: ServiceMethodFactory<
       throw new Error(`Could not find a valid git repository at: ${repoPath}`);
     }
 
+    // could also set default branch: git symbolic-ref HEAD refs/heads/develop
+    // const gitSymbolicRefProcess = spawn(
+    //   "git",
+    //   ["symbolic-ref", "--short", "HEAD"],
+    //   {
+    //     cwd: repoPath,
+    //     env: {
+    //       LANG: "C",
+    //     },
+    //   },
+    // );
+
+    // // resolve default ref using git symbolic-ref
+    // // so default refs like "HEAD" are resolved to branch names
+    // const gitSymbolicRefResult = await new Promise<string>(
+    //   (resolve, reject) => {
+    //     let buffer = [] as string[];
+    //     gitSymbolicRefProcess.stdout.on("data", (data) => buffer.push(data));
+    //     gitSymbolicRefProcess.stderr.on("data", (data) => {
+    //       reject(new Error(Buffer.from(data).toString("utf-8")));
+    //     });
+    //     gitSymbolicRefProcess.stdout.on("close", () => {
+    //       resolve(buffer.join(""));
+    //     });
+    //   },
+    // );
+    //
+    // ref = gitSymbolicRefResult;
+
     if (ref === Const.DEFAULT_HEAD_REF) {
       ref = Const.PRIMARY_BRANCH_REF;
     }

app/services/user/addUserSSHKey.ts
@@ -1,11 +1,13 @@
 // std
 import fs from "fs";
+import { spawn } from "node:child_process";
 // 1st-party
 import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
 // generated via script[generate:prisma]
 import { User } from "@prisma/client";
 // app
 import type { UsersServiceDeps } from "./types";
+import { spawnConcatChunks } from "../../utils/server";
 
 const SSH_RSA_KEY_REGEXP =
   /^ssh-rsa AAAA[0-9A-Za-z+\/]+[=]{0,3} ([^@]+@[^@]+)$/i;

...
@@ -19,7 +21,7 @@ const addUserSSHKey: ServiceMethodFactory<
     // 0. Validate key is actually a ssh-rsa key
     if (key.match(SSH_RSA_KEY_REGEXP) == null) {
       throw new Error(
-        "Invalid public key. Please provide a valid SSH RSA public key."
+        "Invalid public key. Please provide a valid SSH RSA public key.",
       );
     }
 

...
@@ -32,20 +34,49 @@ const addUserSSHKey: ServiceMethodFactory<
     // 1. Check if public key is already registered
     if (existingKey != null) {
       throw new Error(
-        "Public key is already registered. Please use another one."
+        "Public key is already registered. Please use another one.",
       );
     }
 
-    // 2. Add key to database
+    // 2. Compute key fingerprints (using ssh-keygen)
+    const fingerprintSHA256Process = spawn("ssh-keygen", ["-l", "-f", "-"], {
+      env: { LANG: "C" },
+    });
+
+    // pipe key to ssh-keygen stdin
+    fingerprintSHA256Process.stdin.write(key);
+
+    const fingerprintMD5Process = spawn(
+      "ssh-keygen",
+      ["-lf", "-E", "md5", "-"],
+      { env: { LANG: "C" } },
+    );
+
+    // pipe key to ssh-keygen stdin
+    fingerprintMD5Process.stdin.write(key);
+
+    const fingerprintSHA256 = await spawnConcatChunks(fingerprintSHA256Process);
+    const fingerprintMD5 = await spawnConcatChunks(fingerprintMD5Process);
+
+    console.log("fingerprints:", {
+      sha256: fingerprintSHA256,
+      md5: fingerprintMD5,
+    });
+
+    // 3. Add key to database
     const userKey = await request.prisma.userSSHKey.create({
       data: {
         name: name,
-        key: key,
         user: {
           connect: {
             id: user.id,
           },
         },
+        key: key,
+        key_fingerprint: fingerprintSHA256,
+        // key_fingerprint_md5: fingerprintMD5,
+        revoked: false,
+        lastUsedAt: null,
       },
     });
 

...
@@ -54,6 +85,7 @@ const addUserSSHKey: ServiceMethodFactory<
     }
 
     let line = "";
+    line += `environment="KEY=${key},KEY_FINGERPRINT=${fingerprint}",`;
     line += `command="ssh_command ${user.username}",`;
     line += "no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ";
     line += `${key}\n`;

app/utils/server/index.ts
@@ -9,3 +9,4 @@ export { localAppDomainPreHandler } from "./localAppDomainPreHandler";
 export { makeRequestHandler } from "./makeRequestHandler";
 export { sessionSetupPreHandler } from "./sessionSetupPreHandler";
 export { loadRepositoryCounters } from "./loadRepositoryCounters";
+export { spawnConcatChunks } from "./spawnConcatChunks";

new file
app/utils/server/spawnConcatChunks.ts
@@ -0,0 +1,16 @@
+import { ChildProcessWithoutNullStreams } from "node:child_process";
+
+export function spawnConcatChunks(
+  process: ChildProcessWithoutNullStreams,
+): Promise<string> {
+  return new Promise<string>((resolve, reject) => {
+    let buffer = [] as string[];
+    process.stdout.on("data", (data) => buffer.push(data));
+    process.stderr.on("data", (data) => {
+      reject(new Error(Buffer.from(data).toString("utf-8")));
+    });
+    process.stdout.on("close", () => {
+      resolve(buffer.join(""));
+    });
+  });
+}

packages/gitfoss-ssh-command/src/ssh-command.cr
@@ -55,7 +55,7 @@ end
 
 def sideband_println(msg : String)
   txt = "#{SIDEBAND_PREFIX}#{msg.ends_with?("\n") ? msg : msg + "\n"}"
-  STDERR.puts txt
+  # STDERR.puts txt
   write_to_file txt
 end
 

"},{"type":"normal","normal":true,"oldLine":133,"newLine":116,"position":162,"content":" "},{"type":"normal","normal":true,"oldLine":141,"newLine":124,"position":171,"content":" {repositories.map((repo) => ("},{"type":"normal","normal":true,"oldLine":142,"newLine":125,"position":172,"content":" "}],"oldStart":61,"oldLines":82,"newStart":113,"newLines":13},{"content":"@@ -151,10 +134,10 @@ const RepositoriesList: ReactIsland<","changes":[{"type":"normal","normal":true,"oldLine":151,"newLine":134,"position":173,"content":" {repo.parentOrg.displayName || repo.parentOrg.slug}"},{"type":"normal","normal":true,"oldLine":152,"newLine":135,"position":174,"content":" {\" / \"}"},{"type":"normal","normal":true,"oldLine":153,"newLine":136,"position":175,"content":" {repo.displayName || repo.slug}"},{"type":"del","del":true,"oldLine":154,"position":176,"content":"- {\" ∙ \"}"},{"type":"add","add":true,"newLine":137,"position":177,"content":"+ {/*{\" ∙ \"}"},{"type":"normal","normal":true,"oldLine":155,"newLine":138,"position":178,"content":" "},{"type":"normal","normal":true,"oldLine":156,"newLine":139,"position":179,"content":" ({repo.visibility.toLowerCase()})"},{"type":"del","del":true,"oldLine":157,"position":180,"content":"- "},{"type":"add","add":true,"newLine":140,"position":181,"content":"+ */}"},{"type":"normal","normal":true,"oldLine":158,"newLine":141,"position":182,"content":" "},{"type":"normal","normal":true,"oldLine":159,"newLine":142,"position":183,"content":" "},{"type":"normal","normal":true,"oldLine":160,"newLine":143,"position":184,"content":" {repo.isFork && ("}],"oldStart":151,"oldLines":10,"newStart":134,"newLines":10},{"content":"@@ -164,7 +147,7 @@ const RepositoriesList: ReactIsland<","changes":[{"type":"normal","normal":true,"oldLine":164,"newLine":147,"position":185,"content":" )}"},{"type":"normal","normal":true,"oldLine":165,"newLine":148,"position":186,"content":" "},{"type":"normal","normal":true,"oldLine":166,"newLine":149,"position":187,"content":" "},{"type":"del","del":true,"oldLine":167,"position":188,"content":"-

{repo.shortDescription}

"},{"type":"add","add":true,"newLine":150,"position":189,"content":"+

{repo.shortDescription}

"},{"type":"normal","normal":true,"oldLine":168,"newLine":151,"position":190,"content":" {repo.lastPushedAt != null && ("},{"type":"normal","normal":true,"oldLine":169,"newLine":152,"position":191,"content":" "},{"type":"normal","normal":true,"oldLine":178,"newLine":161,"position":195,"content":" )}"},{"type":"add","add":true,"newLine":162,"position":196,"content":"+ "},{"type":"add","add":true,"newLine":163,"position":197,"content":"+ {repo.visibility.toLowerCase()}"},{"type":"add","add":true,"newLine":164,"position":198,"content":"+ "},{"type":"normal","normal":true,"oldLine":179,"newLine":165,"position":199,"content":"
"},{"type":"normal","normal":true,"oldLine":180,"newLine":166,"position":200,"content":"
"},{"type":"normal","normal":true,"oldLine":181,"newLine":167,"position":201,"content":" ))}"}],"oldStart":176,"oldLines":6,"newStart":159,"newLines":9}],"deletions":81,"additions":67,"index":["25598c7..8bf9f91","100644"]},{"from":"app/services/gitServer/authorizationResolver.ts","to":"app/services/gitServer/authorizationResolver.ts","chunks":[{"content":"@@ -13,9 +13,16 @@ const makeAuthorizationResolver: ServiceMethodFactory<","changes":[{"type":"normal","normal":true,"oldLine":13,"newLine":13,"position":1,"content":" return async (repoPath, { username, password }) => {"},{"type":"normal","normal":true,"oldLine":14,"newLine":14,"position":2,"content":" const [orgSlug, repoSlugUnsafe] = repoPath.split(\"/\");"},{"type":"normal","normal":true,"oldLine":15,"newLine":15,"position":3,"content":" "},{"type":"del","del":true,"oldLine":16,"position":4,"content":"- // password contains the publicKey instead of regular password"},{"type":"del","del":true,"oldLine":17,"position":5,"content":"- const isPubKeyAuth = repoSlugUnsafe.endsWith(\".pub\");"},{"type":"add","add":true,"newLine":16,"position":6,"content":"+ if (orgSlug == null || orgSlug.trim() === \"\") {"},{"type":"add","add":true,"newLine":17,"position":7,"content":"+ return false;"},{"type":"add","add":true,"newLine":18,"position":8,"content":"+ }"},{"type":"normal","normal":true,"oldLine":18,"newLine":19,"position":9,"content":" "},{"type":"add","add":true,"newLine":20,"position":10,"content":"+ if (repoSlugUnsafe == null || repoSlugUnsafe.trim() === \"\") {"},{"type":"add","add":true,"newLine":21,"position":11,"content":"+ return false;"},{"type":"add","add":true,"newLine":22,"position":12,"content":"+ }"},{"type":"add","add":true,"newLine":23,"position":13,"content":"+"},{"type":"add","add":true,"newLine":24,"position":14,"content":"+ // if password contains the publicKey instead of regular password"},{"type":"add","add":true,"newLine":25,"position":15,"content":"+ const isPubKeyAuth = repoSlugUnsafe.endsWith(\".pub\");"},{"type":"normal","normal":true,"oldLine":19,"newLine":26,"position":16,"content":" const repoSlug = repoSlugUnsafe.replace(/\\.(git|pub)$/, \"\");"},{"type":"normal","normal":true,"oldLine":20,"newLine":27,"position":17,"content":" "},{"type":"normal","normal":true,"oldLine":21,"newLine":28,"position":18,"content":" console.log(\"AuthorizationResolver called with:\", {"}],"oldStart":13,"oldLines":9,"newStart":13,"newLines":16},{"content":"@@ -65,45 +72,43 @@ const makeAuthorizationResolver: ServiceMethodFactory<","changes":[{"type":"normal","normal":true,"oldLine":65,"newLine":72,"position":19,"content":" const repoMembershipsHasReqUser ="},{"type":"normal","normal":true,"oldLine":66,"newLine":73,"position":20,"content":" org.memberships.find((m) => m.id === user.id) != null;"},{"type":"normal","normal":true,"oldLine":67,"newLine":74,"position":21,"content":" "},{"type":"add","add":true,"newLine":75,"position":22,"content":"+ // allow read-only for unlisted users without auth, but write still behind auth."},{"type":"normal","normal":true,"oldLine":68,"newLine":76,"position":23,"content":" if (repo.visibility === ResourceVisibility.PUBLIC) {"},{"type":"normal","normal":true,"oldLine":69,"newLine":77,"position":24,"content":" return true;"},{"type":"del","del":true,"oldLine":70,"position":25,"content":"- } else {"},{"type":"del","del":true,"oldLine":71,"position":26,"content":"- // TODO: allow read-only for unlisted users without auth, but write behind auth."},{"type":"del","del":true,"oldLine":72,"position":27,"content":"- if ("},{"type":"del","del":true,"oldLine":73,"position":28,"content":"- repoOrgOwnerIsReqUser === false &&"},{"type":"del","del":true,"oldLine":74,"position":29,"content":"- repoMembershipsHasReqUser === false"},{"type":"del","del":true,"oldLine":75,"position":30,"content":"- ) {"},{"type":"del","del":true,"oldLine":76,"position":31,"content":"- return false;"},{"type":"del","del":true,"oldLine":77,"position":32,"content":"- }"},{"type":"del","del":true,"oldLine":78,"position":33,"content":"-"},{"type":"del","del":true,"oldLine":79,"position":34,"content":"- if (isPubKeyAuth) {"},{"type":"del","del":true,"oldLine":80,"position":35,"content":"- const matchingPk = await request.prisma.userSSHKey.findFirst({"},{"type":"del","del":true,"oldLine":81,"position":36,"content":"- where: {"},{"type":"del","del":true,"oldLine":82,"position":37,"content":"- // password contains the publicKey instead of regular password"},{"type":"del","del":true,"oldLine":83,"position":38,"content":"- key: password,"},{"type":"del","del":true,"oldLine":84,"position":39,"content":"- revoked: false,"},{"type":"del","del":true,"oldLine":85,"position":40,"content":"- },"},{"type":"del","del":true,"oldLine":86,"position":41,"content":"- include: {"},{"type":"del","del":true,"oldLine":87,"position":42,"content":"- user: true,"},{"type":"del","del":true,"oldLine":88,"position":43,"content":"- },"},{"type":"del","del":true,"oldLine":89,"position":44,"content":"- });"},{"type":"del","del":true,"oldLine":90,"position":45,"content":"-"},{"type":"del","del":true,"oldLine":91,"position":46,"content":"- return ("},{"type":"del","del":true,"oldLine":92,"position":47,"content":"- matchingPk != null &&"},{"type":"del","del":true,"oldLine":93,"position":48,"content":"- matchingPk.user.id === user.id &&"},{"type":"del","del":true,"oldLine":94,"position":49,"content":"- matchingPk.user.username === username"},{"type":"del","del":true,"oldLine":95,"position":50,"content":"- );"},{"type":"del","del":true,"oldLine":96,"position":51,"content":"- }"},{"type":"del","del":true,"oldLine":97,"position":52,"content":"-"},{"type":"del","del":true,"oldLine":98,"position":53,"content":"- const hashedPassword = cryptoService.computeHash(password);"},{"type":"del","del":true,"oldLine":99,"position":54,"content":"- const authed = hashedPassword === user.hashedPassword;"},{"type":"del","del":true,"oldLine":100,"position":55,"content":"-"},{"type":"del","del":true,"oldLine":101,"position":56,"content":"- console.log(\"git.authorizationResolver:\", authed);"},{"type":"del","del":true,"oldLine":102,"position":57,"content":"-"},{"type":"del","del":true,"oldLine":103,"position":58,"content":"- console.log(\"git.authorizationResolver:\", authed, username);"},{"type":"del","del":true,"oldLine":104,"position":59,"content":"-"},{"type":"del","del":true,"oldLine":105,"position":60,"content":"- return authed;"},{"type":"normal","normal":true,"oldLine":106,"newLine":78,"position":61,"content":" }"},{"type":"add","add":true,"newLine":79,"position":62,"content":"+"},{"type":"add","add":true,"newLine":80,"position":63,"content":"+ if ("},{"type":"add","add":true,"newLine":81,"position":64,"content":"+ repoOrgOwnerIsReqUser === false &&"},{"type":"add","add":true,"newLine":82,"position":65,"content":"+ repoMembershipsHasReqUser === false"},{"type":"add","add":true,"newLine":83,"position":66,"content":"+ ) {"},{"type":"add","add":true,"newLine":84,"position":67,"content":"+ return false;"},{"type":"add","add":true,"newLine":85,"position":68,"content":"+ }"},{"type":"add","add":true,"newLine":86,"position":69,"content":"+"},{"type":"add","add":true,"newLine":87,"position":70,"content":"+ if (isPubKeyAuth) {"},{"type":"add","add":true,"newLine":88,"position":71,"content":"+ const matchingPk = await request.prisma.userSSHKey.findFirst({"},{"type":"add","add":true,"newLine":89,"position":72,"content":"+ where: {"},{"type":"add","add":true,"newLine":90,"position":73,"content":"+ // password contains the publicKey instead of regular password"},{"type":"add","add":true,"newLine":91,"position":74,"content":"+ key: password,"},{"type":"add","add":true,"newLine":92,"position":75,"content":"+ revoked: false,"},{"type":"add","add":true,"newLine":93,"position":76,"content":"+ },"},{"type":"add","add":true,"newLine":94,"position":77,"content":"+ include: {"},{"type":"add","add":true,"newLine":95,"position":78,"content":"+ user: true,"},{"type":"add","add":true,"newLine":96,"position":79,"content":"+ },"},{"type":"add","add":true,"newLine":97,"position":80,"content":"+ });"},{"type":"add","add":true,"newLine":98,"position":81,"content":"+"},{"type":"add","add":true,"newLine":99,"position":82,"content":"+ return ("},{"type":"add","add":true,"newLine":100,"position":83,"content":"+ matchingPk != null &&"},{"type":"add","add":true,"newLine":101,"position":84,"content":"+ matchingPk.user.id === user.id &&"},{"type":"add","add":true,"newLine":102,"position":85,"content":"+ matchingPk.user.username === username"},{"type":"add","add":true,"newLine":103,"position":86,"content":"+ );"},{"type":"add","add":true,"newLine":104,"position":87,"content":"+ }"},{"type":"add","add":true,"newLine":105,"position":88,"content":"+"},{"type":"add","add":true,"newLine":106,"position":89,"content":"+ const hashedPassword = cryptoService.computeHash(password);"},{"type":"add","add":true,"newLine":107,"position":90,"content":"+ const authed = hashedPassword === user.hashedPassword;"},{"type":"add","add":true,"newLine":108,"position":91,"content":"+"},{"type":"add","add":true,"newLine":109,"position":92,"content":"+ console.log(\"git.authorizationResolver:\", authed, username);"},{"type":"add","add":true,"newLine":110,"position":93,"content":"+"},{"type":"add","add":true,"newLine":111,"position":94,"content":"+ return authed;"},{"type":"normal","normal":true,"oldLine":107,"newLine":112,"position":95,"content":" };"},{"type":"normal","normal":true,"oldLine":108,"newLine":113,"position":96,"content":" };"},{"type":"normal","normal":true,"oldLine":109,"newLine":114,"position":97,"content":" "}],"oldStart":65,"oldLines":45,"newStart":72,"newLines":43}],"deletions":38,"additions":43,"index":["2ee98a9..f8ea5d0","100644"]},{"from":"app/services/repository/getRepositoryHead.ts","to":"app/services/repository/getRepositoryHead.ts","chunks":[{"content":"@@ -44,6 +44,35 @@ const makeGetRepositoryHead: ServiceMethodFactory<","changes":[{"type":"normal","normal":true,"oldLine":44,"newLine":44,"position":1,"content":" throw new Error(`Could not find a valid git repository at: ${repoPath}`);"},{"type":"normal","normal":true,"oldLine":45,"newLine":45,"position":2,"content":" }"},{"type":"normal","normal":true,"oldLine":46,"newLine":46,"position":3,"content":" "},{"type":"add","add":true,"newLine":47,"position":4,"content":"+ // could also set default branch: git symbolic-ref HEAD refs/heads/develop"},{"type":"add","add":true,"newLine":48,"position":5,"content":"+ // const gitSymbolicRefProcess = spawn("},{"type":"add","add":true,"newLine":49,"position":6,"content":"+ // \"git\","},{"type":"add","add":true,"newLine":50,"position":7,"content":"+ // [\"symbolic-ref\", \"--short\", \"HEAD\"],"},{"type":"add","add":true,"newLine":51,"position":8,"content":"+ // {"},{"type":"add","add":true,"newLine":52,"position":9,"content":"+ // cwd: repoPath,"},{"type":"add","add":true,"newLine":53,"position":10,"content":"+ // env: {"},{"type":"add","add":true,"newLine":54,"position":11,"content":"+ // LANG: \"C\","},{"type":"add","add":true,"newLine":55,"position":12,"content":"+ // },"},{"type":"add","add":true,"newLine":56,"position":13,"content":"+ // },"},{"type":"add","add":true,"newLine":57,"position":14,"content":"+ // );"},{"type":"add","add":true,"newLine":58,"position":15,"content":"+"},{"type":"add","add":true,"newLine":59,"position":16,"content":"+ // // resolve default ref using git symbolic-ref"},{"type":"add","add":true,"newLine":60,"position":17,"content":"+ // // so default refs like \"HEAD\" are resolved to branch names"},{"type":"add","add":true,"newLine":61,"position":18,"content":"+ // const gitSymbolicRefResult = await new Promise("},{"type":"add","add":true,"newLine":62,"position":19,"content":"+ // (resolve, reject) => {"},{"type":"add","add":true,"newLine":63,"position":20,"content":"+ // let buffer = [] as string[];"},{"type":"add","add":true,"newLine":64,"position":21,"content":"+ // gitSymbolicRefProcess.stdout.on(\"data\", (data) => buffer.push(data));"},{"type":"add","add":true,"newLine":65,"position":22,"content":"+ // gitSymbolicRefProcess.stderr.on(\"data\", (data) => {"},{"type":"add","add":true,"newLine":66,"position":23,"content":"+ // reject(new Error(Buffer.from(data).toString(\"utf-8\")));"},{"type":"add","add":true,"newLine":67,"position":24,"content":"+ // });"},{"type":"add","add":true,"newLine":68,"position":25,"content":"+ // gitSymbolicRefProcess.stdout.on(\"close\", () => {"},{"type":"add","add":true,"newLine":69,"position":26,"content":"+ // resolve(buffer.join(\"\"));"},{"type":"add","add":true,"newLine":70,"position":27,"content":"+ // });"},{"type":"add","add":true,"newLine":71,"position":28,"content":"+ // },"},{"type":"add","add":true,"newLine":72,"position":29,"content":"+ // );"},{"type":"add","add":true,"newLine":73,"position":30,"content":"+ //"},{"type":"add","add":true,"newLine":74,"position":31,"content":"+ // ref = gitSymbolicRefResult;"},{"type":"add","add":true,"newLine":75,"position":32,"content":"+"},{"type":"normal","normal":true,"oldLine":47,"newLine":76,"position":33,"content":" if (ref === Const.DEFAULT_HEAD_REF) {"},{"type":"normal","normal":true,"oldLine":48,"newLine":77,"position":34,"content":" ref = Const.PRIMARY_BRANCH_REF;"},{"type":"normal","normal":true,"oldLine":49,"newLine":78,"position":35,"content":" }"}],"oldStart":44,"oldLines":6,"newStart":44,"newLines":35}],"deletions":0,"additions":29,"index":["6706f46..d52e322","100644"]},{"from":"app/services/user/addUserSSHKey.ts","to":"app/services/user/addUserSSHKey.ts","chunks":[{"content":"@@ -1,11 +1,13 @@","changes":[{"type":"normal","normal":true,"oldLine":1,"newLine":1,"position":1,"content":" // std"},{"type":"normal","normal":true,"oldLine":2,"newLine":2,"position":2,"content":" import fs from \"fs\";"},{"type":"add","add":true,"newLine":3,"position":3,"content":"+import { spawn } from \"node:child_process\";"},{"type":"normal","normal":true,"oldLine":3,"newLine":4,"position":4,"content":" // 1st-party"},{"type":"normal","normal":true,"oldLine":4,"newLine":5,"position":5,"content":" import type { ServiceMethodFactory } from \"@ethicdevs/react-monolith\";"},{"type":"normal","normal":true,"oldLine":5,"newLine":6,"position":6,"content":" // generated via script[generate:prisma]"},{"type":"normal","normal":true,"oldLine":6,"newLine":7,"position":7,"content":" import { User } from \"@prisma/client\";"},{"type":"normal","normal":true,"oldLine":7,"newLine":8,"position":8,"content":" // app"},{"type":"normal","normal":true,"oldLine":8,"newLine":9,"position":9,"content":" import type { UsersServiceDeps } from \"./types\";"},{"type":"add","add":true,"newLine":10,"position":10,"content":"+import { spawnConcatChunks } from \"../../utils/server\";"},{"type":"normal","normal":true,"oldLine":9,"newLine":11,"position":11,"content":" "},{"type":"normal","normal":true,"oldLine":10,"newLine":12,"position":12,"content":" const SSH_RSA_KEY_REGEXP ="},{"type":"normal","normal":true,"oldLine":11,"newLine":13,"position":13,"content":" /^ssh-rsa AAAA[0-9A-Za-z+\\/]+[=]{0,3} ([^@]+@[^@]+)$/i;"}],"oldStart":1,"oldLines":11,"newStart":1,"newLines":13},{"content":"@@ -19,7 +21,7 @@ const addUserSSHKey: ServiceMethodFactory<","changes":[{"type":"normal","normal":true,"oldLine":19,"newLine":21,"position":14,"content":" // 0. Validate key is actually a ssh-rsa key"},{"type":"normal","normal":true,"oldLine":20,"newLine":22,"position":15,"content":" if (key.match(SSH_RSA_KEY_REGEXP) == null) {"},{"type":"normal","normal":true,"oldLine":21,"newLine":23,"position":16,"content":" throw new Error("},{"type":"del","del":true,"oldLine":22,"position":17,"content":"- \"Invalid public key. Please provide a valid SSH RSA public key.\""},{"type":"add","add":true,"newLine":24,"position":18,"content":"+ \"Invalid public key. Please provide a valid SSH RSA public key.\","},{"type":"normal","normal":true,"oldLine":23,"newLine":25,"position":19,"content":" );"},{"type":"normal","normal":true,"oldLine":24,"newLine":26,"position":20,"content":" }"},{"type":"normal","normal":true,"oldLine":25,"newLine":27,"position":21,"content":" "}],"oldStart":19,"oldLines":7,"newStart":21,"newLines":7},{"content":"@@ -32,20 +34,49 @@ const addUserSSHKey: ServiceMethodFactory<","changes":[{"type":"normal","normal":true,"oldLine":32,"newLine":34,"position":22,"content":" // 1. Check if public key is already registered"},{"type":"normal","normal":true,"oldLine":33,"newLine":35,"position":23,"content":" if (existingKey != null) {"},{"type":"normal","normal":true,"oldLine":34,"newLine":36,"position":24,"content":" throw new Error("},{"type":"del","del":true,"oldLine":35,"position":25,"content":"- \"Public key is already registered. Please use another one.\""},{"type":"add","add":true,"newLine":37,"position":26,"content":"+ \"Public key is already registered. Please use another one.\","},{"type":"normal","normal":true,"oldLine":36,"newLine":38,"position":27,"content":" );"},{"type":"normal","normal":true,"oldLine":37,"newLine":39,"position":28,"content":" }"},{"type":"normal","normal":true,"oldLine":38,"newLine":40,"position":29,"content":" "},{"type":"del","del":true,"oldLine":39,"position":30,"content":"- // 2. Add key to database"},{"type":"add","add":true,"newLine":41,"position":31,"content":"+ // 2. Compute key fingerprints (using ssh-keygen)"},{"type":"add","add":true,"newLine":42,"position":32,"content":"+ const fingerprintSHA256Process = spawn(\"ssh-keygen\", [\"-l\", \"-f\", \"-\"], {"},{"type":"add","add":true,"newLine":43,"position":33,"content":"+ env: { LANG: \"C\" },"},{"type":"add","add":true,"newLine":44,"position":34,"content":"+ });"},{"type":"add","add":true,"newLine":45,"position":35,"content":"+"},{"type":"add","add":true,"newLine":46,"position":36,"content":"+ // pipe key to ssh-keygen stdin"},{"type":"add","add":true,"newLine":47,"position":37,"content":"+ fingerprintSHA256Process.stdin.write(key);"},{"type":"add","add":true,"newLine":48,"position":38,"content":"+"},{"type":"add","add":true,"newLine":49,"position":39,"content":"+ const fingerprintMD5Process = spawn("},{"type":"add","add":true,"newLine":50,"position":40,"content":"+ \"ssh-keygen\","},{"type":"add","add":true,"newLine":51,"position":41,"content":"+ [\"-lf\", \"-E\", \"md5\", \"-\"],"},{"type":"add","add":true,"newLine":52,"position":42,"content":"+ { env: { LANG: \"C\" } },"},{"type":"add","add":true,"newLine":53,"position":43,"content":"+ );"},{"type":"add","add":true,"newLine":54,"position":44,"content":"+"},{"type":"add","add":true,"newLine":55,"position":45,"content":"+ // pipe key to ssh-keygen stdin"},{"type":"add","add":true,"newLine":56,"position":46,"content":"+ fingerprintMD5Process.stdin.write(key);"},{"type":"add","add":true,"newLine":57,"position":47,"content":"+"},{"type":"add","add":true,"newLine":58,"position":48,"content":"+ const fingerprintSHA256 = await spawnConcatChunks(fingerprintSHA256Process);"},{"type":"add","add":true,"newLine":59,"position":49,"content":"+ const fingerprintMD5 = await spawnConcatChunks(fingerprintMD5Process);"},{"type":"add","add":true,"newLine":60,"position":50,"content":"+"},{"type":"add","add":true,"newLine":61,"position":51,"content":"+ console.log(\"fingerprints:\", {"},{"type":"add","add":true,"newLine":62,"position":52,"content":"+ sha256: fingerprintSHA256,"},{"type":"add","add":true,"newLine":63,"position":53,"content":"+ md5: fingerprintMD5,"},{"type":"add","add":true,"newLine":64,"position":54,"content":"+ });"},{"type":"add","add":true,"newLine":65,"position":55,"content":"+"},{"type":"add","add":true,"newLine":66,"position":56,"content":"+ // 3. Add key to database"},{"type":"normal","normal":true,"oldLine":40,"newLine":67,"position":57,"content":" const userKey = await request.prisma.userSSHKey.create({"},{"type":"normal","normal":true,"oldLine":41,"newLine":68,"position":58,"content":" data: {"},{"type":"normal","normal":true,"oldLine":42,"newLine":69,"position":59,"content":" name: name,"},{"type":"del","del":true,"oldLine":43,"position":60,"content":"- key: key,"},{"type":"normal","normal":true,"oldLine":44,"newLine":70,"position":61,"content":" user: {"},{"type":"normal","normal":true,"oldLine":45,"newLine":71,"position":62,"content":" connect: {"},{"type":"normal","normal":true,"oldLine":46,"newLine":72,"position":63,"content":" id: user.id,"},{"type":"normal","normal":true,"oldLine":47,"newLine":73,"position":64,"content":" },"},{"type":"normal","normal":true,"oldLine":48,"newLine":74,"position":65,"content":" },"},{"type":"add","add":true,"newLine":75,"position":66,"content":"+ key: key,"},{"type":"add","add":true,"newLine":76,"position":67,"content":"+ key_fingerprint: fingerprintSHA256,"},{"type":"add","add":true,"newLine":77,"position":68,"content":"+ // key_fingerprint_md5: fingerprintMD5,"},{"type":"add","add":true,"newLine":78,"position":69,"content":"+ revoked: false,"},{"type":"add","add":true,"newLine":79,"position":70,"content":"+ lastUsedAt: null,"},{"type":"normal","normal":true,"oldLine":49,"newLine":80,"position":71,"content":" },"},{"type":"normal","normal":true,"oldLine":50,"newLine":81,"position":72,"content":" });"},{"type":"normal","normal":true,"oldLine":51,"newLine":82,"position":73,"content":" "}],"oldStart":32,"oldLines":20,"newStart":34,"newLines":49},{"content":"@@ -54,6 +85,7 @@ const addUserSSHKey: ServiceMethodFactory<","changes":[{"type":"normal","normal":true,"oldLine":54,"newLine":85,"position":74,"content":" }"},{"type":"normal","normal":true,"oldLine":55,"newLine":86,"position":75,"content":" "},{"type":"normal","normal":true,"oldLine":56,"newLine":87,"position":76,"content":" let line = \"\";"},{"type":"add","add":true,"newLine":88,"position":77,"content":"+ line += `environment=\"KEY=${key},KEY_FINGERPRINT=${fingerprint}\",`;"},{"type":"normal","normal":true,"oldLine":57,"newLine":89,"position":78,"content":" line += `command=\"ssh_command ${user.username}\",`;"},{"type":"normal","normal":true,"oldLine":58,"newLine":90,"position":79,"content":" line += \"no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty \";"},{"type":"normal","normal":true,"oldLine":59,"newLine":91,"position":80,"content":" line += `${key}\\n`;"}],"oldStart":54,"oldLines":6,"newStart":85,"newLines":7}],"deletions":4,"additions":36,"index":["6943e13..2da993c","100644"]},{"from":"app/utils/server/index.ts","to":"app/utils/server/index.ts","chunks":[{"content":"@@ -9,3 +9,4 @@ export { localAppDomainPreHandler } from \"./localAppDomainPreHandler\";","changes":[{"type":"normal","normal":true,"oldLine":9,"newLine":9,"position":1,"content":" export { makeRequestHandler } from \"./makeRequestHandler\";"},{"type":"normal","normal":true,"oldLine":10,"newLine":10,"position":2,"content":" export { sessionSetupPreHandler } from \"./sessionSetupPreHandler\";"},{"type":"normal","normal":true,"oldLine":11,"newLine":11,"position":3,"content":" export { loadRepositoryCounters } from \"./loadRepositoryCounters\";"},{"type":"add","add":true,"newLine":12,"position":4,"content":"+export { spawnConcatChunks } from \"./spawnConcatChunks\";"}],"oldStart":9,"oldLines":3,"newStart":9,"newLines":4}],"deletions":0,"additions":1,"index":["35cd808..7474f5b","100644"]},{"from":"/dev/null","to":"app/utils/server/spawnConcatChunks.ts","chunks":[{"content":"@@ -0,0 +1,16 @@","changes":[{"type":"add","add":true,"newLine":1,"position":1,"content":"+import { ChildProcessWithoutNullStreams } from \"node:child_process\";"},{"type":"add","add":true,"newLine":2,"position":2,"content":"+"},{"type":"add","add":true,"newLine":3,"position":3,"content":"+export function spawnConcatChunks("},{"type":"add","add":true,"newLine":4,"position":4,"content":"+ process: ChildProcessWithoutNullStreams,"},{"type":"add","add":true,"newLine":5,"position":5,"content":"+): Promise {"},{"type":"add","add":true,"newLine":6,"position":6,"content":"+ return new Promise((resolve, reject) => {"},{"type":"add","add":true,"newLine":7,"position":7,"content":"+ let buffer = [] as string[];"},{"type":"add","add":true,"newLine":8,"position":8,"content":"+ process.stdout.on(\"data\", (data) => buffer.push(data));"},{"type":"add","add":true,"newLine":9,"position":9,"content":"+ process.stderr.on(\"data\", (data) => {"},{"type":"add","add":true,"newLine":10,"position":10,"content":"+ reject(new Error(Buffer.from(data).toString(\"utf-8\")));"},{"type":"add","add":true,"newLine":11,"position":11,"content":"+ });"},{"type":"add","add":true,"newLine":12,"position":12,"content":"+ process.stdout.on(\"close\", () => {"},{"type":"add","add":true,"newLine":13,"position":13,"content":"+ resolve(buffer.join(\"\"));"},{"type":"add","add":true,"newLine":14,"position":14,"content":"+ });"},{"type":"add","add":true,"newLine":15,"position":15,"content":"+ });"},{"type":"add","add":true,"newLine":16,"position":16,"content":"+}"}],"oldStart":0,"oldLines":0,"newStart":1,"newLines":16}],"deletions":0,"additions":16,"new":true,"index":["0000000..897ca01"]},{"from":"packages/gitfoss-ssh-command/src/ssh-command.cr","to":"packages/gitfoss-ssh-command/src/ssh-command.cr","chunks":[{"content":"@@ -55,7 +55,7 @@ end","changes":[{"type":"normal","normal":true,"oldLine":55,"newLine":55,"position":1,"content":" "},{"type":"normal","normal":true,"oldLine":56,"newLine":56,"position":2,"content":" def sideband_println(msg : String)"},{"type":"normal","normal":true,"oldLine":57,"newLine":57,"position":3,"content":" txt = \"#{SIDEBAND_PREFIX}#{msg.ends_with?(\"\\n\") ? msg : msg + \"\\n\"}\""},{"type":"del","del":true,"oldLine":58,"position":4,"content":"- STDERR.puts txt"},{"type":"add","add":true,"newLine":58,"position":5,"content":"+ # STDERR.puts txt"},{"type":"normal","normal":true,"oldLine":59,"newLine":59,"position":6,"content":" write_to_file txt"},{"type":"normal","normal":true,"oldLine":60,"newLine":60,"position":7,"content":" end"},{"type":"normal","normal":true,"oldLine":61,"newLine":61,"position":8,"content":" "}],"oldStart":55,"oldLines":7,"newStart":55,"newLines":7}],"deletions":1,"additions":1,"index":["0701dea..da6911d","100644"]}],"themeScheme":"light","orgSlug":"ethicdevs","repoSlug":"gitfoss","commitHash":"44976c9e17b474292a236316215899fe21dd884f"}, "Code$$0": {"code":"@@ -1,5 +1,5 @@\n {\n- \"_generatedAtUnix\": 1778839401647,\n+ \"_generatedAtUnix\": 1779047887440,\n \"_hashAlgorithm\": \"sha1\",\n \"_version\": 2,\n \"assets\": {\n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":8,"borderTopRightRadius":8,"borderBottomLeftRadius":0,"borderBottomRightRadius":0}}, "Code$$1": {"code":"@@ -40,7 +40,7 @@\n \"pathSourceMap\": \"./public/.islands/PullRequestSourceSelect.bundle.js.map\"\n },\n \"RepositoriesList\": {\n- \"hash\": \"648218c692146b6bff49bf45eb44b2d650a6a2b1\",\n+ \"hash\": \"42a9c5e680868fed97e448c96287f3fa70935911\",\n \"pathSource\": \"./app/islands/RepositoriesList.tsx\",\n \"pathBundle\": \"./public/.islands/RepositoriesList.bundle.js\",\n \"pathSourceMap\": \"./public/.islands/RepositoriesList.bundle.js.map\"\n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":0,"borderTopRightRadius":0,"borderBottomLeftRadius":8,"borderBottomRightRadius":8}}, "Code$$2": {"code":"@@ -1,6 +1,6 @@\n // 3rd-party\n import type { ReqHandler } from \"@ethicdevs/react-monolith\";\n-import { User } from \"@prisma/client\";\n+import { ResourceVisibility, User } from \"@prisma/client\";\n // app\n import { AppRoute, AppRouteParams } from \"../../routes.defs\";\n import { makeUsersService } from \"../../services/user\";\n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":8,"borderTopRightRadius":8,"borderBottomLeftRadius":0,"borderBottomRightRadius":0}}, "Code$$3": {"code":"@@ -34,7 +34,14 @@ const getUserDetailsView: ReqHandler<\n title: `@${username}`,\n currentUser,\n user,\n- repositories: await usersService.getUserRepositories(user),\n+ repositories: (await usersService.getUserRepositories(user)).filter(\n+ (repo) => {\n+ if (user.id === curr_user_uid) {\n+ return true;\n+ }\n+ return repo.visibility === ResourceVisibility.PUBLIC;\n+ },\n+ ),\n });\n };\n \n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":0,"borderTopRightRadius":0,"borderBottomLeftRadius":8,"borderBottomRightRadius":8}}, "Code$$4": {"code":"@@ -8,6 +8,7 @@ import type { Organization, Repository } from \"@prisma/client\";\n import type { WithThemeSchemeProp } from \"../types\";\n import { AppRoute } from \"../routes.defs\";\n import { Card } from \"../components/Card.styled\";\n+import { Chip } from \"../components/Chip\";\n import { Grid } from \"../components/Grid\";\n import { buildRouteLink } from \"../utils/shared\";\n import { breakpoints, NamedColors } from \"../utils/style\";\n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":8,"borderTopRightRadius":8,"borderBottomLeftRadius":0,"borderBottomRightRadius":0}}, "Code$$5": {"code":"@@ -19,23 +20,74 @@ export interface RepositoriesListProps {\n const RepositoriesList: ReactIsland<\n RepositoriesListProps & WithThemeSchemeProp\n > = ({ repositories, themeScheme }) => {\n- const selector = \".container\";\n+ // const selector = \".container\";\n return (\n <>\n \n- \n \n {repositories.map((repo) => (\n \n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":0,"borderTopRightRadius":0,"borderBottomLeftRadius":0,"borderBottomRightRadius":0}}, "Code$$7": {"code":"@@ -151,10 +134,10 @@ const RepositoriesList: ReactIsland<\n {repo.parentOrg.displayName || repo.parentOrg.slug}\n {\" / \"}\n {repo.displayName || repo.slug}\n- {\" ∙ \"}\n+ {/*{\" ∙ \"}\n \n ({repo.visibility.toLowerCase()})\n- \n+ */}\n \n \n {repo.isFork && (\n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":0,"borderTopRightRadius":0,"borderBottomLeftRadius":0,"borderBottomRightRadius":0}}, "Code$$8": {"code":"@@ -164,7 +147,7 @@ const RepositoriesList: ReactIsland<\n )}\n \n \n-

{repo.shortDescription}

\n+

{repo.shortDescription}

\n {repo.lastPushedAt != null && (\n \n )}\n+ \n+ {repo.visibility.toLowerCase()}\n+ \n
\n
\n ))}\n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":0,"borderTopRightRadius":0,"borderBottomLeftRadius":8,"borderBottomRightRadius":8}}, "Code$$10": {"code":"@@ -13,9 +13,16 @@ const makeAuthorizationResolver: ServiceMethodFactory<\n return async (repoPath, { username, password }) => {\n const [orgSlug, repoSlugUnsafe] = repoPath.split(\"/\");\n \n- // password contains the publicKey instead of regular password\n- const isPubKeyAuth = repoSlugUnsafe.endsWith(\".pub\");\n+ if (orgSlug == null || orgSlug.trim() === \"\") {\n+ return false;\n+ }\n \n+ if (repoSlugUnsafe == null || repoSlugUnsafe.trim() === \"\") {\n+ return false;\n+ }\n+\n+ // if password contains the publicKey instead of regular password\n+ const isPubKeyAuth = repoSlugUnsafe.endsWith(\".pub\");\n const repoSlug = repoSlugUnsafe.replace(/\\.(git|pub)$/, \"\");\n \n console.log(\"AuthorizationResolver called with:\", {\n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":8,"borderTopRightRadius":8,"borderBottomLeftRadius":0,"borderBottomRightRadius":0}}, "Code$$11": {"code":"@@ -65,45 +72,43 @@ const makeAuthorizationResolver: ServiceMethodFactory<\n const repoMembershipsHasReqUser =\n org.memberships.find((m) => m.id === user.id) != null;\n \n+ // allow read-only for unlisted users without auth, but write still behind auth.\n if (repo.visibility === ResourceVisibility.PUBLIC) {\n return true;\n- } else {\n- // TODO: allow read-only for unlisted users without auth, but write behind auth.\n- if (\n- repoOrgOwnerIsReqUser === false &&\n- repoMembershipsHasReqUser === false\n- ) {\n- return false;\n- }\n-\n- if (isPubKeyAuth) {\n- const matchingPk = await request.prisma.userSSHKey.findFirst({\n- where: {\n- // password contains the publicKey instead of regular password\n- key: password,\n- revoked: false,\n- },\n- include: {\n- user: true,\n- },\n- });\n-\n- return (\n- matchingPk != null &&\n- matchingPk.user.id === user.id &&\n- matchingPk.user.username === username\n- );\n- }\n-\n- const hashedPassword = cryptoService.computeHash(password);\n- const authed = hashedPassword === user.hashedPassword;\n-\n- console.log(\"git.authorizationResolver:\", authed);\n-\n- console.log(\"git.authorizationResolver:\", authed, username);\n-\n- return authed;\n }\n+\n+ if (\n+ repoOrgOwnerIsReqUser === false &&\n+ repoMembershipsHasReqUser === false\n+ ) {\n+ return false;\n+ }\n+\n+ if (isPubKeyAuth) {\n+ const matchingPk = await request.prisma.userSSHKey.findFirst({\n+ where: {\n+ // password contains the publicKey instead of regular password\n+ key: password,\n+ revoked: false,\n+ },\n+ include: {\n+ user: true,\n+ },\n+ });\n+\n+ return (\n+ matchingPk != null &&\n+ matchingPk.user.id === user.id &&\n+ matchingPk.user.username === username\n+ );\n+ }\n+\n+ const hashedPassword = cryptoService.computeHash(password);\n+ const authed = hashedPassword === user.hashedPassword;\n+\n+ console.log(\"git.authorizationResolver:\", authed, username);\n+\n+ return authed;\n };\n };\n \n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":0,"borderTopRightRadius":0,"borderBottomLeftRadius":8,"borderBottomRightRadius":8}}, "Code$$12": {"code":"@@ -44,6 +44,35 @@ const makeGetRepositoryHead: ServiceMethodFactory<\n throw new Error(`Could not find a valid git repository at: ${repoPath}`);\n }\n \n+ // could also set default branch: git symbolic-ref HEAD refs/heads/develop\n+ // const gitSymbolicRefProcess = spawn(\n+ // \"git\",\n+ // [\"symbolic-ref\", \"--short\", \"HEAD\"],\n+ // {\n+ // cwd: repoPath,\n+ // env: {\n+ // LANG: \"C\",\n+ // },\n+ // },\n+ // );\n+\n+ // // resolve default ref using git symbolic-ref\n+ // // so default refs like \"HEAD\" are resolved to branch names\n+ // const gitSymbolicRefResult = await new Promise(\n+ // (resolve, reject) => {\n+ // let buffer = [] as string[];\n+ // gitSymbolicRefProcess.stdout.on(\"data\", (data) => buffer.push(data));\n+ // gitSymbolicRefProcess.stderr.on(\"data\", (data) => {\n+ // reject(new Error(Buffer.from(data).toString(\"utf-8\")));\n+ // });\n+ // gitSymbolicRefProcess.stdout.on(\"close\", () => {\n+ // resolve(buffer.join(\"\"));\n+ // });\n+ // },\n+ // );\n+ //\n+ // ref = gitSymbolicRefResult;\n+\n if (ref === Const.DEFAULT_HEAD_REF) {\n ref = Const.PRIMARY_BRANCH_REF;\n }\n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":8,"borderTopRightRadius":8,"borderBottomLeftRadius":8,"borderBottomRightRadius":8}}, "Code$$13": {"code":"@@ -1,11 +1,13 @@\n // std\n import fs from \"fs\";\n+import { spawn } from \"node:child_process\";\n // 1st-party\n import type { ServiceMethodFactory } from \"@ethicdevs/react-monolith\";\n // generated via script[generate:prisma]\n import { User } from \"@prisma/client\";\n // app\n import type { UsersServiceDeps } from \"./types\";\n+import { spawnConcatChunks } from \"../../utils/server\";\n \n const SSH_RSA_KEY_REGEXP =\n /^ssh-rsa AAAA[0-9A-Za-z+\\/]+[=]{0,3} ([^@]+@[^@]+)$/i;\n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":8,"borderTopRightRadius":8,"borderBottomLeftRadius":0,"borderBottomRightRadius":0}}, "Code$$14": {"code":"@@ -19,7 +21,7 @@ const addUserSSHKey: ServiceMethodFactory<\n // 0. Validate key is actually a ssh-rsa key\n if (key.match(SSH_RSA_KEY_REGEXP) == null) {\n throw new Error(\n- \"Invalid public key. Please provide a valid SSH RSA public key.\"\n+ \"Invalid public key. Please provide a valid SSH RSA public key.\",\n );\n }\n \n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":0,"borderTopRightRadius":0,"borderBottomLeftRadius":0,"borderBottomRightRadius":0}}, "Code$$15": {"code":"@@ -32,20 +34,49 @@ const addUserSSHKey: ServiceMethodFactory<\n // 1. Check if public key is already registered\n if (existingKey != null) {\n throw new Error(\n- \"Public key is already registered. Please use another one.\"\n+ \"Public key is already registered. Please use another one.\",\n );\n }\n \n- // 2. Add key to database\n+ // 2. Compute key fingerprints (using ssh-keygen)\n+ const fingerprintSHA256Process = spawn(\"ssh-keygen\", [\"-l\", \"-f\", \"-\"], {\n+ env: { LANG: \"C\" },\n+ });\n+\n+ // pipe key to ssh-keygen stdin\n+ fingerprintSHA256Process.stdin.write(key);\n+\n+ const fingerprintMD5Process = spawn(\n+ \"ssh-keygen\",\n+ [\"-lf\", \"-E\", \"md5\", \"-\"],\n+ { env: { LANG: \"C\" } },\n+ );\n+\n+ // pipe key to ssh-keygen stdin\n+ fingerprintMD5Process.stdin.write(key);\n+\n+ const fingerprintSHA256 = await spawnConcatChunks(fingerprintSHA256Process);\n+ const fingerprintMD5 = await spawnConcatChunks(fingerprintMD5Process);\n+\n+ console.log(\"fingerprints:\", {\n+ sha256: fingerprintSHA256,\n+ md5: fingerprintMD5,\n+ });\n+\n+ // 3. Add key to database\n const userKey = await request.prisma.userSSHKey.create({\n data: {\n name: name,\n- key: key,\n user: {\n connect: {\n id: user.id,\n },\n },\n+ key: key,\n+ key_fingerprint: fingerprintSHA256,\n+ // key_fingerprint_md5: fingerprintMD5,\n+ revoked: false,\n+ lastUsedAt: null,\n },\n });\n \n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":0,"borderTopRightRadius":0,"borderBottomLeftRadius":0,"borderBottomRightRadius":0}}, "Code$$16": {"code":"@@ -54,6 +85,7 @@ const addUserSSHKey: ServiceMethodFactory<\n }\n \n let line = \"\";\n+ line += `environment=\"KEY=${key},KEY_FINGERPRINT=${fingerprint}\",`;\n line += `command=\"ssh_command ${user.username}\",`;\n line += \"no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty \";\n line += `${key}\\n`;\n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":0,"borderTopRightRadius":0,"borderBottomLeftRadius":8,"borderBottomRightRadius":8}}, "Code$$17": {"code":"@@ -9,3 +9,4 @@ export { localAppDomainPreHandler } from \"./localAppDomainPreHandler\";\n export { makeRequestHandler } from \"./makeRequestHandler\";\n export { sessionSetupPreHandler } from \"./sessionSetupPreHandler\";\n export { loadRepositoryCounters } from \"./loadRepositoryCounters\";\n+export { spawnConcatChunks } from \"./spawnConcatChunks\";\n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":8,"borderTopRightRadius":8,"borderBottomLeftRadius":8,"borderBottomRightRadius":8}}, "Code$$18": {"code":"@@ -0,0 +1,16 @@\n+import { ChildProcessWithoutNullStreams } from \"node:child_process\";\n+\n+export function spawnConcatChunks(\n+ process: ChildProcessWithoutNullStreams,\n+): Promise {\n+ return new Promise((resolve, reject) => {\n+ let buffer = [] as string[];\n+ process.stdout.on(\"data\", (data) => buffer.push(data));\n+ process.stderr.on(\"data\", (data) => {\n+ reject(new Error(Buffer.from(data).toString(\"utf-8\")));\n+ });\n+ process.stdout.on(\"close\", () => {\n+ resolve(buffer.join(\"\"));\n+ });\n+ });\n+}\n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":8,"borderTopRightRadius":8,"borderBottomLeftRadius":8,"borderBottomRightRadius":8}}, "Code$$19": {"code":"@@ -55,7 +55,7 @@ end\n \n def sideband_println(msg : String)\n txt = \"#{SIDEBAND_PREFIX}#{msg.ends_with?(\"\\n\") ? msg : msg + \"\\n\"}\"\n- STDERR.puts txt\n+ # STDERR.puts txt\n write_to_file txt\n end\n \n","language":"diff","themeScheme":"light","style":{"borderTopLeftRadius":8,"borderTopRightRadius":8,"borderBottomLeftRadius":8,"borderBottomRightRadius":8}}, }; function afterRevival(revivalResults) { return undefined; } $IslandsRuntime.reviveIslands(islands, islandsProps, islandsEls) .then(afterRevival) .catch(afterRevival); })(IslandsRuntime);