Ajout des fichiers de base pour le projet.
This commit is contained in:
7
.devcontainer/Dockerfile
Normal file
7
.devcontainer/Dockerfile
Normal 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
|
13
.devcontainer/devcontainer.json
Normal file
13
.devcontainer/devcontainer.json
Normal 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
14
.oxlintrc.json
Normal 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
3
.prettierrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"plugins": ["@prettier/plugin-oxc"]
|
||||
}
|
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal 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
1692
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal 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
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);
|
||||
}
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user