feat(auth,crypto): add crypto service so its reusable@@ -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"
}
}
@@ -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.",
@@ -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");
});
@@ -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;
@@ -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),
},
});
@@ -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,
@@ -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;
@@ -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;
}
@@ -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;
@@ -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;
@@ -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,
+});
@@ -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;
+}
@@ -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>
@@ -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>;