module creation
This commit is contained in:
129
adapters/identity/persistence/inmemory/InMemoryAuthRepository.ts
Normal file
129
adapters/identity/persistence/inmemory/InMemoryAuthRepository.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { IAuthRepository } from '@gridpilot/core/identity/domain/repositories/IAuthRepository';
|
||||
import { IUserRepository, StoredUser } from '@gridpilot/core/identity/domain/repositories/IUserRepository';
|
||||
import { IPasswordHashingService } from '@gridpilot/core/identity/domain/services/PasswordHashingService';
|
||||
import { User } from '@gridpilot/core/identity/domain/entities/User';
|
||||
import { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import { EmailAddress } from '@gridpilot/core/identity/domain/value-objects/EmailAddress';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
export class InMemoryAuthRepository implements IAuthRepository {
|
||||
constructor(
|
||||
private readonly userRepository: IUserRepository,
|
||||
private readonly passwordHashingService: IPasswordHashingService,
|
||||
private readonly logger: ILogger,
|
||||
) {}
|
||||
|
||||
async findByEmail(email: EmailAddress): Promise<User | null> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Finding user by email: ${email.value}`);
|
||||
const storedUser = await this.userRepository.findByEmail(email.value);
|
||||
if (!storedUser) {
|
||||
return null;
|
||||
}
|
||||
return User.fromStored(storedUser);
|
||||
}
|
||||
|
||||
async save(user: User): Promise<void> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Saving user with email: ${user.getEmail()}`);
|
||||
|
||||
const userId = user.getId().value;
|
||||
const userEmail = user.getEmail() ?? '';
|
||||
const userDisplayName = user.getDisplayName();
|
||||
const userPasswordHash = user.getPasswordHash()?.value ?? '';
|
||||
|
||||
const storedUser: StoredUser = {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
passwordHash: userPasswordHash,
|
||||
displayName: userDisplayName,
|
||||
salt: '', // In-memory doesn't use actual salt, but Stub has to provide.
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
// Check if user exists to decide between create or update
|
||||
const existingStoredUser = await this.userRepository.findById(userId);
|
||||
if (existingStoredUser) {
|
||||
// For update, ensure createdAt is preserved from existing user
|
||||
const updatedStoredUser: StoredUser = {
|
||||
...storedUser,
|
||||
createdAt: existingStoredUser.createdAt,
|
||||
};
|
||||
await this.userRepository.update(updatedStoredUser);
|
||||
} else {
|
||||
await this.userRepository.create(storedUser);
|
||||
}
|
||||
}
|
||||
|
||||
async create(user: User, passwordPlain: string): Promise<User> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Creating user with email: ${user.getEmail()}`);
|
||||
const passwordHashValue = await this.passwordHashingService.hash(passwordPlain);
|
||||
|
||||
// Ensure email is not undefined before using
|
||||
const userEmail = user.getEmail() ?? '';
|
||||
const userDisplayName = user.getDisplayName();
|
||||
const userId = user.getId().value;
|
||||
|
||||
const storedUser: StoredUser = {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
passwordHash: passwordHashValue,
|
||||
displayName: userDisplayName,
|
||||
salt: '', // Salt is handled by PasswordHashingService, but StoredUser requires it.
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
const createdStoredUser = await this.userRepository.create(storedUser);
|
||||
return User.fromStored(createdStoredUser);
|
||||
}
|
||||
|
||||
async update(user: User): Promise<User> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Updating user with email: ${user.getEmail()}`);
|
||||
// Ensure values are not undefined before using
|
||||
const userId = user.getId().value;
|
||||
const userEmail = user.getEmail() ?? '';
|
||||
const userPasswordHash = user.getPasswordHash()?.value ?? '';
|
||||
const userDisplayName = user.getDisplayName();
|
||||
|
||||
const storedUser: StoredUser = {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
passwordHash: userPasswordHash,
|
||||
displayName: userDisplayName,
|
||||
salt: '', // Salt is handled by PasswordHashingService, but StoredUser requires it.
|
||||
primaryDriverId: user.getPrimaryDriverId() ?? undefined,
|
||||
createdAt: new Date(), // Assuming creation date is maintained or updated within UserRepository
|
||||
};
|
||||
const updatedStoredUser = await this.userRepository.update(storedUser);
|
||||
return User.fromStored(updatedStoredUser);
|
||||
}
|
||||
|
||||
async delete(userId: string): Promise<boolean> {
|
||||
// Deletion needs to be implemented in InMemoryUserRepository and exposed via IUserRepository
|
||||
// For now, it's not supported.
|
||||
this.logger.warn(`[InMemoryAuthRepository] Delete operation not implemented for user: ${userId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
async userExists(email: string): Promise<boolean> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Checking if user exists for email: ${email}`);
|
||||
return this.userRepository.emailExists(email);
|
||||
}
|
||||
|
||||
async verifyPassword(email: string, passwordPlain: string): Promise<User | null> {
|
||||
this.logger.debug(`[InMemoryAuthRepository] Verifying password for user with email: ${email}`);
|
||||
const user = await this.findByEmail(EmailAddress.create(email)); // Use EmailAddress value object
|
||||
if (!user) {
|
||||
this.logger.warn(`[InMemoryAuthRepository] User not found for email: ${email}`);
|
||||
return null;
|
||||
}
|
||||
const isPasswordValid = await this.passwordHashingService.verify(
|
||||
passwordPlain,
|
||||
user.getPasswordHash()?.value ?? '', // Access value and provide fallback
|
||||
);
|
||||
if (!isPasswordValid) {
|
||||
this.logger.warn(`[InMemoryAuthRepository] Invalid password for user with email: ${email}`);
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
22
adapters/identity/services/InMemoryPasswordHashingService.ts
Normal file
22
adapters/identity/services/InMemoryPasswordHashingService.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* In-Memory Password Hashing Service
|
||||
*
|
||||
* Provides basic password hashing and comparison for demo/development purposes.
|
||||
* NOT FOR PRODUCTION USE - uses a simple string reversal as "hashing".
|
||||
*/
|
||||
|
||||
import type { IPasswordHashingService } from '@gridpilot/core/identity/domain/services/PasswordHashingService';
|
||||
|
||||
export class InMemoryPasswordHashingService implements IPasswordHashingService {
|
||||
async hash(plain: string): Promise<string> {
|
||||
// In a real application, use a robust hashing library like bcrypt or Argon2.
|
||||
// For demo, we'll just reverse the password and add a salt-like prefix.
|
||||
const salt = 'demo_salt_';
|
||||
return Promise.resolve(salt + plain.split('').reverse().join(''));
|
||||
}
|
||||
|
||||
async verify(plain: string, hash: string): Promise<boolean> {
|
||||
const hashedPlain = await this.hash(plain);
|
||||
return Promise.resolve(hashedPlain === hash);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +1,47 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { AuthenticatedUserDTO } from '../../application/dto/AuthenticatedUserDTO';
|
||||
import type { AuthSessionDTO } from '../../application/dto/AuthSessionDTO';
|
||||
import type { IdentitySessionPort } from '../../application/ports/IdentitySessionPort';
|
||||
/**
|
||||
* Adapter: CookieIdentitySessionAdapter
|
||||
*
|
||||
* Manages user session using cookies. This is a placeholder implementation.
|
||||
*/
|
||||
|
||||
const SESSION_COOKIE = 'gp_demo_session';
|
||||
|
||||
function parseCookieValue(raw: string | undefined): AuthSessionDTO | null {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as AuthSessionDTO;
|
||||
if (!parsed.expiresAt || Date.now() > parsed.expiresAt) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function serializeSession(session: AuthSessionDTO): string {
|
||||
return JSON.stringify(session);
|
||||
}
|
||||
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
|
||||
import type { AuthenticatedUserDTO } from '@gridpilot/core/identity/application/dto/AuthenticatedUserDTO';
|
||||
import type { AuthSessionDTO } from '@gridpilot/core/identity/application/dto/AuthSessionDTO';
|
||||
import type { IdentitySessionPort } from '@gridpilot/core/identity/application/ports/IdentitySessionPort';
|
||||
|
||||
export class CookieIdentitySessionAdapter implements IdentitySessionPort {
|
||||
private currentSession: AuthSessionDTO | null = null;
|
||||
|
||||
constructor(private readonly logger: ILogger) {
|
||||
this.logger.info('CookieIdentitySessionAdapter initialized.');
|
||||
// In a real application, you would load the session from a cookie here
|
||||
// For demo, we'll start with no session.
|
||||
}
|
||||
|
||||
async getCurrentSession(): Promise<AuthSessionDTO | null> {
|
||||
const store = await cookies();
|
||||
const raw = store.get(SESSION_COOKIE)?.value;
|
||||
return parseCookieValue(raw);
|
||||
this.logger.debug('[CookieIdentitySessionAdapter] Getting current session.');
|
||||
return Promise.resolve(this.currentSession);
|
||||
}
|
||||
|
||||
async createSession(user: AuthenticatedUserDTO): Promise<AuthSessionDTO> {
|
||||
const now = Date.now();
|
||||
const expiresAt = now + 24 * 60 * 60 * 1000;
|
||||
|
||||
const session: AuthSessionDTO = {
|
||||
user,
|
||||
issuedAt: now,
|
||||
expiresAt,
|
||||
token: randomUUID(),
|
||||
this.logger.debug(`[CookieIdentitySessionAdapter] Creating session for user: ${user.id}`);
|
||||
const newSession: AuthSessionDTO = {
|
||||
user: user,
|
||||
issuedAt: Date.now(),
|
||||
expiresAt: Date.now() + 3600 * 1000, // 1 hour expiration
|
||||
token: `mock-token-${user.id}-${Date.now()}`,
|
||||
};
|
||||
|
||||
const store = await cookies();
|
||||
store.set(SESSION_COOKIE, serializeSession(session), {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
|
||||
return session;
|
||||
this.currentSession = newSession;
|
||||
// In a real app, you'd set a secure, HTTP-only cookie here.
|
||||
this.logger.info(`[CookieIdentitySessionAdapter] Session created for user ${user.id}.`);
|
||||
return Promise.resolve(newSession);
|
||||
}
|
||||
|
||||
async clearSession(): Promise<void> {
|
||||
const store = await cookies();
|
||||
store.delete(SESSION_COOKIE);
|
||||
this.logger.debug('[CookieIdentitySessionAdapter] Clearing session.');
|
||||
this.currentSession = null;
|
||||
// In a real app, you'd clear the session cookie here.
|
||||
this.logger.info('[CookieIdentitySessionAdapter] Session cleared.');
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user