feat(auth): add login feature and complete registration@@ -1,5 +1,5 @@
{
- "_generatedAtUnix": 1663364284973,
+ "_generatedAtUnix": 1663370041613,
"_hashAlgorithm": "sha1",
"_version": 2,
"islands": {
@@ -25,6 +25,10 @@
"hash": "389861a6e7f9ff12026ddf5e91bdf4f06646116e",
"pathSource": "./app/views/InternalErrorView.tsx"
},
+ "LoginView": {
+ "hash": "ca64e50d382088fd54ac2617a983d369c2fe8820",
+ "pathSource": "./app/views/auth/LoginView.tsx"
+ },
"RegisterView": {
"hash": "3e2c7053b529624ed6cfa9ea8631b4480bd9775b",
"pathSource": "./app/views/auth/RegisterView.tsx"
@@ -0,0 +1,11 @@
+// 3rd-party
+import type { ReqHandler } from "@ethicdevs/react-monolith";
+// app
+import LoginView, { LoginViewProps } from "../../views/auth/LoginView";
+
+const getLoginView: ReqHandler = (request, reply) => {
+ const reqHandler = reply.makeRequestHandler(request, reply);
+ return reqHandler<LoginViewProps>(LoginView.name, {});
+};
+
+export default getLoginView;
@@ -1,9 +1,11 @@
-// import { default as getLoginView } from "./getLoginView";
+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 = {
- // getLoginView,
+ getLoginView,
getRegisterView,
+ postLoginAction,
postRegisterAction,
};
@@ -0,0 +1,78 @@
+// 3rd-party
+import type { ReqHandler } from "@ethicdevs/react-monolith";
+// app
+import { AppRoute, AppRoutesParams } from "../../routes";
+import LoginView, { LoginViewProps } from "../../views/auth/LoginView";
+import { makeAuthService } from "../../services/auth";
+
+const postLoginView: ReqHandler = async (request, reply) => {
+ const authService = makeAuthService({
+ cryptoService: request.cryptoService,
+ request,
+ });
+
+ const reqHandler = reply.makeRequestHandler(request, reply);
+
+ const { email_address: emailAddress, password } =
+ request.body as AppRoutesParams[AppRoute.AUTH_LOGIN_ACTION]["body"];
+
+ const initialValues = { emailAddress };
+
+ if (request.validationError != null) {
+ const { message: errorMessage } = request.validationError;
+
+ return reqHandler<LoginViewProps>(LoginView.name, {
+ errorMessage,
+ initialValues,
+ });
+ }
+
+ if (emailAddress.trim() === "") {
+ return reqHandler<LoginViewProps>(LoginView.name, {
+ errorMessage: "Please provide a non-empty email address.",
+ initialValues: { emailAddress },
+ });
+ }
+
+ if (password.trim() === "") {
+ return reqHandler<LoginViewProps>(LoginView.name, {
+ errorMessage: "Please provide a non-empty password.",
+ initialValues: { emailAddress },
+ });
+ }
+
+ if ((await authService.isExistingEmailAddress(emailAddress)) === false) {
+ return reqHandler<LoginViewProps>(LoginView.name, {
+ errorMessage:
+ "Invalid credentials. Please verify your input and try again.",
+ initialValues: { emailAddress },
+ });
+ }
+
+ const [isLoginAllowed, user] = await authService.shouldAllowUserLogin(
+ emailAddress,
+ password
+ );
+
+ if (isLoginAllowed === false || user == null) {
+ return reqHandler<LoginViewProps>(LoginView.name, {
+ errorMessage:
+ "Invalid credentials. Please verify your input and try again.",
+ initialValues: { emailAddress },
+ });
+ }
+
+ const { avatarUri, role, id: userId, username } = user;
+ request.session.data.authenticated = true;
+ request.session.data.curr_user_avatar_uri = avatarUri;
+ request.session.data.curr_user_role = role;
+ request.session.data.curr_user_uid = userId;
+ request.session.data.curr_user_username = username;
+
+ console.log(`Logged user with id: ${userId}`);
+
+ reply.redirect(request.namedViewsPathMap[AppRoute.HOME]);
+ return reply;
+};
+
+export default postLoginView;
@@ -15,6 +15,7 @@ export enum AppRoute {
AUTH_REGISTER = "auth.register",
AUTH_REGISTER_ACTION = "auth.register.action",
AUTH_LOGIN = "auth.login",
+ AUTH_LOGIN_ACTION = "auth.login.action",
}
export interface AppRoutesParams extends IRouteParams {
@@ -31,6 +32,12 @@ export interface AppRoutesParams extends IRouteParams {
};
};
[AppRoute.AUTH_LOGIN]: undefined;
+ [AppRoute.AUTH_LOGIN_ACTION]: {
+ body: {
+ email_address: string;
+ password: string;
+ };
+ };
}
export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
@@ -62,6 +69,17 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
},
},
[AppRoute.AUTH_LOGIN]: undefined,
+ [AppRoute.AUTH_LOGIN_ACTION]: {
+ body: {
+ type: "object",
+ required: ["email_address", "password"],
+ additionalProperties: false,
+ properties: {
+ email_address: { type: "string" },
+ password: { type: "string" },
+ },
+ },
+ },
};
const RootAppRouter: AppRouter = () => (
@@ -94,6 +112,19 @@ const RootAppRouter: AppRouter = () => (
schema={AppRoutesSchemas[AppRoute.AUTH_REGISTER_ACTION]}
handler={AuthController.postRegisterAction}
/>
+ <Router.Route
+ name={AppRoute.AUTH_LOGIN}
+ method={"GET"}
+ path={"/auth/login"}
+ handler={AuthController.getLoginView}
+ />
+ <Router.Route
+ name={AppRoute.AUTH_LOGIN_ACTION}
+ method={"POST"}
+ path={"/auth/login"}
+ schema={AppRoutesSchemas[AppRoute.AUTH_LOGIN_ACTION]}
+ handler={AuthController.postLoginAction}
+ />
</Router.Group>
</Router.Root>
);
@@ -6,6 +6,7 @@ import type { AuthServiceAPI, AuthServiceDeps } from "./types";
import { default as makeCreateUser } from "./createUser";
import { default as makeIsExistingUsername } from "./isExistingUsername";
import { default as makeIsExistingEmailAddress } from "./isExistingEmailAddress";
+import { default as makeShouldAllowUserLogin } from "./shouldAllowUserLogin";
export const makeAuthService = makeService<AuthServiceAPI, AuthServiceDeps>({
createUser: makeCreateUser,
@@ -15,5 +16,6 @@ export const makeAuthService = makeService<AuthServiceAPI, AuthServiceDeps>({
findUserByUid: () => () => undefined,
findUserByUsername: () => () => undefined,
sendUserEmailAddressV8nEmail: () => () => undefined,
+ shouldAllowUserLogin: makeShouldAllowUserLogin,
validateUserEmailAddress: () => () => Promise.resolve(undefined),
});
@@ -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 { AuthServiceDeps } from "./types";
+
+const makeShouldAllowUserLogin: ServiceMethodFactory<
+ AuthServiceDeps,
+ [string, string],
+ Promise<[boolean, User | null]>
+> = ({ cryptoService, request }) => {
+ return async (emailAddress, password) => {
+ const passwordHash = cryptoService.computeHash(password);
+ const matchingUser = await request.prisma.user.findUnique({
+ where: {
+ email: emailAddress,
+ },
+ });
+ if (matchingUser == null) return [false, null];
+ const shouldAllowLogin = matchingUser.hashedPassword === passwordHash;
+ if (shouldAllowLogin === false) return [false, null];
+ return [true, matchingUser];
+ };
+};
+
+export default makeShouldAllowUserLogin;
@@ -24,6 +24,10 @@ export interface AuthServiceAPI extends ServiceApiContract {
findUserByUsername(username: string): void;
findUserByUid(userUid: string): void;
sendUserEmailAddressV8nEmail(args: unknown[]): void;
+ shouldAllowUserLogin(
+ emailAddress: string,
+ password: string
+ ): Promise<[boolean, User | null]>;
validateUserEmailAddress(
userUid: string,
emailAddress: string
@@ -0,0 +1,58 @@
+import type { ReactView } from "@ethicdevs/react-monolith";
+import React from "react";
+// app
+import type { CommonProps } from "../../types";
+import { Button, Layout, PageWrapper } from "../../components";
+
+export interface LoginViewProps extends CommonProps {
+ errorMessage?: null | string;
+ initialValues?: {
+ emailAddress?: string;
+ password?: string;
+ };
+}
+
+const LoginView: ReactView<LoginViewProps> = ({
+ commonProps,
+ errorMessage = undefined,
+ initialValues = undefined,
+}) => {
+ return (
+ <Layout {...commonProps} showSideMenu={false}>
+ <PageWrapper>
+ {errorMessage && (
+ <div className={"error_message"}>
+ <p>{errorMessage}</p>
+ </div>
+ )}
+ <form action={`/auth/login`} method={"POST"}>
+ {/* Email Address */}
+ <div>
+ <label htmlFor={"username"}>Email Address:</label>
+ <input
+ type={"text"}
+ name={"email_address"}
+ placeholder={"Enter your email address..."}
+ defaultValue={initialValues?.emailAddress}
+ />
+ </div>
+ {/* Password */}
+ <div>
+ <label htmlFor={"username"}>Password:</label>
+ <input
+ type={"password"}
+ name={"password"}
+ placeholder={"Enter your password..."}
+ defaultValue={initialValues?.password}
+ />
+ </div>
+ {/* Submit Button */}
+ <Button type={"submit"}>Log me In!</Button>
+ </form>
+ </PageWrapper>
+ </Layout>
+ );
+};
+
+LoginView.displayName = "LoginView";
+export default LoginView;