import { Context, Effect, Layer } from "effect"; import * as bcrypt from "bcrypt"; import { AuthError, ValidationError } from "../../../shared/types.js"; const SALT_ROUNDS = 12; export class PasswordService extends Context.Tag("PasswordService")< PasswordService, { readonly hash: (password: string) => Effect.Effect; readonly verify: ( password: string, hash: string ) => Effect.Effect; readonly validatePasswordStrength: ( password: string ) => Effect.Effect; } >() { static Live = Layer.succeed( this, { hash: (password: string) => Effect.tryPromise({ try: () => bcrypt.hash(password, SALT_ROUNDS), catch: (error) => new AuthError( "HASH_FAILED", "Failed to hash password", error ), }), verify: (password: string, hash: string) => Effect.tryPromise({ try: () => bcrypt.compare(password, hash), catch: (error) => new AuthError( "VERIFY_FAILED", "Failed to verify password", error ), }), validatePasswordStrength: (password: string) => Effect.gen(function* () { if (password.length < 8) { return yield* Effect.fail( new ValidationError( "password", "Password must be at least 8 characters long" ) ); } if (!/[A-Z]/.test(password)) { return yield* Effect.fail( new ValidationError( "password", "Password must contain at least one uppercase letter" ) ); } if (!/[a-z]/.test(password)) { return yield* Effect.fail( new ValidationError( "password", "Password must contain at least one lowercase letter" ) ); } if (!/[0-9]/.test(password)) { return yield* Effect.fail( new ValidationError( "password", "Password must contain at least one number" ) ); } }), } ); }