feat(auth,crypto): add crypto service so its reusable
+ 153
- 44
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1663360458599,
+  "_generatedAtUnix": 1663362544680,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

...
@@ -26,7 +26,7 @@
       "pathSource": "./app/views/InternalErrorView.tsx"
     },
     "RegisterView": {
-      "hash": "43766073188abc2654160639989e88f1a6d9e0e0",
+      "hash": "bd965e7ad0c08c28cd6544cc57bca52567db19d1",
       "pathSource": "./app/views/auth/RegisterView.tsx"
     }
   }

app/controllers/auth/postRegisterAction.ts
@@ -6,7 +6,11 @@ import RegisterView, { RegisterViewProps } from "../../views/auth/RegisterView";
 import { makeAuthService } from "../../services/auth";
 
 const postRegisterView: ReqHandler = async (request, reply) => {
-  const authService = makeAuthService({ request });
+  const authService = makeAuthService({
+    cryptoService: request.cryptoService,
+    request,
+  });
+
   const reqHandler = reply.makeRequestHandler(request, reply);
 
   const {

...
@@ -18,14 +22,7 @@ const postRegisterView: ReqHandler = async (request, reply) => {
   const initialValues = { emailAddress, username };
 
   if (request.validationError != null) {
-    const {
-      message: errorMessage,
-      validation,
-      validationContext,
-    } = request.validationError;
-
-    console.log("validation:", validation);
-    console.log("validationContext:", validationContext);
+    const { message: errorMessage } = request.validationError;
 
     return reqHandler<RegisterViewProps>(RegisterView.name, {
       errorMessage,

...
@@ -33,6 +30,14 @@ const postRegisterView: ReqHandler = async (request, reply) => {
     });
   }
 
+  if (await authService.isExistingEmailAddress(emailAddress)) {
+    return reqHandler<RegisterViewProps>(RegisterView.name, {
+      errorMessage:
+        "This is email address is already used. Please use another.",
+      initialValues: { emailAddress, username },
+    });
+  }
+
   if (await authService.isExistingUsername(username)) {
     return reqHandler<RegisterViewProps>(RegisterView.name, {
       errorMessage: "This is username is already taken. Please choose another.",

new file
app/plugins/crypto.ts
@@ -0,0 +1,26 @@
+// std
+import { createHash } from "node:crypto";
+// 3rd-party
+import fp from "fastify-plugin";
+import { FastifyPluginAsync } from "fastify";
+// app
+import { makeCryptoService } from "../services/crypto";
+
+export const cryptoPlugin: FastifyPluginAsync<{ serverSecret: string }> = fp(
+  async (server, { serverSecret }) => {
+    const cryptoServiceKey = "cryptoService";
+    const cryptoService = makeCryptoService({
+      compare: (a, b) => a === b,
+      hash: (str, salt) =>
+        createHash("sha512")
+          .update(`${salt}.${str}.${serverSecret}`)
+          .digest("hex"),
+    });
+    server.decorate(cryptoServiceKey, {
+      getter: () => cryptoService,
+    });
+    server.decorateRequest(cryptoServiceKey, {
+      getter: () => cryptoService,
+    });
+  }
+);

@@ -1 +1,2 @@
+export { cryptoPlugin } from "./crypto";
 export { prismaPlugin } from "./prisma";

@@ -24,7 +24,7 @@ import { version as appVersion } from "../package.json";
 // app
 import { Const } from "./const";
 import { Env } from "./env";
-import { prismaPlugin } from "./plugins";
+import { cryptoPlugin, prismaPlugin } from "./plugins";
 import {
   getEnv,
   localAppDomainPreHandler,

...
@@ -127,6 +127,9 @@ async function main(): Promise<AppServer> {
     ],
     setupServerBeforeRoutes(s) {
       s.addHook("preHandler", localAppDomainPreHandler);
+      s.decorateReply("makeRequestHandler", makeRequestHandler);
+
+      s.register(cryptoPlugin);
 
       s.register(prismaPlugin, { prisma });
 

...
@@ -232,8 +235,6 @@ async function main(): Promise<AppServer> {
         },
       });
 
-      s.decorateReply("makeRequestHandler", makeRequestHandler);
-
       s.get("/interceptor-imsw.js", {}, async (_, reply) => {
         return reply.sendFile("interceptor-imsw.js");
       });

file deleted
app/services/auth/comparePasswordHashes.ts
@@ -1,16 +0,0 @@
-// 1st-party
-import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
-// app
-import type { AuthServiceDeps } from "./types";
-
-const makeComparePasswordHashes: ServiceMethodFactory<
-  AuthServiceDeps,
-  [string, string],
-  boolean
-> = (_) => {
-  return (a, b) => {
-    return a === b;
-  };
-};
-
-export default makeComparePasswordHashes;

app/services/auth/createUser.ts
@@ -9,14 +9,13 @@ const makeCreateUser: ServiceMethodFactory<
   AuthServiceDeps,
   [AuthServiceCreateUserDTO],
   Promise<User>
-> = ({ request }) => {
-  const hashPassword = (x: string) => x;
+> = ({ cryptoService, request }) => {
   return async ({ emailAddress, username, password }) => {
     const user = await request.prisma.user.create({
       data: {
         email: emailAddress,
         username: username,
-        hashedPassword: hashPassword(password),
+        hashedPassword: cryptoService.computeHash(password),
       },
     });
 

app/services/auth/index.ts
@@ -3,14 +3,14 @@ import { makeService } from "@ethicdevs/react-monolith";
 // app
 import type { AuthServiceAPI, AuthServiceDeps } from "./types";
 // service methods
-import { default as makeComparePasswordHashes } from "./comparePasswordHashes";
 import { default as makeCreateUser } from "./createUser";
 import { default as makeIsExistingUsername } from "./isExistingUsername";
+import { default as makeIsExistingEmailAddress } from "./isExistingEmailAddress";
 
 export const makeAuthService = makeService<AuthServiceAPI, AuthServiceDeps>({
-  comparePasswordHashes: makeComparePasswordHashes,
   createUser: makeCreateUser,
   isExistingUsername: makeIsExistingUsername,
+  isExistingEmailAddress: makeIsExistingEmailAddress,
   isExistingUserUid: () => () => undefined,
   findUserByUid: () => () => undefined,
   findUserByUsername: () => () => undefined,

new file
app/services/auth/isExistingEmailAddress.ts
@@ -0,0 +1,24 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// app
+import type { AuthServiceDeps } from "./types";
+
+const makeIsExistingEmailAddress: ServiceMethodFactory<
+  AuthServiceDeps,
+  [string],
+  Promise<boolean>
+> = ({ request }) => {
+  return async (emailAddress) => {
+    const matchingUser = await request.prisma.user.findUnique({
+      select: {
+        id: true,
+      },
+      where: {
+        email: emailAddress,
+      },
+    });
+    return matchingUser != null;
+  };
+};
+
+export default makeIsExistingEmailAddress;

app/services/auth/types.ts
@@ -1,10 +1,14 @@
-import { FastifyRequest } from "fastify";
+// 1st-party
 import {
   ServiceApiContract,
   ServiceDependencies,
 } from "@ethicdevs/react-monolith";
+// 3rd-party
+import { FastifyRequest } from "fastify";
 // generated via script[generate:prisma]
 import { User } from "@prisma/client";
+// app
+import type { CryptoServiceAPI } from "../crypto/types";
 
 export interface AuthServiceCreateUserDTO {
   emailAddress: string;

...
@@ -13,9 +17,9 @@ export interface AuthServiceCreateUserDTO {
 }
 
 export interface AuthServiceAPI extends ServiceApiContract {
-  compareUserPasswordHashes(a: string, b: string): boolean;
-  createUser(dto: AuthServiceCreateUserDTO): Promise<User>;
+  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;

...
@@ -27,8 +31,6 @@ export interface AuthServiceAPI extends ServiceApiContract {
 }
 
 export interface AuthServiceDeps extends ServiceDependencies {
-  // i.e. to access request decorators, one may pass request as a dependency
-  //      to this service and all the methods within it would have access to
-  //      a reference to it (i.e; database, cache, another service, etc...)
+  cryptoService: CryptoServiceAPI;
   request: FastifyRequest;
 }

new file
app/services/crypto/compareHashes.ts
@@ -0,0 +1,16 @@
+// 1st-party
+import { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// app
+import { CryptoServiceDeps } from "./types";
+
+const makeCompareHash: ServiceMethodFactory<
+  CryptoServiceDeps,
+  [string, string],
+  boolean
+> = (deps) => {
+  return (a, b) => {
+    return deps.compare(a, b);
+  };
+};
+
+export default makeCompareHash;

new file
app/services/crypto/computeHash.ts
@@ -0,0 +1,16 @@
+// 1st-party
+import { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// app
+import { CryptoServiceDeps } from "./types";
+
+const makeComputeHash: ServiceMethodFactory<
+  CryptoServiceDeps,
+  [string, string],
+  string
+> = (deps) => {
+  return (strToHash, salt) => {
+    return deps.hash(strToHash, salt);
+  };
+};
+
+export default makeComputeHash;

new file
app/services/crypto/index.ts
@@ -0,0 +1,15 @@
+// 1st-party
+import { makeService } from "@ethicdevs/react-monolith";
+// app
+import type { CryptoServiceAPI, CryptoServiceDeps } from "./types";
+// service methods
+import { default as makeComputeHash } from "./computeHash";
+import { default as makeCompareHashes } from "./compareHashes";
+
+export const makeCryptoService = makeService<
+  CryptoServiceAPI,
+  CryptoServiceDeps
+>({
+  computeHash: makeComputeHash,
+  compareHashes: makeCompareHashes,
+});

new file
app/services/crypto/types.ts
@@ -0,0 +1,11 @@
+import { ServiceApiContract } from "@ethicdevs/react-monolith";
+
+export interface CryptoServiceAPI extends ServiceApiContract {
+  computeHash(strToHash: string, salt?: string): string;
+  compareHashes(a: string, b: string): boolean;
+}
+
+export interface CryptoServiceDeps {
+  hash(strToHash: string, salt?: string): string;
+  compare(a: string, b: string): boolean;
+}

app/views/auth/RegisterView.tsx
@@ -5,6 +5,7 @@ import type { CommonProps } from "../../types";
 import { Button, Layout, PageWrapper } from "../../components";
 
 export interface RegisterViewProps extends CommonProps {
+  errorMessage?: null | string;
   initialValues?: {
     username?: string;
     password?: string;

...
@@ -13,11 +14,17 @@ export interface RegisterViewProps extends CommonProps {
 
 const RegisterView: ReactView<RegisterViewProps> = ({
   commonProps,
+  errorMessage = undefined,
   initialValues = undefined,
 }) => {
   return (
     <Layout {...commonProps} showSideMenu={false}>
       <PageWrapper>
+        {errorMessage && (
+          <div className={"error_message"}>
+            <p>{errorMessage}</p>
+          </div>
+        )}
         <form action={`/auth/register`} method={"POST"}>
           {/* Email Address */}
           <div>

types/global/index.d.ts
@@ -8,6 +8,7 @@ import type {
   AppThemeScheme,
   SectionsWithPages,
 } from "../../app/types";
+import type { CryptoServiceAPI } from "../../app/services/crypto/types";
 
 declare module "@ethicdevs/fastify-custom-session" {
   declare interface CustomSession extends AppSessionData {}

...
@@ -15,21 +16,22 @@ declare module "@ethicdevs/fastify-custom-session" {
 
 declare module "fastify" {
   interface FastifyInstance {
+    // from crypto plugin
+    cryptoService: CryptoServiceAPI;
     // from prisma plugin
     prisma: PrismaClient;
   }
   interface FastifyRequest {
+    // from crypto plugin
+    cryptoService: CryptoServiceAPI;
     // from prisma plugin
     prisma: PrismaClient;
-
     // from cookies
     cookies: {
       theme_scheme: AppThemeScheme;
     };
-
     // from app
     sectionsWithPages: SectionsWithPages;
-
     // from react-monolith
     // A request utility that maps a viewName to its routerPath
     namedViewsPathMap: Record<string, string>;