GitFOSS
refactor: add user service methods to get user organizations/memberships + cleanup the services methods & their names
+ 235
- 75
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1663468360993,
+  "_generatedAtUnix": 1663506813442,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

...
@@ -10,7 +10,7 @@
       "pathSourceMap": "./public/.islands/InstantRouterIndicator.bundle.js.map"
     },
     "RepositoryCreateForm": {
-      "hash": "8ef66a3025d8c4f72d871d32035bbf47f8e47809",
+      "hash": "13bc29f8548d0df057f56a78bb6f2f6b31cb7d31",
       "pathSource": "./app/islands/RepositoryCreateForm.tsx",
       "pathBundle": "./public/.islands/RepositoryCreateForm.bundle.js",
       "pathSourceMap": "./public/.islands/RepositoryCreateForm.bundle.js.map"

new file
app/controllers/auth/getLogoutAction.ts
@@ -0,0 +1,12 @@
+// 3rd-party
+import type { ReqHandler } from "@ethicdevs/react-monolith";
+// app
+import { AppRoute } from "../../routes";
+
+const getLogoutAction: ReqHandler = async (request, reply) => {
+  await request.session.destroy();
+  reply.redirect(request.namedViewsPathMap[AppRoute.HOME]);
+  return reply;
+};
+
+export default getLogoutAction;

app/controllers/auth/index.ts
@@ -1,5 +1,6 @@
 import { default as getDashboardView } from "./getDashboardView";
 import { default as getLoginView } from "./getLoginView";
+import { default as getLogoutAction } from "./getLogoutAction";
 import { default as getRegisterView } from "./getRegisterView";
 import { default as postLoginAction } from "./postLoginAction";
 import { default as postRegisterAction } from "./postRegisterAction";

...
@@ -7,6 +8,7 @@ import { default as postRegisterAction } from "./postRegisterAction";
 export const AuthController = {
   getDashboardView,
   getLoginView,
+  getLogoutAction,
   getRegisterView,
   postLoginAction,
   postRegisterAction,

app/controllers/auth/postLoginAction.ts
@@ -49,7 +49,7 @@ const postLoginView: ReqHandler = async (request, reply) => {
     });
   }
 
