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

7
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,7 @@
FROM node:24
WORKDIR /app
RUN apt update &&\
apt install git curl unzip -y &&\
curl -fsSL https://bun.sh/install | bash

View File

@ -0,0 +1,13 @@
{
"workspaceFolder": "/workspace",
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,Z",
"runArgs": ["--network=dev-network"],
"build": {
"dockerfile": "Dockerfile"
},
"customizations": {
"vscode": {
"extensions": ["oxc.oxc-vscode", "esbenp.prettier-vscode"]
}
}
}

14
.oxlintrc.json Normal file
View File

@ -0,0 +1,14 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["unicorn", "typescript", "oxc"],
"categories": {
"correctness": "error",
"perf": "error",
"suspicious": "off",
"style": "off",
"nursery": "off",
"pedantic": "off",
"restriction": "off"
},
"ignorePatterns": ["node_modules/", "dist/"]
}

3
.prettierrc Normal file
View File

@ -0,0 +1,3 @@
{
"plugins": ["@prettier/plugin-oxc"]
}

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"editor.tabSize": 2,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.tsdk": "node_modules/typescript/lib"
}

1692
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "nixi-api",
"version": "1.0.0",
"description": "",
"repository": {
"type": "git",
"url": "https://gitea.qpismont.fr/qpismont/nixi-api"
},
"license": "MIT",
"author": "qpismont",
"type": "module",
"main": "dist/app.js",
"scripts": {
"dev": "tsx watch src/app.ts",
"lint": "oxlint .",
"type": "tsc --noEmit",
"build": "rolldown src/app.ts --file dist/app.js --platform node --format esm --minify"
},
"devDependencies": {
"@prettier/plugin-oxc": "^0.0.4",
"@types/node": "^24.2.1",
"@types/pg": "^8.15.5",
"oxlint": "^1.11.1",
"prettier": "3.6.2",
"rolldown": "^1.0.0-beta.32",
"tsx": "^4.20.3",
"typescript": "^5.9.2"
},
"dependencies": {
"@hono/node-server": "^1.18.1",
"hono": "^4.9.0",
"pg": "^8.16.3",
"zod": "^4.0.17"
}
}

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);
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "bundler",
"lib": ["esnext"],
"types": ["node"],
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"isolatedModules": true,
"skipLibCheck": true
}
}