refactor: add user service methods to get user organizations/memberships + cleanup the services methods & their names@@ -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"
@@ -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;
@@ -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,
@@ -49,7 +49,7 @@ const postLoginView: ReqHandler = async (request, reply) => {
});
}
- const [isLoginAllowed, user] = await authService.shouldAllowUserLogin(
+ const [isLoginAllowed, user] = await authService.isUserLoginAllowed(
emailAddress,
password
);
@@ -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
+ ),
});
};
@@ -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>
@@ -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;
};
};
@@ -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,
});
@@ -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;
@@ -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 {
@@ -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),
}))
);
@@ -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;
@@ -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;
@@ -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,
});
@@ -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 {
@@ -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;
@@ -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;
@@ -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,
});
@@ -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 {