feat(api): make it possible to create git repository via repositoryService + make auth/repo resolvers integrated with database model and remove 100% of mock data
+ 166
- 90
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1663449267544,
+  "_generatedAtUnix": 1663457839996,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

...
@@ -38,7 +38,7 @@
       "pathSource": "./app/views/auth/RegisterView.tsx"
     },
     "RepositoryExploreView": {
-      "hash": "d776d1d9f5a8559427f64fe13e14886ac3afe526",
+      "hash": "53c3c318d1f6dd198a9627e9024dfc5737091a80",
       "pathSource": "./app/views/repository/RepositoryExploreView.tsx"
     }
   }

app/controllers/repository/getRepositoryExploreView.ts
@@ -1,14 +1,46 @@
 // 1st-party
 import { ReqHandler } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { ResourceVisibility } from "@prisma/client";
+import { join, resolve } from "node:path";
 // app
+import { Env } from "../../env";
 import { makeRepositoryService } from "../../services/repository";
 // app views
 import RepositoryExploreView, {
   RepositoryExploreViewProps,
 } from "../../views/repository/RepositoryExploreView";
 
+const TEST_ENABLE_CREATE_REPOSITORY: boolean = false;
+
 const getRepositoryExploreView: ReqHandler = async (request, reply) => {
   const repoService = makeRepositoryService({ request });
+
+  if ((TEST_ENABLE_CREATE_REPOSITORY as boolean) === true) {
+    const newRepo = await repoService.createRepository({
+      parentOrgSlug: "ethicdevs",
+      repoSlug: "gitfoss-on-gitfoss",
+      repoData: {
+        avatarUri: null,
+        displayName: "GitFOSS on GitFOSS",
+        keywords: ["gitfoss", "self-hosted", "git", "foss", "server"],
+        shortDescription:
+          "GitFOSS is a Git Server and Client that is Free and Open-Source Software and that you can easily self-host",
+        visibility: ResourceVisibility.PUBLIC,
+        websiteUrl: "https://gitfoss.io/gitfoss/gitfoss",
+      },
+      repoInitFlags: {
+        orgRepositoriesDir: resolve(
+          join(Env.GIT_REPOSITORIES_ROOT, "ethicdevs")
+        ),
+        withBaseReadmeFile: false,
+        withLicenseFile: "",
+      },
+    });
+
+    console.log("MADE REPO FROM API:", newRepo);
+  }
+
   const repositories = await repoService.getRepositoryExploreCollection();
 
   const reqHandler = reply.makeRequestHandler(request, reply);

@@ -34,7 +34,6 @@ let server: null | AppServer = null;
 
 async function main(): Promise<AppServer> {
   const env = getEnv();
-  const gitService = makeGitServerService({});
   const prisma = new PrismaClient();
 
   const depsBaseUrl = `/${Paths.PUBLIC_FOLDER_NAME}/${Paths.ASSET_DEPS_FOLDER_NAME}`;

...
@@ -108,7 +107,21 @@ async function main(): Promise<AppServer> {
       s.addHook("preHandler", localAppDomainPreHandler);
       s.decorateReply("makeRequestHandler", makeRequestHandler);
 
-      s.register(cryptoPlugin);
+      s.register(cryptoPlugin).after(() => {
+        const gitService = makeGitServerService({
+          cryptoService: s.cryptoService,
+          request: {
+            prisma,
+          } as any,
+        });
+
+        s.register(fastifyGitServer, {
+          withSideBandMessages: true,
+          authorizationResolver: gitService.authorizationResolver,
+          repositoryResolver: gitService.repositoryResolver,
+          onPush: gitService.onPushEvent,
+        });
+      });
 
       s.register(prismaPlugin, { prisma });
 

...
@@ -146,13 +159,6 @@ async function main(): Promise<AppServer> {
         },
       });
 
-      s.register(fastifyGitServer, {
-        withSideBandMessages: true,
-        authorizationResolver: gitService.authorizationResolver,
-        repositoryResolver: gitService.repositoryResolver,
-        onPush: gitService.onPushEvent,
-      });
-
       s.get("/interceptor-imsw.js", {}, async (_, reply) => {
         return reply.sendFile("interceptor-imsw.js");
       });

app/services/gitServer/authorizationResolver.ts
@@ -3,29 +3,54 @@ import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
 import { GitServer } from "@ethicdevs/fastify-git-server";
 // app
 import { GitServerServiceDeps } from "./types";
-import { ORGS_REPOS } from "./temp_mock";
 
 const makeAuthorizationResolver: ServiceMethodFactory<
   GitServerServiceDeps,
   [string, GitServer.AuthCredentials],
   PromiseLike<boolean>
-> = (_) => {
-  return async (repoSlug, { username, password }) => {
-    const [org, repo] = repoSlug.split("/");
-    if (org == null || org in ORGS_REPOS === false || ORGS_REPOS[org] == null) {
-      throw new Error(`Unknown organization "${org}".`);
+> = ({ cryptoService, request }) => {
+  return async (repoPath, { username, password }) => {
+    const [orgSlug, repoSlug] = repoPath.split("/");
+
+    const hashedPassword = cryptoService.computeHash(password);
+    const user = await request.prisma.user.findUnique({
+      where: {
+        username,
+      },
+    });
+
+    if (user == null) {
+      return false;
+    }
+
+    const org = await request.prisma.organization.findUnique({
+      include: {
+        owner: true,
+      },
+      where: {
+        slug: orgSlug,
+      },
+    });
+
+    if (org == null) {
+      return false;
     }
-    if (
-      repo == null ||
-      repo in ORGS_REPOS[org] === false ||
-      ORGS_REPOS[org][repo] == null
-    ) {
-      throw new Error(
-        `Unknown repository "${repo}" in (known) organization "${org}".`
-      );
+
+    const repo = await request.prisma.repository.findFirst({
+      where: {
+        slug: repoSlug,
+        organization: {
+          slug: orgSlug,
+        },
+      },
+    });
+
+    if (repo == null) {
+      return false;
     }
-    const orgRepo = ORGS_REPOS[org][repo];
-    return username === orgRepo.username && password === orgRepo.password;
+
+    // for now only allow owner to push to its own repo (no members in model yet)
+    return org.ownerId === user.id && hashedPassword === user.hashedPassword;
   };
 };
 

app/services/gitServer/repositoryResolver.ts
@@ -1,33 +1,56 @@
+// std
+import { join, resolve } from "node:path";
 // 1st-party
 import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
 import { GitServer } from "@ethicdevs/fastify-git-server";
 // app
-import { GitServerServiceDeps } from "./types";
-import { ORGS_REPOS } from "./temp_mock";
+import type { GitServerServiceDeps } from "./types";
+import { Env } from "../../env";
 
 const makeRepositoryResolver: ServiceMethodFactory<
   GitServerServiceDeps,
   [string],
   PromiseLike<GitServer.RepositoryResolverResult>
-> = (_) => {
-  return async (repoSlug) => {
-    const [org, repo] = repoSlug.split("/");
-    if (org == null || org in ORGS_REPOS === false || ORGS_REPOS[org] == null) {
+> = ({ request }) => {
+  return async (repoPath) => {
+    const [orgSlug, repoSlug] = repoPath.split("/");
+
+    const org = await request.prisma.organization.findUnique({
+      where: {
+        slug: orgSlug,
+      },
+    });
+
+    if (org == null) {
       throw new Error(`Unknown organization "${org}".`);
     }
-    if (
-      repo == null ||
-      repo in ORGS_REPOS[org] === false ||
-      ORGS_REPOS[org][repo] == null
-    ) {
+
+    const repo = await request.prisma.repository.findFirst({
+      include: {
+        organization: true,
+      },
+      where: {
+        slug: repoSlug,
+        organization: {
+          slug: orgSlug,
+        },
+      },
+    });
+
+    if (repo == null) {
       throw new Error(
         `Unknown repository "${repo}" in (known) organization "${org}".`
       );
     }
-    const orgRepo = ORGS_REPOS[org][repo];
+
+    // /!\ Notice how it use db's data instead of user's input to build path.
+    const gitRepositoryDir = resolve(
+      join(Env.GIT_REPOSITORIES_ROOT, org.slug, repo.slug)
+    );
+
     return {
       authMode: GitServer.AuthMode.ALWAYS,
-      gitRepositoryDir: orgRepo.gitRepositoryDir,
+      gitRepositoryDir,
     };
   };
 };

file deleted
app/services/gitServer/temp_mock.ts
@@ -1,29 +0,0 @@
-import { join, resolve } from "node:path";
-import { Env } from "../../env";
-
-export const ORGS_REPOS: Record<
-  string,
-  Record<
-    string,
-    { username: string; password: string; gitRepositoryDir: string }
-  >
-> = {
-  ethicdevs: {
-    "test-repository": {
-      username: "ethicdevs",
-      password: "secret",
-      gitRepositoryDir: resolve(
-        join(Env.GIT_REPOSITORIES_ROOT, "ethicdevs", "test-repository")
-      ),
-    },
-  },
-  wnemencha: {
-    "react-monolith-samples": {
-      username: "wnemencha",
-      password: "secret",
-      gitRepositoryDir: resolve(
-        join(Env.GIT_REPOSITORIES_ROOT, "wnemencha", "react-monolith-samples")
-      ),
-    },
-  },
-};

app/services/gitServer/types.ts
@@ -1,5 +1,10 @@
+// 1st-party
 import { GitServer } from "@ethicdevs/fastify-git-server";
 import { ServiceApiContract } from "@ethicdevs/react-monolith";
+// 3rd-party
+import { FastifyRequest } from "fastify";
+// app
+import { CryptoServiceAPI } from "../crypto/types";
 
 export interface GitServerServiceAPI extends ServiceApiContract {
   authorizationResolver(

...
@@ -12,4 +17,7 @@ export interface GitServerServiceAPI extends ServiceApiContract {
   onPushEvent(event: GitServer.Event): void;
 }
 
-export interface GitServerServiceDeps {}
+export interface GitServerServiceDeps {
+  cryptoService: CryptoServiceAPI;
+  request: FastifyRequest;
+}

app/services/repository/createRepository.ts
@@ -1,3 +1,5 @@
+// std
+import { spawn } from "node:child_process";
 // 1st-party
 import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
 // generated via script[generate:prisma]

...
@@ -12,7 +14,7 @@ const makeCreateRepository: ServiceMethodFactory<
 > = ({ request }) => {
   return async ({ parentOrgSlug, repoSlug, repoData, repoInitFlags }) => {
     const {
-      gitRepositoryDir,
+      orgRepositoriesDir,
       withBaseReadmeFile: _,
       withLicenseFile: __,
     } = repoInitFlags;

...
@@ -46,21 +48,30 @@ const makeCreateRepository: ServiceMethodFactory<
     );
 
     console.log(
-      `[..] creating repository folder with "git init --bare --shared=group"`
+      `[..] creating repository folder with "git init --bare --shared=group" in org directory:`,
+      orgRepositoriesDir
     );
 
-    const gitInitBareRepoProcess = request.spawnGitCommand(
-      ["init --bare --shared=group"],
-      gitRepositoryDir
+    const gitInitBareRepoProcess = spawn(
+      "git",
+      ["init", "--bare", "--shared=group", `${newRepo.slug}.git`],
+      { cwd: orgRepositoriesDir.toString() }
+    );
+    const gitInitBareRepoResult = await new Promise<string>(
+      (resolve, reject) => {
+        let buffer = [] as string[];
+        gitInitBareRepoProcess.stdout.on("data", (data) => {
+          console.log("stdout::data:", data);
+          buffer.push(data);
+        });
+        gitInitBareRepoProcess.stderr.on("data", (data) => {
+          reject(new Error(Buffer.from(data).toString("utf-8")));
+        });
+        gitInitBareRepoProcess.stdout.on("close", () => {
+          resolve(buffer.join(""));
+        });
+      }
     );
-
-    const gitInitBareRepoResult = await new Promise<string>((resolve) => {
-      let buffer = [] as string[];
-      gitInitBareRepoProcess.stdout.on("data", (chunk) => buffer.push(chunk));
-      gitInitBareRepoProcess.stdout.on("close", () => {
-        resolve(buffer.join(""));
-      });
-    });
 
     console.log(
       `[ok] finished execution of "git init --bare --shared=group" with result:\n\t`,

app/services/repository/index.ts
@@ -6,11 +6,13 @@ import type { RepositoryServiceAPI, RepositoryServiceDeps } from "./types";
 import { default as makeGetHttpCloneUrl } from "./getHttpCloneUrl";
 import { default as makeGetRepositoryExploreCollection } from "./getRepositoryExploreCollection";
 import { default as makeGetSshCloneUrl } from "./getSshCloneUrl";
+import { default as makeCreateRepository } from "./createRepository";
 
 export const makeRepositoryService = makeService<
   RepositoryServiceAPI,
   RepositoryServiceDeps
 >({
+  createRepository: makeCreateRepository,
   getHttpCloneUrl: makeGetHttpCloneUrl,
   getRepositoryExploreCollection: makeGetRepositoryExploreCollection,
   getSshCloneUrl: makeGetSshCloneUrl,

app/services/repository/types.ts
@@ -15,18 +15,14 @@ export interface CreateRepositoryDTO {
     "id" | "slug" | "createdAt" | "updatedAt" | "organizationId"
   >;
   repoInitFlags: {
-    gitRepositoryDir: PathLike;
+    orgRepositoriesDir: PathLike;
     withBaseReadmeFile: boolean;
     withLicenseFile: string;
   };
 }
 
 export interface RepositoryServiceAPI extends ServiceApiContract {
-  createRepository(
-    parentOrgSlug: string,
-    repoSlug: string,
-    repoData: Repository
-  ): Promise<Repository>;
+  createRepository(dto: CreateRepositoryDTO): Promise<Repository>;
   getRepositoryExploreCollection(): Promise<Repository[]>;
   getHttpCloneUrl(repository: Repository): Promise<string>;
   getSshCloneUrl(repository: Repository): Promise<string>;

app/views/repository/RepositoryExploreView.tsx
@@ -23,7 +23,9 @@ const RepositoryExploreView: ReactView<RepositoryExploreViewProps> = ({
           <div key={repo.id}>
             <h2>{repo.displayName}</h2>
             <code>
-              <pre>{JSON.stringify(repo, null, 2)}</pre>
+              <pre style={{ maxWidth: 600 }}>
+                {JSON.stringify(repo, null, 2)}
+              </pre>
             </code>
           </div>
         ))}