Ajout des fichiers de base pour le projet.

This commit is contained in:
2025-08-12 17:18:10 +00:00
parent 0695d2ca1e
commit 14e69e1f61
20 changed files with 2087 additions and 0 deletions

17
src/app.ts Normal file
View File

@ -0,0 +1,17 @@
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import loadConfiguration from "./config";
import { DatabaseInterface } from "./database/DatabaseInterface";
import PgDatabase from "./database/PgDatabase";
import { setup as setupAccounts } from "./domain/account/setup";
const config = loadConfiguration();
const database: DatabaseInterface = new PgDatabase(config.database);
const app = new Hono();
app.route("/accounts", setupAccounts(database));
serve({
port: config.port,
fetch: app.fetch,
});

45
src/config.ts Normal file
View File

@ -0,0 +1,45 @@
import { DEFAULT_PORT } from "./const";
export interface Configuration {
database: {
host: string;
port: number;
user: string;
password: string;
database: string;
};
port: number;
}
export default function loadConfiguration(): Configuration {
process.loadEnvFile();
return {
database: {
host: getUnsafeEnv("DATABASE_HOST"),
port: parseInt(getUnsafeEnv("DATABASE_PORT")),
user: getUnsafeEnv("DATABASE_USER"),
password: getUnsafeEnv("DATABASE_PASSWORD"),
database: getUnsafeEnv("DATABASE_NAME"),
},
port: parseInt(getEnvOrDefault("PORT", DEFAULT_PORT)),
};
}
export function getEnvOrDefault(key: string, defaultValue: string): string {
const value = process.env[key];
if (value === undefined) {
return defaultValue;
}
return value;
}
export function getUnsafeEnv(key: string): string {
const value = process.env[key];
if (value === undefined) {
throw new Error(`Environment variable ${key} is not set`);
}
return value;
}

1
src/const.ts Normal file
View File

@ -0,0 +1 @@
export const DEFAULT_PORT = "3000";

View File

@ -0,0 +1,6 @@
export interface DatabaseInterface {
ping(): Promise<void>;
fetchAll<T = any>(sql: string, params?: any[]): Promise<T[]>;
fetchOne<T = any>(sql: string, params?: any[]): Promise<T | undefined>;
execute(sql: string, params?: any[]): Promise<void>;
}

View File

@ -0,0 +1,44 @@
import { Pool } from "pg";
import { DatabaseInterface } from "./DatabaseInterface";
export interface PgDatabaseOptions {
host: string;
port: number;
user: string;
password: string;
database: string;
}
export default class PgDatabase implements DatabaseInterface {
private readonly pool: Pool;
constructor(options: PgDatabaseOptions) {
this.pool = new Pool({
host: options.host,
port: options.port,
user: options.user,
password: options.password,
database: options.database,
});
}
async ping(): Promise<void> {
await this.pool.query("SELECT 1");
}
async fetchAll<T = any>(sql: string, params?: any[]): Promise<T[]> {
const res = await this.pool.query(sql, params);
return res.rows;
}
async fetchOne<T = any>(sql: string, params?: any[]): Promise<T | undefined> {
const res = await this.fetchAll<T>(sql, params);
return res[0];
}
async execute(sql: string, params?: any[]): Promise<void> {
await this.pool.query(sql, params);
}
}

View File

@ -0,0 +1,54 @@
import { Hono } from "hono";
import { AccountServiceInterface } from "../service/AccountServiceInterface";
export default function toRoutes(
accountService: AccountServiceInterface,
): Hono {
const app = new Hono();
app.post("/login", async (ctx) => {
try {
const { email, password } = await ctx.req.json();
const account = await accountService.login(email, password);
return ctx.json({
success: true,
accountId: account.id,
email: account.email,
});
} catch (error) {
return ctx.json(
{
success: false,
error: error instanceof Error ? error.message : "Erreur inconnue",
},
400,
);
}
});
app.post("/register", async (ctx) => {
try {
const { email, password } = await ctx.req.json();
const account = await accountService.createAccount(email, password);
return ctx.json({
success: true,
accountId: account.id,
email: account.email,
});
} catch (error) {
return ctx.json(
{
success: false,
error: error instanceof Error ? error.message : "Erreur inconnue",
},
400,
);
}
});
return app;
}