-  const [isLoginAllowed, user] = await authService.shouldAllowUserLogin(
+  const [isLoginAllowed, user] = await authService.isUserLoginAllowed(
     emailAddress,
     password
   );

app/controllers/repository/getRepositoryCreateView.ts
@@ -1,17 +1,29 @@
 // 1st-party
 import { ReqHandler } from "@ethicdevs/react-monolith";
 // app
-// import { makeRepositoryService } from "../../services/repository";
+import { AppRoute } from "../../routes";
+import { makeUsersService } from "../../services/user";
 // app views
 import RepositoryCreateView, {
   RepositoryCreateViewProps,
 } from "../../views/repository/RepositoryCreateView";
 
 const getRepositoryCreateView: ReqHandler = async (request, reply) => {
-  // const repoService = makeRepositoryService({ request });
+  if (
+    request.session.data.authenticated === false ||
+    request.session.data.curr_user_uid == null
+  ) {
+    reply.redirect(302, request.namedViewsPathMap[AppRoute.AUTH_LOGIN]);
+    return reply;
+  }
+
   const reqHandler = reply.makeRequestHandler(request, reply);
+  const usersService = makeUsersService({ request });
+
   return reqHandler<RepositoryCreateViewProps>(RepositoryCreateView.name, {
-    availableParentOrgs: [],
+    availableParentOrgs: await usersService.getUserOrganizations(
+      request.session.data.curr_user_uid
+    ),
   });
 };
 

app/islands/RepositoryCreateForm.tsx
@@ -129,24 +129,6 @@ const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
     <div>
       <fieldset>
         <legend>Repository details</legend>
-        {/* Parent Organization Select */}
-        <Grid.Col fluid nowrap>
-          <label htmlFor={"repo_display_name"}>
-            Owner Organization <span>(*)</span>:
-          </label>
-          <select
-            defaultValue={initialValues?.parent_org_slug}
-            name={"parent_org_slug"}
-            required
-            style={styles.inputMaxWidth}
-          >
-            {availableParentOrgs.map((org) => (
-              <option key={org.id} value={org.slug}>
-                {org.displayName || org.slug}
-              </option>
-            ))}
-          </select>
-        </Grid.Col>
         {/* Repository Name */}
         <Grid.Col fluid nowrap>
           <label htmlFor={"repo_display_name"}>

...
@@ -162,20 +144,65 @@ const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
             value={displayName}
           />
         </Grid.Col>
-        {/* Repository Slug */}
+        <Grid.Row fluid nowrap alignItems={"center"}>
+          {/* Parent Organization Select */}
+          <Grid.Col fluid nowrap>
+            <label htmlFor={"repo_display_name"}>
+              Owner Organization <span>(*)</span>:
+            </label>
+            <select
+              defaultValue={
+                availableParentOrgs.length >= 1
+                  ? availableParentOrgs[0].slug
+                  : initialValues?.parent_org_slug
+              }
+              name={"parent_org_slug"}
+              required
+              style={styles.inputMaxWidth}
+            >
+              {availableParentOrgs.map((org) => (
+                <option key={org.id} value={org.slug}>
+                  {org.displayName || org.slug}
+                </option>
+              ))}
+            </select>
+          </Grid.Col>
+          {/* Repository Slug */}
+          <Grid.Col fluid nowrap>
+            <label htmlFor={"repo_slug"}>
+              Repository Slug <span>(*)</span>:
+            </label>
+            <input
+              name={"repo_slug"}
+              onChange={onSlugInputChange}
+              placeholder={"i.e. my-super-project"}
+              required
+              style={styles.inputMaxWidth}
+              type={"text"}
+              value={slug}
+            />
+          </Grid.Col>
+        </Grid.Row>
+        {/* Repository Visibility */}
         <Grid.Col fluid nowrap>
-          <label htmlFor={"repo_slug"}>
-            Repository Slug <span>(*)</span>:
+          <label htmlFor={"repo_visibility"}>
+            Repository Visibility <span>(*)</span>:
           </label>
-          <input
-            name={"repo_slug"}
-            onChange={onSlugInputChange}
-            placeholder={"i.e. my-super-project"}
-            required
+          <select
+            defaultValue={"PRIVATE"}
+            name={"repo_visibility"}
             style={styles.inputMaxWidth}
-            type={"text"}
-            value={slug}
-          />
+          >
+            <option key={"private"} value={"PRIVATE"}>
+              Private
+            </option>
+            <option key={"unlisted"} value={"UNLISTED"}>
+              Unlisted
+            </option>
+            <option key={"public"} value={"PUBLIC"}>
+              Public
+            </option>
+          </select>
         </Grid.Col>
       </fieldset>
       <fieldset>

...
@@ -273,7 +300,9 @@ const RepositoryCreateForm: ReactIsland<RepositoryCreateFormProps> = ({
         </Grid.Row>
       </fieldset>
       {/* Submit Button */}
-      <Button type={"submit"}>Create Repository</Button>
+      <Button style={styles.inputMaxWidth} type={"submit"}>
+        Create Repository
+      </Button>
     </div>
   );
 };

@@ -19,6 +19,7 @@ export enum AppRoute {
   AUTH_REGISTER_ACTION = "auth.register.action",
   AUTH_LOGIN = "auth.login",
   AUTH_LOGIN_ACTION = "auth.login.action",
+  AUTH_LOGOUT_ACTION = "auth.logout.action",
   USER_DASHBOARD = "user.dashboard",
   REPOSITORY_EXPLORE = "repository.explore",
 }

...
@@ -43,6 +44,7 @@ export interface AppRoutesParams extends IRouteParams {
       password: string;
     };
   };
+  [AppRoute.AUTH_LOGOUT_ACTION]: undefined;
   [AppRoute.USER_DASHBOARD]: undefined;
   [AppRoute.REPOSITORY_EXPLORE]: undefined;
 }

...
@@ -87,12 +89,14 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
       },
     },
   },
