Ajout des fichiers de base pour le projet.
This commit is contained in:
17
src/app.ts
Normal file
17
src/app.ts
Normal 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
45
src/config.ts
Normal 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
1
src/const.ts
Normal file
@ -0,0 +1 @@
|
||||
export const DEFAULT_PORT = "3000";
|
6
src/database/DatabaseInterface.ts
Normal file
6
src/database/DatabaseInterface.ts
Normal 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>;
|
||||
}
|
44
src/database/PgDatabase.ts
Normal file
44
src/database/PgDatabase.ts
Normal 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);
|
||||
}
|
||||
}
|
54
src/domain/account/controller/AccountController.ts
Normal file
54
src/domain/account/controller/AccountController.ts
Normal 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;
|
||||
}
|
45
src/domain/account/entity/AccountEntity.ts
Normal file
45
src/domain/account/entity/AccountEntity.ts
Normal 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;
|
||||
};
|
@ -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>;
|
||||
}
|
26
src/domain/account/repository/AccoutRepository.ts
Normal file
26
src/domain/account/repository/AccoutRepository.ts
Normal 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;
|
||||
}
|
||||
}
|
38
src/domain/account/service/AccountService.ts
Normal file
38
src/domain/account/service/AccountService.ts
Normal 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;
|
||||
}
|
||||
}
|
6
src/domain/account/service/AccountServiceInterface.ts
Normal file
6
src/domain/account/service/AccountServiceInterface.ts
Normal 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>;
|
||||
}
|
12
src/domain/account/setup.ts
Normal file
12
src/domain/account/setup.ts
Normal 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);
|
||||
}
|
Reference in New Issue
Block a user