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@@ -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"
}
}
@@ -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");
});
@@ -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;
};
};
@@ -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,
};
};
};
@@ -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")
- ),
- },
- },
-};
@@ -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;
+}
@@ -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`,
@@ -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,
@@ -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>;
@@ -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>
))}