Skip to content

Commit

Permalink
Update auth/recover and auth/verify-email services
Browse files Browse the repository at this point in the history
Add Dependencies interface to facilitate testing
  • Loading branch information
sergiodxa committed Oct 26, 2024
1 parent bda3e21 commit 96ec304
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 11 deletions.
31 changes: 25 additions & 6 deletions app/services.server/auth/recover.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,39 @@
import type { Credential } from "app:entities/credential";
import type { User } from "app:entities/user";
import { AuditLogsRepository } from "app:repositories.server/audit-logs";
import { CredentialsRepository } from "app:repositories.server/credentials";
import { UsersRepository } from "app:repositories.server/users";
import { type Entity, waitUntil } from "@edgefirst-dev/core";
import type { Email } from "@edgefirst-dev/email";
import { encodeBase32 } from "@oslojs/encoding";

/**
* Initiates a password recovery process by generating a one-time password (OTP).
*
* @param input - The recovery input data containing the email.
* @param repos - The dependency injection object containing repositories.
* @param deps - The dependency injection object containing repositories.
* @returns A promise that resolves to an OTP.
* @throws {Error} If the user is not found.
*/
export async function recover(
input: recover.Input,
repos = {
deps: recover.Dependencies = {
audits: new AuditLogsRepository(),
users: new UsersRepository(),
credentials: new CredentialsRepository(),
},
): Promise<recover.Output> {
let [user] = await repos.users.findByEmail(input.email);
let [user] = await deps.users.findByEmail(input.email);
if (!user) throw new Error("User not found");

let [credential] = await repos.credentials.findByUser(user);
let [credential] = await deps.credentials.findByUser(user);
if (!credential) throw new Error("User has no associated credentials");

let token = generateRandomOTP();

await repos.credentials.createResetToken(credential, token);
await deps.credentials.createResetToken(credential, token);

await repos.audits.create(user, "generate_recovery_code");
waitUntil(deps.audits.create(user, "generate_recovery_code"));

return { token };
}
Expand Down Expand Up @@ -64,4 +67,20 @@ export namespace recover {
/** The generated OTP (one-time password) for recovery. */
token: string;
}

export interface Dependencies {
audits: {
create(user: User, action: string, entity?: Entity): Promise<void>;
};
users: {
findByEmail: (email: Email) => Promise<User[]>;
};
credentials: {
findByUser: (user: User) => Promise<Credential[]>;
createResetToken: (
credential: Credential,
token: string,
) => Promise<void>;
};
}
}
22 changes: 17 additions & 5 deletions app/services.server/auth/verify-email.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,50 @@
import type { User } from "app:entities/user";
import { AuditLogsRepository } from "app:repositories.server/audit-logs";
import { UsersRepository } from "app:repositories.server/users";
import { type Entity, waitUntil } from "@edgefirst-dev/core";
import type { Email } from "@edgefirst-dev/email";

/**
* Verifies a user's email address by setting the `emailVerifiedAt` field.
*
* @param input - The registration input data containing the email and password.
* @param repos - The dependency injection object containing repositories.
* @param deps - The dependency injection object containing repositories.
* @throws {Error} If the user is not found.
* @returns A promise that resolves when the email is verified, or immediately if the user is already verified.
*/
export async function verifyEmail(
input: verifyEmail.Input,
repos = {
deps: verifyEmail.Dependencies = {
audits: new AuditLogsRepository(),
user: new UsersRepository(),
},
) {
let [user] = await repos.user.findByEmail(input.email);
let [user] = await deps.user.findByEmail(input.email);
if (!user) throw new Error("User not found");

// If the user is already verified, we don't need to do anything
if (user.hasEmailVerified) return;

// Here we should validate that data.code is valid

await repos.user.verifyEmail(user);
await deps.user.verifyEmail(user);

await repos.audits.create(user, "verified_email");
waitUntil(deps.audits.create(user, "verified_email"));
}

export namespace verifyEmail {
export interface Input {
readonly email: Email;
readonly code: string;
}

export interface Dependencies {
audits: {
create(user: User, action: string, entity?: Entity): Promise<void>;
};
user: {
findByEmail(email: Email): Promise<User[]>;
verifyEmail(user: User): Promise<void>;
};
}
}

0 comments on commit 96ec304

Please sign in to comment.