feat(dashboard,routes): add logged users dashboard (wip) + add routes prehandlers for security@@ -1,5 +1,5 @@
{
- "_generatedAtUnix": 1663370041613,
+ "_generatedAtUnix": 1663372097423,
"_hashAlgorithm": "sha1",
"_version": 2,
"islands": {
@@ -10,7 +10,7 @@
"pathSourceMap": "./public/.islands/InstantRouterIndicator.bundle.js.map"
},
"SideMenu": {
- "hash": "41d8972c9315c3cdeae9cf898b79d6cda1b6af62",
+ "hash": "5d01374da1cbee58e081b9022aa56f0624209e27",
"pathSource": "./app/islands/SideMenu.tsx",
"pathBundle": "./public/.islands/SideMenu.bundle.js",
"pathSourceMap": "./public/.islands/SideMenu.bundle.js.map"
@@ -25,6 +25,10 @@
"hash": "389861a6e7f9ff12026ddf5e91bdf4f06646116e",
"pathSource": "./app/views/InternalErrorView.tsx"
},
+ "DashboardView": {
+ "hash": "94d966b2d5954a66b0be59fda234c04e55c41d60",
+ "pathSource": "./app/views/auth/DashboardView.tsx"
+ },
"LoginView": {
"hash": "ca64e50d382088fd54ac2617a983d369c2fe8820",
"pathSource": "./app/views/auth/LoginView.tsx"
@@ -4,36 +4,31 @@ import React, { FC } from "react";
import styled, { css } from "styled-components";
// app
-import type { SectionsWithPages, WithThemeSchemeProp } from "../types";
+import type { CommonViewProps, WithThemeSchemeProp } from "../types";
import { NamedColors } from "../utils/style";
import InstantRouterIndicator from "../islands/InstantRouterIndicator";
import SideMenu from "../islands/SideMenu";
import { PageHeader } from "./PageHeader";
-interface LayoutProps {
- foo?: boolean;
- currentPageSlug?: string;
- currentSectionSlug?: string;
- menuDefinition?: SectionsWithPages;
- showSideExamples?: boolean;
+interface LayoutProps extends CommonViewProps {
showSideMenu?: boolean;
}
const BRANDLINE_HEIGHT = 4;
const HEADER_HEIGHT = 72;
const SIDE_MENU_WIDTH = 320;
-const SIDE_EXAMPLES_WIDTH = 490;
-
-export const Layout: FC<LayoutProps & WithThemeSchemeProp> = ({
- children,
- menuDefinition,
- currentPageSlug = undefined,
- currentSectionSlug = undefined,
- showSideExamples = false,
- showSideMenu = menuDefinition != null,
- themeScheme,
-}) => {
+
+export const Layout: FC<LayoutProps & WithThemeSchemeProp> = (commonProps) => {
+ const {
+ children,
+ menuDefinition,
+ currentPageSlug = undefined,
+ currentSectionSlug = undefined,
+ showSideMenu = menuDefinition != null,
+ themeScheme,
+ } = commonProps;
+
const sharedProps = {
themeScheme,
};
@@ -72,7 +67,7 @@ export const Layout: FC<LayoutProps & WithThemeSchemeProp> = ({
</div>
{/*<StyledBrandLine {...sharedProps} />*/}
<StyledPageHeaderWrapper {...sharedProps}>
- <PageHeader themeScheme={themeScheme} />
+ <PageHeader commonProps={commonProps} themeScheme={themeScheme} />
</StyledPageHeaderWrapper>
<StyledPageWrapper>
{menuDefinition && showSideMenu && (
@@ -81,6 +76,7 @@ export const Layout: FC<LayoutProps & WithThemeSchemeProp> = ({
{...sharedProps}
>
<SideMenu
+ commonProps={commonProps}
currentSectionSlug={currentSectionSlug}
currentPageSlug={currentPageSlug}
menuDefinition={menuDefinition}
@@ -91,13 +87,6 @@ export const Layout: FC<LayoutProps & WithThemeSchemeProp> = ({
<StyledChildrenWrapper {...sharedProps}>
{children}
</StyledChildrenWrapper>
- {showSideExamples && (
- <StyledSideExamplesWrapper {...sharedProps}>
- <div style={{ width: "100%" }}>
- <h3>Examples</h3>
- </div>
- </StyledSideExamplesWrapper>
- )}
</StyledPageWrapper>
{/*<StyledPageFooterWrapper>
<h4>Footer</h4>
@@ -218,24 +207,3 @@ const StyledChildrenWrapper = styled.div`
flex: 1;
width: 100%;
`;
-
-const StyledSideExamplesWrapper = styled.aside<WithThemeSchemeProp>`
- display: flex;
- flex-flow: column nowrap;
- justify-content: flex-start;
- align-items: flex-start;
-
- width: ${SIDE_EXAMPLES_WIDTH}px;
- min-height: calc(100% - ${BRANDLINE_HEIGHT + HEADER_HEIGHT}px);
-
- position: sticky;
- top: ${HEADER_HEIGHT}px;
-
- padding: 24px;
- gap: 24px;
-
- ${({ themeScheme }) => css`
- background-color: ${NamedColors.SIDE_EXAMPLES[themeScheme]};
- border-left: 1px solid ${NamedColors.BORDER_DEFAULT[themeScheme]};
- `};
-`;
@@ -1,21 +1,78 @@
// 3rd-party
-import React, { VFC } from "react";
+import React, { useMemo, VFC } from "react";
import styled, { css } from "styled-components";
// app
-import type { WithThemeSchemeProp } from "../types";
+import type { CommonProps, WithThemeSchemeProp } from "../types";
import { Const } from "../const";
import { NamedColors } from "../utils/style";
// import { ReactMonolithLogo } from "./icons";
-interface PageHeaderProps {}
+interface PageHeaderProps extends CommonProps {}
// const LOGO_HEIGHT = 36;
const SIDE_MENU_WIDTH = 320;
export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
+ commonProps,
themeScheme,
}) => {
const invertThemeScheme = themeScheme === "light" ? "dark" : "light";
+
+ const pageHeader = 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>
+ </>
+ );
+ }
+
+ 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>
+ </>
+ );
+ }, [commonProps.authenticated]);
+
return (
<StyledPageHeader themeScheme={themeScheme}>
<StyledLogoArea themeScheme={themeScheme}>
@@ -24,28 +81,7 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
<h1 style={{ margin: 0, marginLeft: 20 }}>{Const.APP_NAME}</h1>
</a>
</StyledLogoArea>
- <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>
+ {pageHeader}
</StyledPageHeader>
);
};
@@ -0,0 +1,26 @@
+// 3rd-party
+import type { ReqHandler } from "@ethicdevs/react-monolith";
+// app
+import { AppRoute } from "../../routes";
+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;
+ if (authenticated === false || curr_user_uid == null) {
+ reply.redirect(307, request.namedViewsPathMap[AppRoute.AUTH_LOGIN]);
+ return reply;
+ }
+
+ const usersService = makeUsersService({ request });
+ const currentUser = await usersService.getUserById(curr_user_uid);
+
+ const reqHandler = reply.makeRequestHandler(request, reply);
+ return reqHandler<DashboardViewProps>(DashboardView.name, {
+ currentUser,
+ });
+};
+
+export default getDashboardView;
@@ -1,9 +1,11 @@
+import { default as getDashboardView } from "./getDashboardView";
import { default as getLoginView } from "./getLoginView";
import { default as getRegisterView } from "./getRegisterView";
import { default as postLoginAction } from "./postLoginAction";
import { default as postRegisterAction } from "./postRegisterAction";
export const AuthController = {
+ getDashboardView,
getLoginView,
getRegisterView,
postLoginAction,
@@ -71,7 +71,7 @@ const postLoginView: ReqHandler = async (request, reply) => {
console.log(`Logged user with id: ${userId}`);
- reply.redirect(request.namedViewsPathMap[AppRoute.HOME]);
+ reply.redirect(302, request.namedViewsPathMap[AppRoute.USER_DASHBOARD]);
return reply;
};
@@ -84,7 +84,7 @@ const postRegisterView: ReqHandler = async (request, reply) => {
request.session.data.curr_user_uid = userId;
request.session.data.curr_user_username = username;
- reply.redirect(request.namedViewsPathMap[AppRoute.HOME]);
+ reply.redirect(302, request.namedViewsPathMap[AppRoute.USER_DASHBOARD]);
return reply;
};
@@ -3,12 +3,16 @@ import type { ReactIsland } from "@ethicdevs/react-monolith";
import React, { useCallback, useState } from "react";
import styled, { css } from "styled-components";
// app
-import type { SectionsWithPages, WithThemeSchemeProp } from "../types";
+import type {
+ CommonProps,
+ SectionsWithPages,
+ WithThemeSchemeProp,
+} from "../types";
import { MenuDivider } from "../components/MenuDivider";
import { MenuItem } from "../components/MenuItem";
import { NamedColors } from "../utils/style";
-interface SideMenuProps {
+interface SideMenuProps extends CommonProps {
foo?: boolean;
currentPageSlug?: string;
currentSectionSlug?: string;
@@ -4,6 +4,8 @@ import { AppRouter, AppRouterGroup, Router } from "@ethicdevs/react-monolith";
// 3rd-party
import { FastifySchema } from "fastify";
import React from "react";
+// app
+import { authenticatedOrLogin, guestOrRedirect } from "./utils/server";
// app controllers
import { AuthController } from "./controllers/auth";
import * as HomeController from "./controllers/HomeController";
@@ -16,6 +18,7 @@ export enum AppRoute {
AUTH_REGISTER_ACTION = "auth.register.action",
AUTH_LOGIN = "auth.login",
AUTH_LOGIN_ACTION = "auth.login.action",
+ USER_DASHBOARD = "user.dashboard",
}
export interface AppRoutesParams extends IRouteParams {
@@ -38,6 +41,7 @@ export interface AppRoutesParams extends IRouteParams {
password: string;
};
};
+ [AppRoute.USER_DASHBOARD]: undefined;
}
export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
@@ -80,18 +84,13 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
},
},
},
+ [AppRoute.USER_DASHBOARD]: undefined,
};
const RootAppRouter: AppRouter = () => (
<Router.Root>
<></>
<Router.Group type={AppRouterGroup.API}>
- <Router.Route
- name={AppRoute.HOME}
- method={"GET"}
- path={"/"}
- handler={HomeController.getHomeView}
- />
<Router.Route
name={AppRoute.SET_THEME}
method={"GET"}
@@ -99,10 +98,20 @@ const RootAppRouter: AppRouter = () => (
schema={AppRoutesSchemas[AppRoute.SET_THEME]}
handler={ThemeController.getTheme}
/>
+ {/* --- */}
+ <Router.Route
+ name={AppRoute.HOME}
+ method={"GET"}
+ path={"/"}
+ preHandler={guestOrRedirect("/dashboard")}
+ handler={HomeController.getHomeView}
+ />
+ {/* --- */}
<Router.Route
name={AppRoute.AUTH_REGISTER}
method={"GET"}
path={"/auth/register"}
+ preHandler={guestOrRedirect("/dashboard")}
handler={AuthController.getRegisterView}
/>
<Router.Route
@@ -110,12 +119,15 @@ const RootAppRouter: AppRouter = () => (
method={"POST"}
path={"/auth/register"}
schema={AppRoutesSchemas[AppRoute.AUTH_REGISTER_ACTION]}
+ preHandler={guestOrRedirect("/dashboard")}
handler={AuthController.postRegisterAction}
/>
+ {/* --- */}
<Router.Route
name={AppRoute.AUTH_LOGIN}
method={"GET"}
path={"/auth/login"}
+ preHandler={guestOrRedirect("/dashboard")}
handler={AuthController.getLoginView}
/>
<Router.Route
@@ -123,8 +135,17 @@ const RootAppRouter: AppRouter = () => (
method={"POST"}
path={"/auth/login"}
schema={AppRoutesSchemas[AppRoute.AUTH_LOGIN_ACTION]}
+ preHandler={guestOrRedirect("/dashboard")}
handler={AuthController.postLoginAction}
/>
+ {/* --- */}
+ <Router.Route
+ name={AppRoute.USER_DASHBOARD}
+ method={"GET"}
+ path={"/dashboard"}
+ preHandler={authenticatedOrLogin()}
+ handler={AuthController.getDashboardView}
+ />
</Router.Group>
</Router.Root>
);
@@ -0,0 +1,24 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { User } from "@prisma/client";
+// app
+import type { UsersServiceDeps } from "./types";
+
+const getUserByEmailAddress: ServiceMethodFactory<
+ UsersServiceDeps,
+ [string],
+ Promise<User | null>
+> = ({ request }) => {
+ return async (emailAddress) => {
+ const user = await request.prisma.user.findUnique({
+ where: {
+ email: emailAddress,
+ },
+ });
+
+ return user;
+ };
+};
+
+export default getUserByEmailAddress;
@@ -0,0 +1,24 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { User } from "@prisma/client";
+// app
+import type { UsersServiceDeps } from "./types";
+
+const getUserById: ServiceMethodFactory<
+ UsersServiceDeps,
+ [string],
+ Promise<User | null>
+> = ({ request }) => {
+ return async (userId) => {
+ const user = await request.prisma.user.findUnique({
+ where: {
+ id: userId,
+ },
+ });
+
+ return user;
+ };
+};
+
+export default getUserById;
@@ -0,0 +1,24 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { User } from "@prisma/client";
+// app
+import type { UsersServiceDeps } from "./types";
+
+const getUserByUsername: ServiceMethodFactory<
+ UsersServiceDeps,
+ [string],
+ Promise<User | null>
+> = ({ request }) => {
+ return async (username) => {
+ const user = await request.prisma.user.findUnique({
+ where: {
+ username,
+ },
+ });
+
+ return user;
+ };
+};
+
+export default getUserByUsername;
@@ -0,0 +1,14 @@
+// 1st-party
+import { makeService } from "@ethicdevs/react-monolith";
+// service
+import type { UsersServiceAPI, UsersServiceDeps } from "./types";
+// service methods
+import { default as makeGetUserByEmailAddress } from "./getUserByEmailAddress";
+import { default as makeGetUserById } from "./getUserById";
+import { default as makeGetUserByUsername } from "./getUserByUsername";
+
+export const makeUsersService = makeService<UsersServiceAPI, UsersServiceDeps>({
+ getUserByEmailAddress: makeGetUserByEmailAddress,
+ getUserById: makeGetUserById,
+ getUserByUsername: makeGetUserByUsername,
+});
@@ -0,0 +1,17 @@
+// 1st-party
+import { ServiceApiContract } from "@ethicdevs/react-monolith";
+// 3rd-party
+import { FastifyRequest } from "fastify";
+// generated via script[generate:prisma]
+import { 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>;
+}
+
+export interface UsersServiceDeps {
+ request: FastifyRequest;
+}
@@ -70,6 +70,7 @@ export type SectionsWithPages = {
};
export interface CommonViewProps {
+ authenticated: boolean;
currentSectionSlug: string | undefined;
currentPageSlug: string | undefined;
menuDefinition: SectionsWithPages;
@@ -0,0 +1,10 @@
+import type { preHandlerHookHandler } from "fastify";
+// app
+import { AppRoute } from "../../routes";
+
+export const authenticatedOrLogin =
+ (): preHandlerHookHandler => async (request, reply) => {
+ if (request.session.data.authenticated === false) {
+ reply.redirect(302, request.namedViewsPathMap[AppRoute.AUTH_LOGIN]);
+ }
+ };
@@ -0,0 +1,9 @@
+import type { preHandlerHookHandler } from "fastify";
+
+export const authenticatedOrRedirect =
+ (redirectToPath: string, statusCode: number = 302): preHandlerHookHandler =>
+ async (request, reply) => {
+ if (request.session.data.authenticated === false) {
+ reply.redirect(statusCode, redirectToPath);
+ }
+ };
@@ -0,0 +1,9 @@
+import type { preHandlerHookHandler } from "fastify";
+
+export const guestOrRedirect =
+ (redirectToPath: string, statusCode: number = 302): preHandlerHookHandler =>
+ async (request, reply) => {
+ if (request.session.data.authenticated) {
+ reply.redirect(statusCode, redirectToPath);
+ }
+ };
@@ -1,7 +1,10 @@
// All exports in this file must not be called from the client-side.
+export { authenticatedOrLogin } from "./authenticatedOrLogin";
+export { authenticatedOrRedirect } from "./authenticatedOrRedirect";
export { getDocFileContent } from "./getDocFileContent";
export { getEnv } from "./getEnv";
+export { guestOrRedirect } from "./guestOrRedirect";
export { localAppDomainPreHandler } from "./localAppDomainPreHandler";
export { makeRequestHandler } from "./makeRequestHandler";
export { sessionSetupPreHandler } from "./sessionSetupPreHandler";
@@ -21,6 +21,7 @@ export const makeRequestHandler = {
const viewProps: T & { commonProps: CommonViewProps } = {
...props,
commonProps: {
+ authenticated: request.session.data.authenticated,
title: props?.title,
currentSectionSlug: sectionSlug,
currentPageSlug: pageSlug,
@@ -0,0 +1,26 @@
+// 1st-party
+import type { ReactView } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React from "react";
+// generated via script[generate:prisma]
+import type { User } from "@prisma/client";
+// app
+import type { CommonProps } from "../../types";
+import { Layout, PageWrapper } from "../../components";
+
+export interface DashboardViewProps extends CommonProps {
+ currentUser: User;
+}
+
+const DashboardView: ReactView<DashboardViewProps> = ({ commonProps }) => {
+ return (
+ <Layout {...commonProps} showSideMenu={false}>
+ <PageWrapper>
+ <h1>Hey, welcome!</h1>
+ </PageWrapper>
+ </Layout>
+ );
+};
+
+DashboardView.displayName = "DashboardView";
+export default DashboardView;