feat(auth): add registration feature (wip)
+ 129
- 19
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1663228194515,
+  "_generatedAtUnix": 1663360458599,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

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

app/controllers/auth/postRegisterAction.ts
@@ -3,17 +3,59 @@ import type { ReqHandler } from "@ethicdevs/react-monolith";
 // app
 import { AppRoute, AppRoutesParams } from "../../routes";
 import RegisterView, { RegisterViewProps } from "../../views/auth/RegisterView";
+import { makeAuthService } from "../../services/auth";
 
-const postRegisterView: ReqHandler = (request, reply) => {
-  const { username, password } =
-    request.body as AppRoutesParams[AppRoute.AUTH_REGISTER_ACTION]["body"];
+const postRegisterView: ReqHandler = async (request, reply) => {
+  const authService = makeAuthService({ request });
+  const reqHandler = reply.makeRequestHandler(request, reply);
 
-  password;
+  const {
+    email_address: emailAddress,
+    username,
+    password,
+  } = request.body as AppRoutesParams[AppRoute.AUTH_REGISTER_ACTION]["body"];
 
-  const reqHandler = reply.makeRequestHandler(request, reply);
-  return reqHandler<RegisterViewProps>(RegisterView.name, {
-    initialValues: { username },
+  const initialValues = { emailAddress, username };
+
+  if (request.validationError != null) {
+    const {
+      message: errorMessage,
+      validation,
+      validationContext,
+    } = request.validationError;
+
+    console.log("validation:", validation);
+    console.log("validationContext:", validationContext);
+
+    return reqHandler<RegisterViewProps>(RegisterView.name, {
+      errorMessage,
+      initialValues,
+    });
+  }
+
+  if (await authService.isExistingUsername(username)) {
+    return reqHandler<RegisterViewProps>(RegisterView.name, {
+      errorMessage: "This is username is already taken. Please choose another.",
+      initialValues: { emailAddress, username },
+    });
+  }
+
+  const newUser = await authService.createUser({
+    emailAddress,
+    username,
+    password,
   });
+
+  console.log(`Made new user with id: ${newUser.id}`);
+
+  request.session.data.authenticated = true;
+  request.session.data.curr_user_uid = newUser.id;
+  request.session.data.curr_user_role = newUser.role;
+  request.session.data.curr_user_username = newUser.username;
+  request.session.data.curr_user_avatar_uri = newUser.avatarUri;
+
+  reply.redirect(request.namedViewsPathMap[AppRoute.HOME]);
+  return reply;
 };
 
 export default postRegisterView;

@@ -25,6 +25,7 @@ export interface AppRoutesParams extends IRouteParams {
   [AppRoute.AUTH_REGISTER]: undefined;
   [AppRoute.AUTH_REGISTER_ACTION]: {
     body: {
+      email_address: string;
       username: string;
       password: string;
     };

...
@@ -51,9 +52,10 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
   [AppRoute.AUTH_REGISTER_ACTION]: {
     body: {
       type: "object",
-      required: ["username", "password"],
+      required: ["email_address", "username", "password"],
       additionalProperties: false,
       properties: {
+        email_address: { type: "string" },
         username: { type: "string" },
         password: { type: "string" },
       },

@@ -148,7 +148,7 @@ async function main(): Promise<AppServer> {
         cookieOptions: cookiesOpts,
         getUniqId: cuid as () => string,
         password: Env.COOKIE_SECRET,
-        storeAdapter: new PrismaSessionAdapter(prisma),
+        storeAdapter: new PrismaSessionAdapter(prisma) as any,
         ttl: Const.SESSION_TTL_SECONDS,
         initialSession: {
           sessionId: null,

new file
app/services/auth/createUser.ts
@@ -0,0 +1,27 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { User } from "@prisma/client";
+// app
+import type { AuthServiceCreateUserDTO, AuthServiceDeps } from "./types";
+
+const makeCreateUser: ServiceMethodFactory<
+  AuthServiceDeps,
+  [AuthServiceCreateUserDTO],
+  Promise<User>
+> = ({ request }) => {
+  const hashPassword = (x: string) => x;
+  return async ({ emailAddress, username, password }) => {
+    const user = await request.prisma.user.create({
+      data: {
+        email: emailAddress,
+        username: username,
+        hashedPassword: hashPassword(password),
+      },
+    });
+
+    return user;
+  };
+};
+
+export default makeCreateUser;

app/services/auth/index.ts
@@ -4,10 +4,12 @@ import { makeService } from "@ethicdevs/react-monolith";
 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";
 
 export const makeAuthService = makeService<AuthServiceAPI, AuthServiceDeps>({
   comparePasswordHashes: makeComparePasswordHashes,
+  createUser: makeCreateUser,
   isExistingUsername: makeIsExistingUsername,
   isExistingUserUid: () => () => undefined,
   findUserByUid: () => () => undefined,

app/services/auth/isExistingUsername.ts
@@ -6,10 +6,18 @@ import type { AuthServiceDeps } from "./types";
 const makeIsExistingUsername: ServiceMethodFactory<
   AuthServiceDeps,
   [string],
-  boolean
-> = (_) => {
-  return (username) => {
-    return username === "username";
+  Promise<boolean>
+> = ({ request }) => {
+  return async (username) => {
+    const matchingUser = await request.prisma.user.findUnique({
+      select: {
+        id: true,
+      },
+      where: {
+        username,
+      },
+    });
+    return matchingUser != null;
   };
 };
 

app/services/auth/types.ts
@@ -3,10 +3,19 @@ import {
   ServiceApiContract,
   ServiceDependencies,
 } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { User } from "@prisma/client";
+
+export interface AuthServiceCreateUserDTO {
+  emailAddress: string;
+  username: string;
+  password: string;
+}
 
 export interface AuthServiceAPI extends ServiceApiContract {
   compareUserPasswordHashes(a: string, b: string): boolean;
-  isExistingUsername(username: string): boolean;
+  createUser(dto: AuthServiceCreateUserDTO): Promise<User>;
+  isExistingUsername(username: string): Promise<boolean>; // implemented.
   isExistingUserUid(userUid: string): boolean;
   findUserByUsername(username: string): void;
   findUserByUid(userUid: string): void;

app/views/auth/RegisterView.tsx
@@ -19,15 +19,27 @@ const RegisterView: ReactView<RegisterViewProps> = ({
     <Layout {...commonProps} showSideMenu={false}>
       <PageWrapper>
         <form action={`/auth/register`} method={"POST"}>
+          {/* Email Address */}
+          <div>
+            <label htmlFor={"username"}>Email Address:</label>
+            <input
+              type={"text"}
+              name={"email_address"}
+              placeholder={"i.e. john.doe@provider.tld..."}
+              defaultValue={initialValues?.username}
+            />
+          </div>
+          {/* Username */}
           <div>
             <label htmlFor={"username"}>Username:</label>
             <input
               type={"text"}
               name={"username"}
-              placeholder={"Choose a username (i.e. john.doe)..."}
+              placeholder={"i.e. john.doe, jane.smith, etc..."}
               defaultValue={initialValues?.username}
             />
           </div>
+          {/* Password */}
           <div>
             <label htmlFor={"username"}>Password:</label>
             <input

...
@@ -37,6 +49,7 @@ const RegisterView: ReactView<RegisterViewProps> = ({
               defaultValue={initialValues?.password}
             />
           </div>
+          {/* Submit Button */}
           <Button type={"submit"}>Create my Account</Button>
         </form>
       </PageWrapper>

types/global/index.d.ts
@@ -1,6 +1,13 @@
+// 3rd-party
 import fastify from "fastify";
-
-import type { AppThemeScheme, SectionsWithPages } from "../../app/types";
+// generated via script[generate:prisma]
+import { PrismaClient } from "@prisma/client";
+// app
+import type {
+  AppSessionData,
+  AppThemeScheme,
+  SectionsWithPages,
+} from "../../app/types";
 
 declare module "@ethicdevs/fastify-custom-session" {
   declare interface CustomSession extends AppSessionData {}