feat(explore): add public repositories explore view@@ -1,5 +1,5 @@
{
- "_generatedAtUnix": 1663372097423,
+ "_generatedAtUnix": 1663449267544,
"_hashAlgorithm": "sha1",
"_version": 2,
"islands": {
@@ -36,6 +36,10 @@
"RegisterView": {
"hash": "3e2c7053b529624ed6cfa9ea8631b4480bd9775b",
"pathSource": "./app/views/auth/RegisterView.tsx"
+ },
+ "RepositoryExploreView": {
+ "hash": "d776d1d9f5a8559427f64fe13e14886ac3afe526",
+ "pathSource": "./app/views/repository/RepositoryExploreView.tsx"
}
}
}
@@ -18,57 +18,23 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
}) => {
const invertThemeScheme = themeScheme === "light" ? "dark" : "light";
- const pageHeader = useMemo(() => {
+ const pageHeaderActions = useMemo(() => {
if (commonProps.authenticated) {
return (
- <>
- <StyledPageHeaderNav>
- <a aria-label={"Explore Projects"} href={"/explore/projects"}>
- Explore Projects
- </a>
- </StyledPageHeaderNav>
- <StyledActionsArea>
- <a
- href={`/theme/${invertThemeScheme}`}
- style={{ color: NamedColors.TEXT_MUTED[themeScheme] }}
- title={`Click to enable ${
- themeScheme === "light" ? "dark" : "light"
- } mode`}
- >
- {`${themeScheme === "light" ? "Dark" : "Light"} mode`}
- </a>
- <a aria-label={"Log off your account"} href={"/auth/logout"}>
- Logout
- </a>
- </StyledActionsArea>
- </>
+ <a aria-label={"Log off your account"} href={"/auth/logout"}>
+ Logout
+ </a>
);
}
return (
<>
- <StyledPageHeaderNav>
- <a aria-label={"Explore Projects"} href={"/explore/projects"}>
- Explore Projects
- </a>
- </StyledPageHeaderNav>
- <StyledActionsArea>
- <a
- href={`/theme/${invertThemeScheme}`}
- style={{ color: NamedColors.TEXT_MUTED[themeScheme] }}
- title={`Click to enable ${
- themeScheme === "light" ? "dark" : "light"
- } mode`}
- >
- {`${themeScheme === "light" ? "Dark" : "Light"} mode`}
- </a>
- <a aria-label={"Register a new account"} href={"/auth/register"}>
- Register
- </a>
- <a aria-label={"Login to your account"} href={"/auth/login"}>
- Login
- </a>
- </StyledActionsArea>
+ <a aria-label={"Register a new account"} href={"/auth/register"}>
+ Register
+ </a>
+ <a aria-label={"Login to your account"} href={"/auth/login"}>
+ Login
+ </a>
</>
);
}, [commonProps.authenticated]);
@@ -81,7 +47,23 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
<h1 style={{ margin: 0, marginLeft: 20 }}>{Const.APP_NAME}</h1>
</a>
</StyledLogoArea>
- {pageHeader}
+ <StyledPageHeaderNav>
+ <a aria-label={"Explore Repositories"} href={"/repositories/explore"}>
+ Explore Repositories
+ </a>
+ </StyledPageHeaderNav>
+ <StyledActionsArea>
+ <a
+ href={`/theme/${invertThemeScheme}`}
+ style={{ color: NamedColors.TEXT_MUTED[themeScheme] }}
+ title={`Click to enable ${
+ themeScheme === "light" ? "dark" : "light"
+ } mode`}
+ >
+ {`${themeScheme === "light" ? "Dark" : "Light"} mode`}
+ </a>
+ {pageHeaderActions}
+ </StyledActionsArea>
</StyledPageHeader>
);
};
@@ -140,11 +122,6 @@ const StyledPageHeaderNav = styled.nav`
gap: 24px;
margin-left: 4px;
-
- a {
- font-size: 20px;
- line-height: 24px;
- }
`;
const StyledActionsArea = styled.div`
@@ -154,4 +131,8 @@ const StyledActionsArea = styled.div`
align-items: center;
padding-right: 12px;
+
+ & > a {
+ margin-left: 12px;
+ }
`;
@@ -2,10 +2,11 @@
import type { ReqHandler } from "@ethicdevs/react-monolith";
// app
import { AppRoute } from "../../routes";
+import { makeUsersService } from "../../services/user";
+// app views
import DashboardView, {
DashboardViewProps,
} from "../../views/auth/DashboardView";
-import { makeUsersService } from "../../services/users";
const getDashboardView: ReqHandler = async (request, reply) => {
const { authenticated, curr_user_uid } = request.session.data;
@@ -0,0 +1,20 @@
+// 1st-party
+import { ReqHandler } from "@ethicdevs/react-monolith";
+// app
+import { makeRepositoryService } from "../../services/repository";
+// app views
+import RepositoryExploreView, {
+ RepositoryExploreViewProps,
+} from "../../views/repository/RepositoryExploreView";
+
+const getRepositoryExploreView: ReqHandler = async (request, reply) => {
+ const repoService = makeRepositoryService({ request });
+ const repositories = await repoService.getRepositoryExploreCollection();
+
+ const reqHandler = reply.makeRequestHandler(request, reply);
+ return reqHandler<RepositoryExploreViewProps>(RepositoryExploreView.name, {
+ repositories,
+ });
+};
+
+export default getRepositoryExploreView;
@@ -0,0 +1,5 @@
+import { default as getRepositoryExploreView } from "./getRepositoryExploreView";
+
+export const RepositoryController = {
+ getRepositoryExploreView,
+};
@@ -60,6 +60,20 @@ const getNodeEnv = (
return defaultEnv;
};
+const getHostname = (hostname?: string | null): string => {
+ if (hostname == null || hostname === "fake") {
+ return "localhost";
+ }
+ return String(hostname);
+};
+
+const getPort = (port?: string | null): number => {
+ if (port == null || port === "fake") {
+ return 4100;
+ }
+ return Number(port);
+};
+
const getCookieName = (cookieName?: string | null): string => {
if (cookieName == null || cookieName === "fake") {
throw new Error("[env] COOKIE_NAME is missing.");
@@ -116,6 +130,8 @@ const getGitRepositoriesRoot = (
interface IEnv {
NODE_ENV: NodeEnv;
+ PORT: number;
+ HOST: string;
COOKIE_NAME: string;
COOKIE_SECRET: string;
DATABASE_URL: string;
@@ -126,11 +142,17 @@ interface IEnv {
export const Env: IEnv = {
NODE_ENV: getNodeEnv(process.env.NODE_ENV),
+ PORT: getPort(process.env.PORT),
+ HOST: getHostname(process.env.HOST),
+ // ---
COOKIE_NAME: getCookieName(process.env.COOKIE_NAME),
COOKIE_SECRET: getCookieSecret(process.env.COOKIE_SECRET),
+ // --
DATABASE_URL: getDatabaseUrl(process.env.DATABASE_URL),
+ // ---
DEPLOYMENT_DOMAIN: getDeploymentDomain(process.env.DEPLOYMENT_DOMAIN),
DEPLOYMENT_SCHEME: getDeploymentScheme(process.env.DEPLOYMENT_SCHEME),
+ // ---
GIT_REPOSITORIES_ROOT: getGitRepositoriesRoot(
process.env.GIT_REPOSITORIES_ROOT
),
@@ -8,6 +8,7 @@ import React from "react";
import { authenticatedOrLogin, guestOrRedirect } from "./utils/server";
// app controllers
import { AuthController } from "./controllers/auth";
+import { RepositoryController } from "./controllers/repository";
import * as HomeController from "./controllers/HomeController";
import * as ThemeController from "./controllers/ThemeController";
@@ -19,6 +20,7 @@ export enum AppRoute {
AUTH_LOGIN = "auth.login",
AUTH_LOGIN_ACTION = "auth.login.action",
USER_DASHBOARD = "user.dashboard",
+ REPOSITORY_EXPLORE = "repository.explore",
}
export interface AppRoutesParams extends IRouteParams {
@@ -42,6 +44,7 @@ export interface AppRoutesParams extends IRouteParams {
};
};
[AppRoute.USER_DASHBOARD]: undefined;
+ [AppRoute.REPOSITORY_EXPLORE]: undefined;
}
export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
@@ -85,6 +88,7 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
},
},
[AppRoute.USER_DASHBOARD]: undefined,
+ [AppRoute.REPOSITORY_EXPLORE]: undefined,
};
const RootAppRouter: AppRouter = () => {
@@ -148,6 +152,14 @@ const RootAppRouter: AppRouter = () => {
preHandler={authenticatedOrLogin()}
handler={AuthController.getDashboardView}
/>
+ {/* --- */}
+ <Router.Route
+ name={AppRoute.REPOSITORY_EXPLORE}
+ method={"GET"}
+ path={"/repositories/explore"}
+ preHandler={authenticatedOrLogin()}
+ handler={RepositoryController.getRepositoryExploreView}
+ />
</Router.Group>
</Router.Root>
);
@@ -15,7 +15,7 @@ import fastifyFormBody from "@fastify/formbody";
import fastifyGitServer from "@ethicdevs/fastify-git-server";
import fastifyServeStatic from "fastify-static";
// generated via script[generate:prisma]
-import { PrismaClient } from "@prisma/client";
+import { GlobalRole, PrismaClient } from "@prisma/client";
// app root
import * as Paths from "../paths";
import { version as appVersion } from "../package.json";
@@ -30,9 +30,6 @@ import {
makeRequestHandler,
} from "./utils/server";
-const HOST = process.env.HOST || "localhost";
-const PORT = process.env.PORT || 4100;
-
let server: null | AppServer = null;
async function main(): Promise<AppServer> {
@@ -52,7 +49,7 @@ async function main(): Promise<AppServer> {
signed: true,
};
- server = await makeAppServer(HOST, PORT, {
+ server = await makeAppServer(Env.HOST, Env.PORT, {
appName: Const.APP_NAME,
appVersion,
env,
@@ -141,7 +138,7 @@ async function main(): Promise<AppServer> {
curr_user_avatar_uri: null,
curr_user_uid: null,
curr_user_username: null,
- curr_user_role: "GUEST",
+ curr_user_role: GlobalRole.GUEST,
flash_message: null,
flash_message_shown_once: false,
two_factor_lock: false,
@@ -8,6 +8,15 @@ export const ORGS_REPOS: Record<
{ 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",
@@ -0,0 +1,40 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import type { Repository } from "@prisma/client";
+// app
+import { Env } from "../../env";
+import { getEnv } from "../../utils/server";
+// service
+import type { RepositoryServiceDeps } from "./types";
+
+const makeGetHttpCloneUrl: ServiceMethodFactory<
+ RepositoryServiceDeps,
+ [Repository],
+ Promise<string>
+> = ({ request }) => {
+ const env = getEnv();
+ return async (repo) => {
+ const parentOrg = await request.prisma.organization.findUnique({
+ where: {
+ id: repo.organizationId,
+ },
+ });
+
+ if (parentOrg == null) {
+ throw new Error(
+ `Could not find the parent organization for project "${repo.slug}".`
+ );
+ }
+
+ const authCredentials = `${parentOrg.slug}:secret`;
+ const baseUrl =
+ env === "development"
+ ? `${Env.DEPLOYMENT_SCHEME}://${authCredentials}@${Env.DEPLOYMENT_DOMAIN}:${Env.PORT}`
+ : `${Env.DEPLOYMENT_SCHEME}://${authCredentials}@${Env.DEPLOYMENT_DOMAIN}`;
+
+ return `${baseUrl}/${parentOrg.slug}/${repo.slug}.git`;
+ };
+};
+
+export default makeGetHttpCloneUrl;
@@ -0,0 +1,37 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { Repository, ResourceVisibility } from "@prisma/client";
+// service
+import type { RepositoryServiceDeps } from "./types";
+import { default as makeGetHttpCloneUrl } from "./getHttpCloneUrl";
+import { default as makeGetSshCloneUrl } from "./getSshCloneUrl";
+
+const makeGetRepositoryExploreCollection: ServiceMethodFactory<
+ RepositoryServiceDeps,
+ void[],
+ Promise<Repository[]>
+> = (deps) => {
+ const { request } = deps;
+ const getHttpCloneUrl = makeGetHttpCloneUrl(deps);
+ const getSshCloneUrl = makeGetSshCloneUrl(deps);
+ return async () => {
+ const repositories = await request.prisma.repository.findMany({
+ where: {
+ visibility: ResourceVisibility.PUBLIC,
+ },
+ });
+
+ const repositoriesWithMetas = await Promise.all(
+ repositories.map(async (repo) => ({
+ ...repo,
+ httpCloneUrl: await getHttpCloneUrl(repo),
+ sshCloneUrl: await getSshCloneUrl(repo),
+ }))
+ );
+
+ return repositoriesWithMetas;
+ };
+};
+
+export default makeGetRepositoryExploreCollection;
@@ -0,0 +1,33 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import type { Repository } from "@prisma/client";
+// app
+import { Env } from "../../env";
+// service
+import type { RepositoryServiceDeps } from "./types";
+
+const makeGetSshCloneUrl: ServiceMethodFactory<
+ RepositoryServiceDeps,
+ [Repository],
+ Promise<string>
+> = ({ request }) => {
+ return async (repo) => {
+ const parentOrg = await request.prisma.organization.findUnique({
+ where: {
+ id: repo.organizationId,
+ },
+ });
+
+ if (parentOrg == null) {
+ throw new Error(
+ `Could not find the parent organization for project "${repo.slug}".`
+ );
+ }
+
+ const baseUrl = `ssh://git@${Env.DEPLOYMENT_DOMAIN}`;
+ return `${baseUrl}/${parentOrg.slug}/${repo.slug}.git`;
+ };
+};
+
+export default makeGetSshCloneUrl;
@@ -0,0 +1,17 @@
+// 1st-party
+import { makeService } from "@ethicdevs/react-monolith";
+// app
+import type { RepositoryServiceAPI, RepositoryServiceDeps } from "./types";
+// service methods
+import { default as makeGetHttpCloneUrl } from "./getHttpCloneUrl";
+import { default as makeGetRepositoryExploreCollection } from "./getRepositoryExploreCollection";
+import { default as makeGetSshCloneUrl } from "./getSshCloneUrl";
+
+export const makeRepositoryService = makeService<
+ RepositoryServiceAPI,
+ RepositoryServiceDeps
+>({
+ getHttpCloneUrl: makeGetHttpCloneUrl,
+ getRepositoryExploreCollection: makeGetRepositoryExploreCollection,
+ getSshCloneUrl: makeGetSshCloneUrl,
+});
@@ -0,0 +1,16 @@
+// 1st-party
+import type { ServiceApiContract } from "@ethicdevs/react-monolith";
+// 3rd-party
+import type { FastifyRequest } from "fastify";
+// generated via script[generate:prisma]
+import type { Repository } from "@prisma/client";
+
+export interface RepositoryServiceAPI extends ServiceApiContract {
+ getRepositoryExploreCollection(): Promise<Repository[]>;
+ getHttpCloneUrl(): Promise<string>;
+ getSshCloneUrl(): Promise<string>;
+}
+
+export interface RepositoryServiceDeps {
+ request: FastifyRequest;
+}
@@ -0,0 +1,36 @@
+// 1st-party
+import type { ReactView } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React from "react";
+// generated via script[generate:prisma]
+import type { Repository } from "@prisma/client";
+// app
+import type { CommonProps } from "../../types";
+import { Layout, PageWrapper } from "../../components";
+
+export interface RepositoryExploreViewProps extends CommonProps {
+ repositories: Repository[];
+}
+
+const RepositoryExploreView: ReactView<RepositoryExploreViewProps> = ({
+ commonProps,
+ repositories,
+}) => {
+ return (
+ <Layout {...commonProps} showSideMenu={false}>
+ <PageWrapper>
+ {repositories.map((repo) => (
+ <div key={repo.id}>
+ <h2>{repo.displayName}</h2>
+ <code>
+ <pre>{JSON.stringify(repo, null, 2)}</pre>
+ </code>
+ </div>
+ ))}
+ </PageWrapper>
+ </Layout>
+ );
+};
+
+RepositoryExploreView.displayName = "RepositoryExploreView";
+export default RepositoryExploreView;
@@ -0,0 +1,8 @@
+-- CreateEnum
+CREATE TYPE "ResourceVisibility" AS ENUM ('PUBLIC', 'UNLISTED', 'PRIVATE');
+
+-- AlterTable
+ALTER TABLE "Organization" ADD COLUMN "visibility" "ResourceVisibility" NOT NULL DEFAULT E'PRIVATE';
+
+-- AlterTable
+ALTER TABLE "Repository" ADD COLUMN "visibility" "ResourceVisibility" NOT NULL DEFAULT E'PRIVATE';
@@ -17,6 +17,12 @@ enum GlobalRole {
SUPER_ADMIN
}
+enum ResourceVisibility {
+ PUBLIC
+ UNLISTED
+ PRIVATE
+}
+
// models
model Organization {
@@ -28,6 +34,7 @@ model Organization {
avatarUri String?
displayName String?
+ visibility ResourceVisibility @default(PRIVATE)
websiteUrl String?
repositories Repository[] @relation("ManyRepositoriesToOneOrganization")
@@ -47,6 +54,7 @@ model Repository {
displayName String?
keywords String[]
shortDescription String?
+ visibility ResourceVisibility @default(PRIVATE)
websiteUrl String?
organization Organization @relation("ManyRepositoriesToOneOrganization", fields: [organizationId], references: [id])