feat(repository): make it possible to create a repository through the UI@@ -1,51 +1,17 @@
// 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);
return reqHandler<RepositoryExploreViewProps>(RepositoryExploreView.name, {
- repositories,
+ repositories: await repoService.getRepositoryExploreCollection(),
});
};
@@ -1,7 +1,9 @@
import { default as getRepositoryCreateView } from "./getRepositoryCreateView";
import { default as getRepositoryExploreView } from "./getRepositoryExploreView";
+import { default as postRepositoryCreateAction } from "./postRepositoryCreateAction";
export const RepositoryController = {
getRepositoryCreateView,
getRepositoryExploreView,
+ postRepositoryCreateAction,
};
@@ -0,0 +1,83 @@
+// std
+import { join, resolve } from "node:path";
+// 1st-party
+import type { ReqHandler } from "@ethicdevs/react-monolith";
+// app
+import { AppRoute, AppRoutesParams } from "../../routes";
+import { Env } from "../../env";
+// app services
+import { makeRepositoryService } from "../../services/repository";
+import { makeUsersService } from "../../services/user";
+// app views
+import RepositoryCreateView, {
+ RepositoryCreateViewProps,
+} from "../../views/repository/RepositoryCreateView";
+
+const getRepositoryCreateView: ReqHandler = async (request, reply) => {
+ 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 repoService = makeRepositoryService({ request });
+ const usersService = makeUsersService({ request });
+
+ const { body, validationError } = request;
+
+ if (validationError != null) {
+ const {
+ message: errorMessage,
+ validation,
+ validationContext,
+ } = validationError;
+ console.log("validation:", validation);
+ console.log("validationContext:", validationContext);
+ return reqHandler<RepositoryCreateViewProps>(RepositoryCreateView.name, {
+ errorMessage,
+ availableParentOrgs: await usersService.getUserOrganizations(
+ request.session.data.curr_user_uid
+ ),
+ });
+ }
+
+ const {
+ parent_org_slug: parentOrgSlug,
+ repo_slug: repoSlug,
+ repo_display_name: displayName,
+ repo_init_license_file: withLicense,
+ repo_init_license_kind: withLicenseKind,
+ repo_init_readme_file: withBaseReadmeFile,
+ repo_keywords: keywords,
+ repo_short_description: shortDescription,
+ repo_visibility: visibility,
+ repo_website_url: websiteUrl,
+ } = body as AppRoutesParams[AppRoute.REPOSITORY_CREATE_ACTION]["body"];
+
+ const newRepo = await repoService.createRepository({
+ parentOrgSlug, // TODO: Validate it exists first.
+ repoSlug, // TODO: Validate it is not already taken first.
+ repoData: {
+ avatarUri: null,
+ displayName,
+ keywords: keywords.split(","),
+ shortDescription,
+ visibility,
+ websiteUrl, // TODO: Ensure it resolves/has a TXT record in DNS pointing to us.
+ },
+ repoInitFlags: {
+ orgRepositoriesDir: resolve(join(Env.GIT_REPOSITORIES_ROOT, "ethicdevs")),
+ withBaseReadmeFile: withBaseReadmeFile === "on",
+ withLicense: withLicense === "on",
+ withLicenseKind,
+ },
+ });
+
+ reply.redirect(302, `/${parentOrgSlug}/${newRepo.slug}`);
+ return reply;
+};
+
+export default getRepositoryCreateView;
@@ -2,8 +2,10 @@
import type { IRouteParams } from "@ethicdevs/react-monolith";
import { AppRouter, AppRouterGroup, Router } from "@ethicdevs/react-monolith";
// 3rd-party
-import { FastifySchema } from "fastify";
+import type { FastifySchema } from "fastify";
import React from "react";
+// generated via script[generate:prisma]
+import { ResourceVisibility } from "@prisma/client";
// app
import { authenticatedOrLogin, guestOrRedirect } from "./utils/server";
// app controllers
@@ -22,6 +24,8 @@ export enum AppRoute {
AUTH_LOGOUT_ACTION = "auth.logout.action",
USER_DASHBOARD = "user.dashboard",
REPOSITORY_EXPLORE = "repository.explore",
+ REPOSITORY_CREATE = "repository.create",
+ REPOSITORY_CREATE_ACTION = "repository.create.action",
}
export interface AppRoutesParams extends IRouteParams {
@@ -47,6 +51,22 @@ export interface AppRoutesParams extends IRouteParams {
[AppRoute.AUTH_LOGOUT_ACTION]: undefined;
[AppRoute.USER_DASHBOARD]: undefined;
[AppRoute.REPOSITORY_EXPLORE]: undefined;
+ [AppRoute.REPOSITORY_CREATE]: undefined;
+ [AppRoute.REPOSITORY_CREATE_ACTION]: {
+ body: {
+ parent_org_slug: string;
+ repo_display_name: string;
+ repo_init_license_file: "on" | "off";
+ repo_init_license_kind: string;
+ repo_init_readme_file: "on" | "off";
+ repo_keywords: string;
+ repo_keywords_add: string;
+ repo_short_description: string;
+ repo_slug: string;
+ repo_visibility: ResourceVisibility;
+ repo_website_url: string;
+ };
+ };
}
export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
@@ -92,6 +112,71 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
[AppRoute.AUTH_LOGOUT_ACTION]: undefined,
[AppRoute.USER_DASHBOARD]: undefined,
[AppRoute.REPOSITORY_EXPLORE]: undefined,
+ [AppRoute.REPOSITORY_CREATE]: undefined,
+ [AppRoute.REPOSITORY_CREATE_ACTION]: {
+ body: {
+ type: "object",
+ required: [
+ "parent_org_slug",
+ "repo_display_name",
+ "repo_init_license_file",
+ "repo_init_license_kind",
+ "repo_init_readme_file",
+ "repo_keywords",
+ "repo_keywords_add",
+ "repo_short_description",
+ "repo_slug",
+ "repo_visibility",
+ "repo_website_url",
+ ],
+ additionalProperties: false,
+ properties: {
+ parent_org_slug: {
+ type: "string",
+ },
+ repo_display_name: {
+ type: "string",
+ minLength: 3,
+ maxLength: 64,
+ },
+ repo_init_license_file: {
+ type: "string",
+ enum: ["on", "off"],
+ },
+ repo_init_license_kind: {
+ type: "string",
+ },
+ repo_init_readme_file: {
+ type: "string",
+ enum: ["on", "off"],
+ },
+ repo_keywords: {
+ type: "string",
+ },
+ repo_keywords_add: {
+ type: "string",
+ },
+ repo_short_description: {
+ type: "string",
+ minLength: 10,
+ maxLength: 140,
+ },
+ repo_slug: {
+ type: "string",
+ minLength: 3,
+ maxLength: 64,
+ },
+ repo_visibility: {
+ type: "string",
+ enum: Object.values(ResourceVisibility),
+ },
+ repo_website_url: {
+ type: "string",
+ format: "uri",
+ },
+ },
+ },
+ },
};
const RootAppRouter: AppRouter = () => {
@@ -128,8 +213,8 @@ const RootAppRouter: AppRouter = () => {
name={AppRoute.AUTH_REGISTER_ACTION}
method={"POST"}
path={"/auth/register"}
- schema={AppRoutesSchemas[AppRoute.AUTH_REGISTER_ACTION]}
preHandler={guestOrDashboardRedirect}
+ schema={AppRoutesSchemas[AppRoute.AUTH_REGISTER_ACTION]}
handler={AuthController.postRegisterAction}
/>
{/* --- */}
@@ -144,16 +229,16 @@ const RootAppRouter: AppRouter = () => {
name={AppRoute.AUTH_LOGIN_ACTION}
method={"POST"}
path={"/auth/login"}
- schema={AppRoutesSchemas[AppRoute.AUTH_LOGIN_ACTION]}
preHandler={guestOrDashboardRedirect}
+ schema={AppRoutesSchemas[AppRoute.AUTH_LOGIN_ACTION]}
handler={AuthController.postLoginAction}
/>
<Router.Route
name={AppRoute.AUTH_LOGOUT_ACTION}
method={"GET"}
path={"/auth/logout"}
- schema={AppRoutesSchemas[AppRoute.AUTH_LOGOUT_ACTION]}
preHandler={loggedOrLoginRedirect}
+ schema={AppRoutesSchemas[AppRoute.AUTH_LOGOUT_ACTION]}
handler={AuthController.getLogoutAction}
/>
{/* --- */}
@@ -172,12 +257,20 @@ const RootAppRouter: AppRouter = () => {
handler={RepositoryController.getRepositoryExploreView}
/>
<Router.Route
- name={AppRoute.REPOSITORY_EXPLORE}
+ name={AppRoute.REPOSITORY_CREATE}
method={"GET"}
path={"/repo/new"}
preHandler={loggedOrLoginRedirect}
handler={RepositoryController.getRepositoryCreateView}
/>
+ <Router.Route
+ name={AppRoute.REPOSITORY_CREATE_ACTION}
+ method={"POST"}
+ path={"/repo/new"}
+ preHandler={loggedOrLoginRedirect}
+ schema={AppRoutesSchemas[AppRoute.REPOSITORY_CREATE_ACTION]}
+ handler={RepositoryController.postRepositoryCreateAction}
+ />
</Router.Group>
</Router.Root>
);
@@ -16,7 +16,8 @@ const makeCreateRepository: ServiceMethodFactory<
const {
orgRepositoriesDir,
withBaseReadmeFile: _,
- withLicenseFile: __,
+ withLicense: __,
+ withLicenseKind: ___,
} = repoInitFlags;
const parentOrg = await request.prisma.organization.findUnique({
@@ -57,13 +58,11 @@ const makeCreateRepository: ServiceMethodFactory<
["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.stdout.on("data", (data) => buffer.push(data));
gitInitBareRepoProcess.stderr.on("data", (data) => {
reject(new Error(Buffer.from(data).toString("utf-8")));
});
@@ -17,7 +17,8 @@ export interface CreateRepositoryDTO {
repoInitFlags: {
orgRepositoriesDir: PathLike;
withBaseReadmeFile: boolean;
- withLicenseFile: string;
+ withLicense: boolean;
+ withLicenseKind: string;
};
}
@@ -0,0 +1,8 @@
+export default function getFormEntries<
+ R extends Record<string, unknown> = Record<string, unknown>
+>(form: HTMLFormElement): R {
+ return Object.values(form).reduce((obj, field) => {
+ obj[field.name] = field.value;
+ return obj;
+ }, {} as R);
+}
@@ -1 +1,2 @@
+export { default as getFormEntries } from "./getFormEntries";
export { default as slugify } from "./slugify";