Files
gridpilot.gg/core/identity/application/use-cases/LoginWithEmailUseCase.ts
2025-12-31 19:55:43 +01:00

124 lines
3.8 KiB
TypeScript

/**
* Login with Email Use Case
*
* Authenticates a user with email and password.
*/
import type { IUserRepository } from '../../domain/repositories/IUserRepository';
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
import { Result } from '@core/shared/application/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { UseCaseOutputPort, Logger } from '@core/shared/application';
export type LoginWithEmailInput = {
email: string;
password: string;
};
export type LoginWithEmailResult = {
sessionToken: string;
userId: string;
displayName: string;
email?: string;
primaryDriverId?: string;
issuedAt: number;
expiresAt: number;
};
export type LoginWithEmailErrorCode =
| 'INVALID_INPUT'
| 'INVALID_CREDENTIALS'
| 'REPOSITORY_ERROR';
export type LoginWithEmailApplicationError = ApplicationErrorCode<
LoginWithEmailErrorCode,
{ message: string }
>;
export class LoginWithEmailUseCase {
constructor(
private readonly userRepository: IUserRepository,
private readonly sessionPort: IdentitySessionPort,
private readonly logger: Logger,
private readonly output: UseCaseOutputPort<LoginWithEmailResult>,
) {}
async execute(input: LoginWithEmailInput): Promise<Result<void, LoginWithEmailApplicationError>> {
try {
if (!input.email || !input.password) {
return Result.err({
code: 'INVALID_INPUT',
details: { message: 'Email and password are required' },
} as LoginWithEmailApplicationError);
}
const normalizedEmail = input.email.toLowerCase().trim();
const user = await this.userRepository.findByEmail(normalizedEmail);
if (!user) {
return Result.err({
code: 'INVALID_CREDENTIALS',
details: { message: 'Invalid email or password' },
} as LoginWithEmailApplicationError);
}
// Verify password using PasswordHash value object
const { PasswordHash } = await import('@core/identity/domain/value-objects/PasswordHash');
const storedPasswordHash = PasswordHash.fromHash(user.passwordHash);
const isValid = await storedPasswordHash.verify(input.password);
if (!isValid) {
return Result.err({
code: 'INVALID_CREDENTIALS',
details: { message: 'Invalid email or password' },
} as LoginWithEmailApplicationError);
}
type CreateSessionInput = Parameters<IdentitySessionPort['createSession']>[0];
const createSessionInput = {
id: user.id,
displayName: user.displayName,
...(user.email !== undefined ? { email: user.email } : {}),
...(user.primaryDriverId !== undefined
? { primaryDriverId: user.primaryDriverId }
: {}),
} satisfies CreateSessionInput;
const session = await this.sessionPort.createSession(createSessionInput);
const result: LoginWithEmailResult = {
sessionToken: session.token,
userId: session.user.id,
displayName: session.user.displayName,
...(session.user.email !== undefined ? { email: session.user.email } : {}),
...(session.user.primaryDriverId !== undefined
? { primaryDriverId: session.user.primaryDriverId }
: {}),
issuedAt: session.issuedAt,
expiresAt: session.expiresAt,
};
this.output.present(result);
return Result.ok(undefined);
} catch (error) {
const message =
error instanceof Error && error.message
? error.message
: 'Failed to execute LoginWithEmailUseCase';
this.logger.error(
'LoginWithEmailUseCase.execute failed',
error instanceof Error ? error : undefined,
{ input },
);
return Result.err({
code: 'REPOSITORY_ERROR',
details: { message },
} as LoginWithEmailApplicationError);
}
}
}