refont back express
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -40,3 +40,5 @@ testem.log
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
/backend/package-lock.json
|
||||
/backend/node_modules
|
||||
|
||||
7
backend/.env
Normal file
7
backend/.env
Normal file
@@ -0,0 +1,7 @@
|
||||
ALLOWED_ORIGIN=http://localhost:4300
|
||||
PORT=3333
|
||||
JWT_SECRET=d1bcf750e6246f1d75a2b32e3ff9ac7a8507b1d27462fc3223b966a86e3ef28762efcfc90a50e20b1888b1c8d7ce107260784b6d3ac9cb3662f499a7b272b1d926e3e0232de3581f25cde6e6da3dd0f62ee00fbe8bbfc3425c5abc0ad3b53f9b9c75875fe57629c9f1fe01ffb280d5605a54ab90ca15ae4cb6c43e298448e95bffd55582a16d18867d3c5db1cb316ba5fc9dfbdde1d8ef523a2d35f425aacd42286058801e79dc0b7c3fd3f9430ef3696e0fbcef5f28ba12ba3e38ee7e9b8f79d9ae51fb81f13528e9008d917d5be6145f3ab9a621dc89aef1f4df09bcdcce9f109f4f792623061ad1cfe541097fdf3695cfb72673ece58db49894d25f486e99
|
||||
DB_HOST=localhost
|
||||
DB_USER=eni
|
||||
DB_PASSWORD=gkAAUlq2e)0*ROLO
|
||||
DB_DATABASE=eni
|
||||
16
backend/config/cors.js
Normal file
16
backend/config/cors.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import dotenv from 'dotenv';
|
||||
import * as path from "node:path";
|
||||
|
||||
const envFile = `.env`;
|
||||
dotenv.config({ path: path.resolve(process.cwd(), envFile), override: true });
|
||||
|
||||
const allowedOrigin = process.env.ALLOWED_ORIGIN || 'http://localhost:4200';
|
||||
|
||||
const corsOptions = {
|
||||
origin: allowedOrigin,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true,
|
||||
};
|
||||
|
||||
export default corsOptions;
|
||||
13
backend/config/db.js
Normal file
13
backend/config/db.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import mariadb from 'mariadb';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const pool = mariadb.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_DATABASE
|
||||
});
|
||||
|
||||
export default pool;
|
||||
6
backend/controllers/apiController.js
Normal file
6
backend/controllers/apiController.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default function getConnect(req, res) {
|
||||
res.json({
|
||||
message: 'Connexion : OK',
|
||||
status: true
|
||||
});
|
||||
};
|
||||
143
backend/controllers/authController.js
Normal file
143
backend/controllers/authController.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import pool from "../config/db.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
// Création d'un utilisateur
|
||||
export const register = async (req, res) => {
|
||||
let conn;
|
||||
try {
|
||||
conn = await pool.getConnection();
|
||||
const { email, firstname, lastname, password_hash } = req.body;
|
||||
if (!email || !firstname || !lastname || !password_hash) {
|
||||
return res.status(400).json({
|
||||
message: 'Tout les champs sont requis !',
|
||||
status: false
|
||||
});
|
||||
}
|
||||
const existingUsers = await conn.query(
|
||||
'SELECT * FROM users WHERE email = ?',
|
||||
[email]
|
||||
);
|
||||
if (existingUsers.length > 0) {
|
||||
return res.status(409).json({
|
||||
message: 'L\'utilisateur existe déjà !',
|
||||
status: false
|
||||
});
|
||||
}
|
||||
// Hash du mot de passe
|
||||
const hashedPassword = await bcrypt.hash(password_hash, 10);
|
||||
const result = await conn.query(
|
||||
'INSERT INTO users (email, firstname, lastname, password_hash) VALUES (?, ?, ?, ?, ?)',
|
||||
[email, firstname, lastname, hashedPassword]
|
||||
);
|
||||
res.status(201).json({
|
||||
message: 'L\'utilisateur à bien été enregistré !',
|
||||
status: true
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Erreur interne :', err);
|
||||
return res.status(500).json({
|
||||
message: 'Erreur interne du serveur.',
|
||||
status: false
|
||||
});
|
||||
} finally {
|
||||
if (conn) await conn.release();
|
||||
}
|
||||
};
|
||||
|
||||
// Connexion
|
||||
export const login = async (req, res) => {
|
||||
let conn;
|
||||
try {
|
||||
conn = await pool.getConnection();
|
||||
const { email, password } = req.body;
|
||||
// Chercher l'utilisateur
|
||||
const users = await conn.query(
|
||||
'SELECT * FROM users WHERE email = ?',
|
||||
[email]
|
||||
);
|
||||
if (users.length === 0) {
|
||||
return res.status(401).json({
|
||||
message: 'Identifiant incorrect',
|
||||
status: false
|
||||
});
|
||||
}
|
||||
const user = users[0];
|
||||
// Verify password
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({
|
||||
message: 'Identifiant incorrect',
|
||||
status: false
|
||||
});
|
||||
}
|
||||
// Generate JWT token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
},
|
||||
process.env.JWT_SECRET || 'fallback_secret',
|
||||
{ expiresIn: '12h' }
|
||||
);
|
||||
await conn.query(
|
||||
'UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[user.id]
|
||||
);
|
||||
await setLogs(email + " s'est connecté !")
|
||||
res.cookie('jwt', token, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'Strict',
|
||||
maxAge: 12 * 60 * 60 * 1000
|
||||
});
|
||||
res.json({
|
||||
message: 'Connexion réussie',
|
||||
status: true
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Erreur interne :', err);
|
||||
return res.status(500).json({
|
||||
message: 'Erreur interne du serveur.',
|
||||
status: false
|
||||
});
|
||||
} finally {
|
||||
if (conn) await conn.release();
|
||||
}
|
||||
};
|
||||
|
||||
// Déconnecter l'utilisateur
|
||||
export const logout = (req, res) => {
|
||||
res.clearCookie('jwt', {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'Strict',
|
||||
});
|
||||
res.status(200).json({
|
||||
message: 'Successfully logged out',
|
||||
status: true
|
||||
});
|
||||
};
|
||||
|
||||
// Vérification de la connexion sur la page pour guard angular
|
||||
export const checkAuth = (req, res) => {
|
||||
const token = req.cookies.jwt;
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
message: 'Non identifié',
|
||||
status: false
|
||||
});
|
||||
}
|
||||
try {
|
||||
req.user = jwt.verify(token, process.env.JWT_SECRET);
|
||||
res.status(200).json({
|
||||
message: 'identifié',
|
||||
status: true
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Erreur interne :', err);
|
||||
return res.status(500).json({
|
||||
message: 'Erreur interne du serveur.',
|
||||
status: false
|
||||
});
|
||||
}
|
||||
};
|
||||
24
backend/middleware/apiKeyMiddleware.js
Normal file
24
backend/middleware/apiKeyMiddleware.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export const apiKeyMiddleware = async (req, res, next) => {
|
||||
const authHeader = req.header('Authorization');
|
||||
if (!authHeader) {
|
||||
return res.status(403).json({
|
||||
message: 'Forbidden: No API Key provided',
|
||||
status: false
|
||||
});
|
||||
}
|
||||
try {
|
||||
if (authHeader !== "9IgFg8cnUS4XJE7Q91A0XjrWnjbnBhdk98jcI6fV1n6NAEYz31SHicge8Vkq0bCGvfKsjylb19ouri6FFUeNC1PgPvwrNCC3G5jcz4PLInlFanzf47hCsBJw4IXuhNHC"){
|
||||
return res.status(403).json({
|
||||
message: 'Forbidden: Invalid API Key',
|
||||
status: false
|
||||
});
|
||||
}
|
||||
next();
|
||||
} catch (err) {
|
||||
console.error('Erreur interne :', err);
|
||||
return res.status(500).json({
|
||||
message: 'Erreur interne du serveur.',
|
||||
status: false
|
||||
});
|
||||
}
|
||||
};
|
||||
25
backend/middleware/tokenJWTMiddleware.js
Normal file
25
backend/middleware/tokenJWTMiddleware.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export const verifyToken = async (req, res, next) => {
|
||||
// Récupérer le token depuis le cookie 'jxwt'
|
||||
const token = req.cookies['jwt'];
|
||||
|
||||
if (!token) {
|
||||
return res.status(403).json({
|
||||
message: 'Token is required',
|
||||
status: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Vérifier le token
|
||||
jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret', (err, decoded) => {
|
||||
if (err) {
|
||||
return res.status(401).json({
|
||||
message: 'Invalid or expired token',
|
||||
status: false
|
||||
});
|
||||
}
|
||||
req.user = decoded;
|
||||
next();
|
||||
});
|
||||
};
|
||||
24
backend/package.json
Normal file
24
backend/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "api_eni_angular",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "API ENI Angular",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"author": "Johan Leroy",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mariadb": "^3.4.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"url": "^0.11.4",
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
}
|
||||
53
backend/routes/apiRoutes.js
Normal file
53
backend/routes/apiRoutes.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import apiController from "../controllers/apiController.js";
|
||||
import {
|
||||
register,
|
||||
login,
|
||||
checkAuth,
|
||||
logout,
|
||||
} from "../controllers/authController.js";
|
||||
import {verifyToken} from "../middleware/tokenJWTMiddleware.js";
|
||||
import multer from "multer";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import path from "node:path";
|
||||
|
||||
router.get('', apiController);
|
||||
|
||||
// Upload
|
||||
const generateFileName = (originalName) => {
|
||||
const ext = path.extname(originalName);
|
||||
const now = new Date();
|
||||
const timestamp = now.getFullYear().toString() +
|
||||
String(now.getMonth() + 1).padStart(2, '0') +
|
||||
String(now.getDate()).padStart(2, '0') +
|
||||
String(now.getHours()).padStart(2, '0') +
|
||||
String(now.getMinutes()).padStart(2, '0') +
|
||||
String(now.getSeconds()).padStart(2, '0');
|
||||
const uuid = uuidv4();
|
||||
return `${timestamp}_${uuid}${ext}`;
|
||||
};
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, 'uploads/'); // Stockage des fichiers dans le dossier uploads/
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
cb(null, generateFileName(file.originalname));
|
||||
}
|
||||
});
|
||||
const upload = multer({ storage: storage });
|
||||
router.post('/upload', verifyToken, upload.single('file'), (req, res) => {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ message: 'Aucun fichier fourni' });
|
||||
}
|
||||
const filePath = `/uploads/${req.file.filename}`;
|
||||
res.json({ filePath });
|
||||
});
|
||||
|
||||
// Authentification et utilisateur
|
||||
router.post('/auth/register', register);
|
||||
router.post('/auth/login', login);
|
||||
router.post('/auth/logout', logout);
|
||||
router.get('/auth/check-auth', verifyToken, checkAuth);
|
||||
|
||||
export default router;
|
||||
82
backend/server.js
Normal file
82
backend/server.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import apiRoutes from './routes/apiRoutes.js';
|
||||
import corsOptions from './config/cors.js'
|
||||
import {apiKeyMiddleware} from './middleware/apiKeyMiddleware.js';
|
||||
import dotenv from 'dotenv';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import pool from "./config/db.js";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const envFile = `.env`;
|
||||
dotenv.config({ path: path.resolve(process.cwd(), envFile), override: true });
|
||||
|
||||
const app = express();
|
||||
|
||||
// Utiliser CORS
|
||||
app.use(cors(corsOptions));
|
||||
|
||||
// Utilisation des cookies
|
||||
app.use(cookieParser());
|
||||
|
||||
// Middleware pour parser les corps de requêtes JSON
|
||||
app.use(express.json());
|
||||
|
||||
// Utilisation des fichiers/images
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
|
||||
// Middleware pour vérifier la clé API (Authorization)
|
||||
app.use('/', apiKeyMiddleware);
|
||||
|
||||
// Utiliser les routes API
|
||||
app.use('/', apiRoutes);
|
||||
|
||||
// Middleware pour les routes non trouvées
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({
|
||||
message: 'Forbidden: Invalid Request',
|
||||
status: false
|
||||
});
|
||||
});
|
||||
|
||||
// Test de conenxion à la base de donnée
|
||||
const testDatabaseConnection = async () => {
|
||||
let conn;
|
||||
try {
|
||||
conn = await pool.getConnection();
|
||||
console.log('✅ Connexion à la base de données réussie ! 🎉');
|
||||
} catch (err) {
|
||||
console.error('❌ Erreur de connexion à la base de données :', err.message);
|
||||
throw new Error('⛔ Impossible de se connecter à la base de données.');
|
||||
} finally {
|
||||
if (conn) await conn.end();
|
||||
}
|
||||
};
|
||||
|
||||
// Lancer le serveur
|
||||
const startServer = async () => {
|
||||
while (true) {
|
||||
try {
|
||||
console.log(`🚀 Lancement de l'API en cours... 🔧`);
|
||||
await testDatabaseConnection();
|
||||
app.listen(process.env.PORT, () => {
|
||||
console.log(`📢 NODE_ENV = ${process.env.NODE_ENV} 🌍`);
|
||||
console.log(`📢 Chargement du fichier : ${envFile} 📄`);
|
||||
console.log(`🚀 API démarrée sur le port ${process.env.PORT} 🎯`);
|
||||
console.log(`🌍 Allowed Origin : ${process.env.ALLOWED_ORIGIN} 🔗`);
|
||||
console.log(`✅ L'API est lancée et prête à l'emploi ! 🎉`);
|
||||
});
|
||||
break;
|
||||
} catch (err) {
|
||||
console.error('❌ Le serveur n’a pas pu démarrer :', err.message);
|
||||
console.log('⏳ Nouvelle tentative de démarrage dans 5 secondes... 🔄');
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
startServer();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"start": "ng serve --port=4300",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
|
||||
@@ -22,12 +22,6 @@
|
||||
<p class="block mr-auto font-sans text-base antialiased font-normal leading-relaxed text-blue-gray-900">
|
||||
Ma bibliothèque
|
||||
</p>
|
||||
<span class="ml-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5"
|
||||
stroke="currentColor" aria-hidden="true" class="w-4 h-4 mx-auto transition-transform rotate-180">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<app-sidbar />
|
||||
<router-outlet />
|
||||
<div class="flex flex-row">
|
||||
<app-sidbar></app-sidbar>
|
||||
<div class="flex-1">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<section class="text-gray-600 body-font">
|
||||
<div class="container px-5 py-24 mx-auto">
|
||||
<div class="flex flex-wrap -m-4">
|
||||
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||
<a class="block relative h-48 rounded overflow-hidden">
|
||||
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/420x260">
|
||||
</a>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||
<h2 class="text-gray-900 title-font text-lg font-medium">The Catalyzer</h2>
|
||||
<p class="mt-1">$16.00</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||
<a class="block relative h-48 rounded overflow-hidden">
|
||||
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/421x261">
|
||||
</a>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||
<h2 class="text-gray-900 title-font text-lg font-medium">Shooting Stars</h2>
|
||||
<p class="mt-1">$21.15</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||
<a class="block relative h-48 rounded overflow-hidden">
|
||||
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/422x262">
|
||||
</a>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||
<h2 class="text-gray-900 title-font text-lg font-medium">Neptune</h2>
|
||||
<p class="mt-1">$12.00</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||
<a class="block relative h-48 rounded overflow-hidden">
|
||||
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/423x263">
|
||||
</a>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||
<h2 class="text-gray-900 title-font text-lg font-medium">The 400 Blows</h2>
|
||||
<p class="mt-1">$18.40</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||
<a class="block relative h-48 rounded overflow-hidden">
|
||||
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/424x264">
|
||||
</a>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||
<h2 class="text-gray-900 title-font text-lg font-medium">The Catalyzer</h2>
|
||||
<p class="mt-1">$16.00</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||
<a class="block relative h-48 rounded overflow-hidden">
|
||||
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/425x265">
|
||||
</a>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||
<h2 class="text-gray-900 title-font text-lg font-medium">Shooting Stars</h2>
|
||||
<p class="mt-1">$21.15</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||
<a class="block relative h-48 rounded overflow-hidden">
|
||||
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/427x267">
|
||||
</a>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||
<h2 class="text-gray-900 title-font text-lg font-medium">Neptune</h2>
|
||||
<p class="mt-1">$12.00</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
|
||||
<a class="block relative h-48 rounded overflow-hidden">
|
||||
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/428x268">
|
||||
</a>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
|
||||
<h2 class="text-gray-900 title-font text-lg font-medium">The 400 Blows</h2>
|
||||
<p class="mt-1">$18.40</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
0
src/app/public/pages/search/search.component.css
Normal file
0
src/app/public/pages/search/search.component.css
Normal file
0
src/app/public/pages/search/search.component.html
Normal file
0
src/app/public/pages/search/search.component.html
Normal file
19
src/app/public/pages/search/search.component.ts
Normal file
19
src/app/public/pages/search/search.component.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {ReactiveFormsModule} from '@angular/forms';
|
||||
import {NgForOf, NgIf} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search',
|
||||
imports: [
|
||||
NgIf,
|
||||
NgForOf,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
templateUrl: './search.component.html',
|
||||
styleUrl: './search.component.css'
|
||||
})
|
||||
export class SearchComponent {
|
||||
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user