View File

@ -0,0 +1,45 @@
export class AccountEntity {
constructor(
public readonly id: string,
public readonly email: string,
private password: string,
public readonly roleId: number,
public readonly createdAt: Date,
public readonly updatedAt: Date,
) {
this.validateEmail(email);
this.validatePassword(password);
}
// Logique métier : validation de l'email
private validateEmail(email: string): void {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error("Format d'email invalide");
}
}
private validatePassword(password: string): void {
if (password.length < 8) {
throw new Error("Mot de passe trop court");
}
}
// Logique métier : vérification du mot de passe
public verifyPassword(plainPassword: string): boolean {
// Dans un vrai projet, on utiliserait bcrypt
return this.password === plainPassword;
}
// Factory method pour créer un nouveau compte
static create(email: string, password: string): AccountEntity {
const now = new Date();
const id = crypto.randomUUID();
return new AccountEntity(id, email, password, 1, now, now);
}
}
export type CreateAccountDto = {
email: string;
password: string;
};

View File

@ -0,0 +1,7 @@
import { AccountEntity } from "../entity/AccountEntity";
export interface AccountRepositoryInterface {
findByEmail(email: string): Promise<AccountEntity | null>;
save(account: AccountEntity): Promise<number>;
findById(id: string): Promise<AccountEntity | null>;
}

View File

@ -0,0 +1,26 @@
import { DatabaseInterface } from "../../../database/DatabaseInterface";
import { AccountEntity } from "../entity/AccountEntity";
import { AccountRepositoryInterface } from "./AccountRepositoryInterface";
export default class AccountRepository implements AccountRepositoryInterface {
constructor(private readonly database: DatabaseInterface) {}
async findByEmail(email: string): Promise<AccountEntity | null> {
// Implémentation simple pour l'exemple
// Dans un vrai projet, on ferait une requête à la base de données
console.log(`Recherche du compte avec email: ${email}`);
return null;
}
async save(account: AccountEntity): Promise<number> {
// Implémentation simple pour l'exemple
console.log(`Sauvegarde du compte: ${account.id}`);
return 1;
}
async findById(id: string): Promise<AccountEntity | null> {
// Implémentation simple pour l'exemple
console.log(`Recherche du compte avec ID: ${id}`);
return null;
}
}

View File

@ -0,0 +1,38 @@
import { AccountEntity } from "../entity/AccountEntity";
import { AccountRepositoryInterface } from "../repository/AccountRepositoryInterface";
import { AccountServiceInterface } from "./AccountServiceInterface";
export default class AccountService implements AccountServiceInterface {
constructor(private readonly accountRepository: AccountRepositoryInterface) {}
async login(email: string, password: string): Promise<AccountEntity> {
// Logique métier DDD : authentification
const account = await this.accountRepository.findByEmail(email);
if (!account) {
throw new Error("Compte non trouvé");
}
if (!account.verifyPassword(password)) {
throw new Error("Mot de passe incorrect");
}
return account;
}
async createAccount(email: string, password: string): Promise<AccountEntity> {
// Logique métier DDD : vérifier que l'email n'existe pas déjà
const existingAccount = await this.accountRepository.findByEmail(email);
if (existingAccount) {
throw new Error("Un compte avec cet email existe déjà");
}
// Utilisation de la factory method de l'entité
const newAccount = AccountEntity.create(email, password);
await this.accountRepository.save(newAccount);
return newAccount;
}
}

View File

@ -0,0 +1,6 @@
import { AccountEntity } from "../entity/AccountEntity";
export interface AccountServiceInterface {
login(email: string, password: string): Promise<AccountEntity>;
createAccount(email: string, password: string): Promise<AccountEntity>;
}

View File

@ -0,0 +1,12 @@
import { Hono } from "hono";
import { DatabaseInterface } from "../../database/DatabaseInterface";
import toRoutes from "./controller/AccountController";
import AccountRepository from "./repository/AccoutRepository";
import AccountService from "./service/AccountService";
export function setup(database: DatabaseInterface): Hono {
const accountRepository = new AccountRepository(database);
const accountService = new AccountService(accountRepository);
return toRoutes(accountService);
}