feat(repository): make private repositories actually private (not reachable when not logged as a member of parent org)
+ 107
- 12
app/controllers/repository/getRepositoryBrowserView.ts
@@ -14,6 +14,7 @@ import RepositoryBrowserView, {
 import RepositoryDetailsView, {
   RepositoryDetailsViewProps,
 } from "../../views/repository/RepositoryDetailsView";
+import { ResourceVisibility } from "@prisma/client";
 
 const getRepositoryBrowserView: ReqHandler = async (request, reply) => {
   const params =

...
@@ -34,10 +35,20 @@ const getRepositoryBrowserView: ReqHandler = async (request, reply) => {
   const parentOrg = await orgService.getOrganizationBySlug(orgSlug);
   const repo = await repoService.getRepository(orgSlug, repoSlug);
 
-  if (repo == null) {
+  if (parentOrg == null || repo == null) {
     return reply.status(404).callNotFound();
   }
 
+  if (repo.visibility === ResourceVisibility.PRIVATE) {
+    if (currentUser == null) {
+      return reply.status(404).callNotFound();
+    } else if (
+      (await repoService.canUserAccessRepository(currentUser, repo)) === false
+    ) {
+      return reply.status(404).callNotFound();
+    }
+  }
+
   const reqHandler = reply.makeRequestHandler(request, reply);
 
   if (path.endsWith("/")) {

app/controllers/repository/getRepositoryCommitsLogView.ts
@@ -1,10 +1,12 @@
 // 1st-party
 import type { ReqHandler } from "@ethicdevs/react-monolith";
+import { ResourceVisibility } from "@prisma/client";
 // app
 import { AppRoute, AppRoutesParams } from "../../routes";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makeRepositoryService } from "../../services/repository";
+import { makeUsersService } from "../../services/user";
 // app views
 import RepositoryCommitsLogView, {
   RepositoryCommitsLogViewProps,

...
@@ -16,14 +18,31 @@ const getRepositoryCommitsLogView: ReqHandler = async (request, reply) => {
 
   const orgService = makeOrganizationService({ request });
   const repoService = makeRepositoryService({ request });
+  const usersService = makeUsersService({ request });
+
+  const currentUser =
+    request.session.data.authenticated &&
+    request.session.data.curr_user_uid != null
+      ? await usersService.getUserById(request.session.data.curr_user_uid)
+      : null;
 
   const parentOrg = await orgService.getOrganizationBySlug(orgSlug);
   const repo = await repoService.getRepository(orgSlug, repoSlug);
 
-  if (repo == null) {
+  if (parentOrg == null || repo == null) {
     return reply.status(404).callNotFound();
   }
 
+  if (repo.visibility === ResourceVisibility.PRIVATE) {
+    if (currentUser == null) {
+      return reply.status(404).callNotFound();
+    } else if (
+      (await repoService.canUserAccessRepository(currentUser, repo)) === false
+    ) {
+      return reply.status(404).callNotFound();
+    }
+  }
+
   const history = await repoService.getRepositoryCommitLog(repo);
 
   const reqHandler = reply.makeRequestHandler(request, reply);

app/controllers/repository/getRepositoryCompareView.ts
@@ -1,7 +1,7 @@
 // 1st-party
 import { ReqHandler } from "@ethicdevs/react-monolith";
 // generated via script[generate:prisma]
-import { User } from "@prisma/client";
+import { ResourceVisibility } from "@prisma/client";
 import { AppRoute, AppRoutesParams } from "app/routes";
 // app services
 import { makeOrganizationService } from "../../services/organization";

...
@@ -13,13 +13,18 @@ import RepositoryCompareView, {
 } from "../../views/repository/RepositoryCompareView";
 
 const getRepositoryCompareView: ReqHandler = async (request, reply) => {
-  const { curr_user_uid } = request.session.data;
   const { orgSlug, repoSlug, refA, refB } =
     request.params as AppRoutesParams[AppRoute.REPOSITORY_COMPARE]["params"];
 
   const orgService = makeOrganizationService({ request });
   const repoService = makeRepositoryService({ request });
-  const userService = makeUsersService({ request });
+  const usersService = makeUsersService({ request });
+
+  const currentUser =
+    request.session.data.authenticated &&
+    request.session.data.curr_user_uid != null
+      ? await usersService.getUserById(request.session.data.curr_user_uid)
+      : null;
 
   const parentOrg = await orgService.getOrganizationBySlug(orgSlug);
   const repo = await repoService.getRepository(orgSlug, repoSlug);

...
@@ -28,10 +33,14 @@ const getRepositoryCompareView: ReqHandler = async (request, reply) => {
     return reply.status(404).callNotFound();
   }
 
-  let currentUser: null | User = null;
-
-  if (curr_user_uid != null) {
-    currentUser = await userService.getUserById(curr_user_uid);
+  if (repo.visibility === ResourceVisibility.PRIVATE) {
+    if (currentUser == null) {
+      return reply.status(404).callNotFound();
+    } else if (
+      (await repoService.canUserAccessRepository(currentUser, repo)) === false
+    ) {
+      return reply.status(404).callNotFound();
+    }
   }
 
   const filesDiffs = await repoService.getRepositoryRefDiff(repo, refA, refB);

app/controllers/repository/getRepositoryDetailsView.ts
@@ -11,6 +11,11 @@ import { makeUsersService } from "../../services/user";
 import RepositoryDetailsView, {
   RepositoryDetailsViewProps,
 } from "../../views/repository/RepositoryDetailsView";
+import {
+  Organization,
+  OrganizationMembership,
+  ResourceVisibility,
+} from "@prisma/client";
 
 const getRepositoryDetailsView: ReqHandler = async (request, reply) => {
   const { orgSlug, repoSlug } =

...
@@ -26,15 +31,30 @@ const getRepositoryDetailsView: ReqHandler = async (request, reply) => {
       ? await usersService.getUserById(request.session.data.curr_user_uid)
       : null;
 
-  const parentOrg = await orgService.getOrganizationBySlug(orgSlug);
   const path = "/";
   const ref = "HEAD";
+
+  const parentOrg = (await orgService.getOrganizationBySlug(orgSlug, {
+    memberships: true,
+  })) as Organization & {
+    memberships: OrganizationMembership[];
+  };
   const repo = await repoService.getRepository(orgSlug, repoSlug);
 
-  if (repo == null) {
+  if (parentOrg == null || repo == null) {
     return reply.status(404).callNotFound();
   }
 
+  if (repo.visibility === ResourceVisibility.PRIVATE) {
+    if (currentUser == null) {
+      return reply.status(404).callNotFound();
+    } else if (
+      (await repoService.canUserAccessRepository(currentUser, repo)) === false
+    ) {
+      return reply.status(404).callNotFound();
+    }
+  }
+
   const readmeFiles = await repoService.isFileInRepositoryPath(
     repo,
     "",

new file
app/services/repository/canUserAccessRepository.ts
@@ -0,0 +1,33 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import type { Repository, User } from "@prisma/client";
+// service
+import type { RepositoryServiceDeps } from "./types";
+
+const makeCanUserAccessRepository: ServiceMethodFactory<
+  RepositoryServiceDeps,
+  [User, Repository],
+  Promise<boolean>
+> = ({ request }) => {
+  return async (user, repo) => {
+    const parentOrg = await request.prisma.organization.findUnique({
+      include: {
+        memberships: true,
+      },
+      where: {
+        id: repo.organizationId,
+      },
+    });
+
+    return (
+      parentOrg != null &&
+      (user.id === parentOrg.ownerId ||
+        parentOrg.memberships.find(
+          (m) => user.id === m.userId && m.revokedAt == null
+        ) != null)
+    );
+  };
+};
+
+export default makeCanUserAccessRepository;

app/services/repository/index.ts
@@ -3,6 +3,7 @@ import { makeService } from "@ethicdevs/react-monolith";
 // app
 import type { RepositoryServiceAPI, RepositoryServiceDeps } from "./types";
 // service methods
+import { default as makeCanUserAccessRepository } from "./canUserAccessRepository";
 import { default as makeCreateRepository } from "./createRepository";
 import { default as makeGetRepository } from "./getRepository";
 import { default as makeGetRepositoryBranches } from "./getRepositoryBranches";

...
@@ -22,6 +23,7 @@ export const makeRepositoryService = makeService<
   RepositoryServiceAPI,
   RepositoryServiceDeps
 >({
+  canUserAccessRepository: makeCanUserAccessRepository,
   createRepository: makeCreateRepository,
   getRepository: makeGetRepository,
   getRepositoryBranches: makeGetRepositoryBranches,

app/services/repository/types.ts
@@ -5,7 +5,7 @@ import type { ServiceApiContract } from "@ethicdevs/react-monolith";
 // 3rd-party
 import type { FastifyRequest } from "fastify";
 // generated via script[generate:prisma]
-import type { Organization, Repository } from "@prisma/client";
+import type { Organization, Repository, User } from "@prisma/client";
 // app
 import type {
   RepositoryFile,

...
@@ -31,6 +31,7 @@ export interface CreateRepositoryDTO {
 }
 
 export interface RepositoryServiceAPI extends ServiceApiContract {
+  canUserAccessRepository(user: User, repo: Repository): Promise<boolean>;
   createRepository(dto: CreateRepositoryDTO): Promise<Repository>;
   getRepository(orgSlug: string, repoSlug: string): Promise<Repository | null>;
   getRepositoryBranches(repository: Repository): Promise<string[]>;