+  [AppRoute.AUTH_LOGOUT_ACTION]: undefined,
   [AppRoute.USER_DASHBOARD]: undefined,
   [AppRoute.REPOSITORY_EXPLORE]: undefined,
 };
 
 const RootAppRouter: AppRouter = () => {
   const guestOrDashboardRedirect = guestOrRedirect("/dashboard");
+  const loggedOrLoginRedirect = authenticatedOrLogin();
   return (
     <Router.Root>
       <></>

...
@@ -144,12 +148,20 @@ const RootAppRouter: AppRouter = () => {
           preHandler={guestOrDashboardRedirect}
           handler={AuthController.postLoginAction}
         />
+        <Router.Route
+          name={AppRoute.AUTH_LOGOUT_ACTION}
+          method={"GET"}
+          path={"/auth/logout"}
+          schema={AppRoutesSchemas[AppRoute.AUTH_LOGOUT_ACTION]}
+          preHandler={loggedOrLoginRedirect}
+          handler={AuthController.getLogoutAction}
+        />
         {/* --- */}
         <Router.Route
           name={AppRoute.USER_DASHBOARD}
           method={"GET"}
           path={"/dashboard"}
-          preHandler={authenticatedOrLogin()}
+          preHandler={loggedOrLoginRedirect}
           handler={AuthController.getDashboardView}
         />
         {/* --- */}

...
@@ -163,7 +175,7 @@ const RootAppRouter: AppRouter = () => {
           name={AppRoute.REPOSITORY_EXPLORE}
           method={"GET"}
           path={"/repo/new"}
-          preHandler={authenticatedOrLogin()}
+          preHandler={loggedOrLoginRedirect}
           handler={RepositoryController.getRepositoryCreateView}
         />
       </Router.Group>

app/services/auth/createUser.ts
@@ -1,7 +1,12 @@
 // 1st-party
 import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
 // generated via script[generate:prisma]
-import { User } from "@prisma/client";
+import {
+  OrganizationKind,
+  OrganizationRole,
+  ResourceVisibility,
+  User,
+} from "@prisma/client";
 // app
 import type { AuthServiceCreateUserDTO, AuthServiceDeps } from "./types";
 

...
@@ -10,6 +15,7 @@ const makeCreateUser: ServiceMethodFactory<
   [AuthServiceCreateUserDTO],
   Promise<User>
 > = ({ cryptoService, request }) => {
+  // create a user with a personal organization
   return async ({ emailAddress, username, password }) => {
     const user = await request.prisma.user.create({
       data: {

...
@@ -19,6 +25,37 @@ const makeCreateUser: ServiceMethodFactory<
       },
     });
 
+    try {
+      await request.prisma.organization.create({
+        data: {
+          kind: OrganizationKind.PERSONAL,
+          slug: username,
+          visibility: ResourceVisibility.PRIVATE,
+          owner: {
+            connect: {
+              id: user.id,
+            },
+          },
+          memberships: {
+            create: {
+              role: OrganizationRole.OWNER,
+              user: {
+                connect: {
+                  id: user.id,
+                },
+              },
+            },
+          },
+        },
+      });
+    } catch (err) {
+      const error = err as Error;
+      console.error(
+        `Could not create User's personal's Organization. Error: ${error.message}`
+      );
+      return user;
+    }
+
     return user;
   };
 };

app/services/auth/index.ts
@@ -6,16 +6,11 @@ import type { AuthServiceAPI, AuthServiceDeps } from "./types";
 import { default as makeCreateUser } from "./createUser";
 import { default as makeIsExistingUsername } from "./isExistingUsername";
 import { default as makeIsExistingEmailAddress } from "./isExistingEmailAddress";
-import { default as makeShouldAllowUserLogin } from "./shouldAllowUserLogin";
+import { default as makeIsUserLoginAllowed } from "./isUserLoginAllowed";
 
 export const makeAuthService = makeService<AuthServiceAPI, AuthServiceDeps>({
   createUser: makeCreateUser,
   isExistingUsername: makeIsExistingUsername,
   isExistingEmailAddress: makeIsExistingEmailAddress,
-  isExistingUserUid: () => () => undefined,
-  findUserByUid: () => () => undefined,
-  findUserByUsername: () => () => undefined,
-  sendUserEmailAddressV8nEmail: () => () => undefined,
-  shouldAllowUserLogin: makeShouldAllowUserLogin,
-  validateUserEmailAddress: () => () => Promise.resolve(undefined),
+  isUserLoginAllowed: makeIsUserLoginAllowed,
 });

app/services/auth/shouldAllowUserLogin.ts -> app/services/auth/isUserLoginAllowed.ts
@@ -5,7 +5,7 @@ import { User } from "@prisma/client";
 // app
 import type { AuthServiceDeps } from "./types";
 
-const makeShouldAllowUserLogin: ServiceMethodFactory<
+const makeIsUserLoginAllowed: ServiceMethodFactory<
   AuthServiceDeps,
   [string, string],
   Promise<[boolean, User | null]>

...
@@ -24,4 +24,4 @@ const makeShouldAllowUserLogin: ServiceMethodFactory<
   };
 };
 
-export default makeShouldAllowUserLogin;
+export default makeIsUserLoginAllowed;

app/services/auth/types.ts
@@ -20,18 +20,10 @@ export interface AuthServiceAPI extends ServiceApiContract {
   createUser(dto: AuthServiceCreateUserDTO): Promise<User>; // implemented.
   isExistingUsername(username: string): Promise<boolean>; // implemented.
   isExistingEmailAddress(emailAddress: string): Promise<boolean>; // implemented.
-  isExistingUserUid(userUid: string): boolean;
-  findUserByUsername(username: string): void;
-  findUserByUid(userUid: string): void;
-  sendUserEmailAddressV8nEmail(args: unknown[]): void;
-  shouldAllowUserLogin(
+  isUserLoginAllowed(
     emailAddress: string,
     password: string
-  ): Promise<[boolean, User | null]>;
-  validateUserEmailAddress(
-    userUid: string,
-    emailAddress: string
-  ): Promise<void>;
+  ): Promise<[boolean, User | null]>; // implemented
 }
 
 export interface AuthServiceDeps extends ServiceDependencies {

app/services/repository/getRepositoryExploreCollection.ts
@@ -4,8 +4,8 @@ import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
 import { Repository, ResourceVisibility } from "@prisma/client";
 // service
 import type { RepositoryServiceDeps } from "./types";
-import { default as makeGetHttpCloneUrl } from "./getHttpCloneUrl";
-import { default as makeGetSshCloneUrl } from "./getSshCloneUrl";
+import { default as makeGetRepositoryHTTPCloneUrl } from "./getRepositoryHTTPCloneUrl";
+import { default as makeGetRepositorySSHCloneUrl } from "./getRepositorySSHCloneUrl";
 
 const makeGetRepositoryExploreCollection: ServiceMethodFactory<
   RepositoryServiceDeps,

...
@@ -13,8 +13,8 @@ const makeGetRepositoryExploreCollection: ServiceMethodFactory<
   Promise<Repository[]>
 > = (deps) => {
   const { request } = deps;
-  const getHttpCloneUrl = makeGetHttpCloneUrl(deps);
-  const getSshCloneUrl = makeGetSshCloneUrl(deps);
+  const getRepositoryHTTPCloneUrl = makeGetRepositoryHTTPCloneUrl(deps);
+  const getRepositorySSHCloneUrl = makeGetRepositorySSHCloneUrl(deps);
   return async () => {
     const repositories = await request.prisma.repository.findMany({
       where: {

...
@@ -25,8 +25,8 @@ const makeGetRepositoryExploreCollection: ServiceMethodFactory<
     const repositoriesWithMetas = await Promise.all(
       repositories.map(async (repo) => ({
         ...repo,
-        httpCloneUrl: await getHttpCloneUrl(repo),
-        sshCloneUrl: await getSshCloneUrl(repo),
+        httpCloneUrl: await getRepositoryHTTPCloneUrl(repo),
+        sshCloneUrl: await getRepositorySSHCloneUrl(repo),
       }))
     );
 

app/services/repository/getHttpCloneUrl.ts -> app/services/repository/getRepositoryHTTPCloneUrl.ts
@@ -8,7 +8,7 @@ import { getEnv } from "../../utils/server";
 // service
 import type { RepositoryServiceDeps } from "./types";
 
-const makeGetHttpCloneUrl: ServiceMethodFactory<
+const makeGetRepositoryHTTPCloneUrl: ServiceMethodFactory<
   RepositoryServiceDeps,
   [Repository],
   Promise<string>

...
@@ -37,4 +37,4 @@ const makeGetHttpCloneUrl: ServiceMethodFactory<
   };
 };
 
-export default makeGetHttpCloneUrl;
+export default makeGetRepositoryHTTPCloneUrl;

app/services/repository/getSshCloneUrl.ts -> app/services/repository/getRepositorySSHCloneUrl.ts
@@ -7,7 +7,7 @@ import { Env } from "../../env";
 // service
 import type { RepositoryServiceDeps } from "./types";
 
-const makeGetSshCloneUrl: ServiceMethodFactory<
+const makeGetRepositorySSHCloneUrl: ServiceMethodFactory<
   RepositoryServiceDeps,
   [Repository],
   Promise<string>

...
@@ -30,4 +30,4 @@ const makeGetSshCloneUrl: ServiceMethodFactory<
   };
 };
 
-export default makeGetSshCloneUrl;
+export default makeGetRepositorySSHCloneUrl;

app/services/repository/index.ts
@@ -3,9 +3,9 @@ import { makeService } from "@ethicdevs/react-monolith";
 // app
 import type { RepositoryServiceAPI, RepositoryServiceDeps } from "./types";
 // service methods
-import { default as makeGetHttpCloneUrl } from "./getHttpCloneUrl";
+import { default as makeGetRepositoryHTTPCloneUrl } from "./getRepositoryHTTPCloneUrl";
 import { default as makeGetRepositoryExploreCollection } from "./getRepositoryExploreCollection";
-import { default as makeGetSshCloneUrl } from "./getSshCloneUrl";
+import { default as makeGetRepositorySSHCloneUrl } from "./getRepositorySSHCloneUrl";
 import { default as makeCreateRepository } from "./createRepository";
 
 export const makeRepositoryService = makeService<

...
@@ -13,7 +13,7 @@ export const makeRepositoryService = makeService<
   RepositoryServiceDeps
 >({
   createRepository: makeCreateRepository,
-  getHttpCloneUrl: makeGetHttpCloneUrl,
+  getRepositoryHTTPCloneUrl: makeGetRepositoryHTTPCloneUrl,
   getRepositoryExploreCollection: makeGetRepositoryExploreCollection,
-  getSshCloneUrl: makeGetSshCloneUrl,
+  getRepositorySSHCloneUrl: makeGetRepositorySSHCloneUrl,
 });

app/services/repository/types.ts
@@ -24,8 +24,8 @@ export interface CreateRepositoryDTO {
 export interface RepositoryServiceAPI extends ServiceApiContract {
   createRepository(dto: CreateRepositoryDTO): Promise<Repository>;
   getRepositoryExploreCollection(): Promise<Repository[]>;
-  getHttpCloneUrl(repository: Repository): Promise<string>;
-  getSshCloneUrl(repository: Repository): Promise<string>;
+  getRepositoryHTTPCloneUrl(repository: Repository): Promise<string>;
+  getRepositorySSHCloneUrl(repository: Repository): Promise<string>;
 }
 
 export interface RepositoryServiceDeps {

new file
app/services/user/getUserOrganizationMemberships.ts
@@ -0,0 +1,26 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { OrganizationMembership } from "@prisma/client";
+// app
+import type { UsersServiceDeps } from "./types";
+
+const getUserOrganizationMemberships: ServiceMethodFactory<
+  UsersServiceDeps,
+  [string],
+  Promise<OrganizationMembership[]>
+> = ({ request }) => {
+  return async (userId) => {
+    const orgMemberships = await request.prisma.organizationMembership.findMany(
+      {
+        where: {
+          userId,
+        },
+      }
+    );
+
+    return orgMemberships;
+  };
+};
+
+export default getUserOrganizationMemberships;

new file
app/services/user/getUserOrganizations.ts
@@ -0,0 +1,35 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { Organization } from "@prisma/client";
+// app
+import type { UsersServiceDeps } from "./types";
+
+const getUserOrganizations: ServiceMethodFactory<
+  UsersServiceDeps,
+  [string],
+  Promise<Organization[]>
+> = ({ request }) => {
+  return async (userId) => {
+    const userOrgs = await request.prisma.organization.findMany({
+      where: {
+        OR: [
+          {
+            ownerId: userId,
+          },
+          {
+            memberships: {
+              some: {
+                userId,
+              },
+            },
+          },
+        ],
+      },
+    });
+
+    return userOrgs;
+  };
+};
+
+export default getUserOrganizations;

app/services/user/index.ts
@@ -6,9 +6,13 @@ import type { UsersServiceAPI, UsersServiceDeps } from "./types";
 import { default as makeGetUserByEmailAddress } from "./getUserByEmailAddress";
 import { default as makeGetUserById } from "./getUserById";
 import { default as makeGetUserByUsername } from "./getUserByUsername";
+import { default as makeGetUserOrganizationMemberships } from "./getUserOrganizationMemberships";
+import { default as makeGetUserOrganizations } from "./getUserOrganizations";
 
 export const makeUsersService = makeService<UsersServiceAPI, UsersServiceDeps>({
   getUserByEmailAddress: makeGetUserByEmailAddress,
   getUserById: makeGetUserById,
   getUserByUsername: makeGetUserByUsername,
+  getUserOrganizationMemberships: makeGetUserOrganizationMemberships,
+  getUserOrganizations: makeGetUserOrganizations,
 });

app/services/user/types.ts
@@ -3,13 +3,17 @@ import { ServiceApiContract } from "@ethicdevs/react-monolith";
 // 3rd-party
 import { FastifyRequest } from "fastify";
 // generated via script[generate:prisma]
-import { User } from "@prisma/client";
+import { Organization, OrganizationMembership, User } from "@prisma/client";
 // app
 
 export interface UsersServiceAPI extends ServiceApiContract {
   getUserById(userId: string): Promise<User | null>;
   getUserByUsername(username: string): Promise<User | null>;
   getUserByEmailAddress(emailAddress: string): Promise<User | null>;
+  getUserOrganizationMemberships(
+    userId: string
+  ): Promise<OrganizationMembership[]>;
+  getUserOrganizations(userId: string): Promise<Organization[]>;
 }
 
 export interface UsersServiceDeps {