Files
gridpilot.gg/core/identity/application/use-cases/LoginWithEmailUseCase.ts
2026-01-16 15:20:25 +01:00

120 lines
3.6 KiB
TypeScript

/**
* Login with Email Use Case
*
* Authenticates a user with email and password.
*/
import { PasswordHash } from '@/identity/domain/value-objects/PasswordHash';
import type { Logger } from '@core/shared/domain/Logger';
import { Result } from '@core/shared/domain/Result';
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
import type { IdentitySessionPort } from '../ports/IdentitySessionPort';
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,
) {}
async execute(input: LoginWithEmailInput): Promise<Result<LoginWithEmailResult, 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 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,
};
return Result.ok(result);
} 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);
}
}
}