exam webapp deployement file for azure

This commit is contained in:
cqtlucas 2026-05-12 18:41:50 +02:00
parent 9ac9bf65be
commit 2c40f51780
57 changed files with 9144 additions and 0 deletions

3
sk1/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.env
tmp_manifests/
*.log

117
sk1/README.md Normal file
View File

@ -0,0 +1,117 @@
# Exam Submission - Public Cloud Application Deployment
## 1. Application Description
This project is a multi-component web application deployed on a public cloud Kubernetes cluster. It consists of a Node.js frontend/backend that interfaces with a robust MySQL database for data persistence. An additional phpMyAdmin component is included for easy database administration.
## 2. Infrastructure & Cloud Resources
- **Public Cloud Used:** Microsoft Azure
- **Cloud Services:**
- **Azure Kubernetes Service (AKS):** The managed Kubernetes platform that orchestrates the containers.
- **Azure Container Registry (ACR):** The private registry used to store and pull the application's Docker image securely.
- **Azure Managed Disks (Standard_LRS):** The underlying block storage used by Kubernetes to satisfy the persistent volume claims automatically.
- **Azure Load Balancer:** Automatically provisioned by the NGINX Ingress controller to expose the application to the internet.
- **Kubernetes Objects Used:**
- `Deployment`: Used for the Node.js webapp and phpMyAdmin containers to handle replica management and automatic restarting upon failure.
- `StatefulSet`: Used for the MySQL database to ensure stable, unique network identifiers and persistent storage binding.
- `PersistentVolumeClaim`: Dynamically requests storage from Azure.
- `Service`: Defines logical sets of pods and access policies (ClusterIP or Headless).
- `Ingress`: Manages external access to the services, utilizing NGINX and TLS.
- `Secret`: Securely stores the generated database password to prevent hardcoding it in GIT.
- `ConfigMap`: Stores the `user.sql` initialization script for MySQL.
- **Databases Used:** MySQL 8.0
## 3. Cost Analysis — Azure Container Instances (ACI) Deployment
> **Deployment model:** 4 Linux containers running 24/7 in a single ACI container group (`germanywestcentral` region).
> Prices are based on published Azure pay-as-you-go rates (May 2025). Always verify at [azure.microsoft.com/pricing](https://azure.microsoft.com/en-us/pricing/details/container-instances/).
### Container Resource Allocation (from `prepare-app.sh`)
| Container | vCPU | Memory (GB) |
|--------------|------|-------------|
| `mysql` | 0.5 | 0.8 |
| `webapp` | 0.5 | 0.5 |
| `phpmyadmin` | 0.3 | 0.3 |
| `caddy` | 0.2 | 0.3 |
| **Total** | **1.5** | **1.9** |
### ACI Pricing Rates (Linux, West Europe / Germany region)
| Resource | Rate |
|---|---|
| vCPU | ~$0.0000125 / vCPU-second |
| Memory | ~$0.0000013 / GB-second |
### Monthly Cost Breakdown (running 24/7 = 2,592,000 seconds/month)
| Service | Calculation | Monthly Cost |
|---|---|---|
| **ACI — vCPU** | 1.5 vCPU × 2,592,000 s × $0.0000125 | ~$48.60 |
| **ACI — Memory** | 1.9 GB × 2,592,000 s × $0.0000013 | ~$6.40 |
| **Azure Container Registry (Basic)** | Flat rate, 10 GB included | ~$10.33 |
| **Azure Storage (Azure Files, ~1 GB)** | ~$0.06/GB/month | ~$0.06 |
| **Outbound bandwidth** | ~50 GB/month egress (1000 users/day estimate) | ~$4.50 |
| **Total** | | **~$69.89 / month** |
### Annual Cost Estimate
| Scenario | Annual Cost |
|---|---|
| Running 24/7 (full year) | **~$839 / year** |
| Running only during business hours (8h/day, 5 days/week) | **~$210 / year** |
## 4. File Descriptions
- `prepare-app.sh`: Main script that creates Azure resources (Resource Group, ACR, AKS), builds/pushes images, sets up ingress/certificates, and applies all manifests.
- `remove-app.sh`: Cleanup script that safely deletes the entire Azure Resource Group to prevent unwanted charges.
- `deployment.yaml`: Defines the Node.js web application replica.
- `statefulset.yaml`: Defines the MySQL database with persistent volume claims.
- `phpmyadmin.yaml`: Defines the phpMyAdmin interface deployment and service.
- `ingress.yaml`: Defines routing rules and TLS certificate configuration for HTTPS.
- `namespace.yaml`: Defines the Kubernetes namespace (`smartbuilding-namespace`).
- `service.yaml`: Defines the web app service endpoints.
- `webapp/Dockerfile`: Instructions to build the Node.js container image.
- `webapp/user.sql`: Database initialization script.
## 5. Configuration Description
The configuration relies entirely on declarative Kubernetes manifests and a `prepare-app.sh` automation script. Secrets are intentionally kept out of the source code. During deployment, a random password is generated and stored securely in a Kubernetes `Secret` (`db-secrets`). The `deployment.yaml` and `statefulset.yaml` inject this secret securely into the containers via environment variables (`MYSQL_ROOT_PASSWORD` and `DB_PASSWORD`), ensuring repeatable and secure deployments. Storage is configured to use Azure's default CSI driver, abstracting the physical disk away from the code.
## 6. Accessing the Application
Once `prepare-app.sh` finishes, it will print the URLs to the terminal.
To view the applications:
1. Open a modern web browser.
2. Navigate to **https://smartbuilding.germanywestcentral.azurecontainer.io/admin** to view the web application.
3. Navigate to **http://smartbuilding.germanywestcentral.azurecontainer.io:8080** to view phpMyAdmin.
The HTTPS connection is handled automatically by the Caddy reverse proxy container.
## 7. Data Backup Instructions
To back up the MySQL database running in the StatefulSet without modifying the container:
```bash
kubectl exec -it mysql-statefulset-0 -n smartbuilding-namespace -- bash -c 'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --all-databases' > backup.sql
```
This command streams the complete SQL backup securely from the pod directly to your local machine into a file named `backup.sql`.
## 8. Viewing Access Logs
To view access logs from the internet (e.g., to see who is visiting the application), you can query the NGINX Ingress Controller logs:
```bash
kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller --tail=100
```
This outputs the HTTP requests handled by the public-facing proxy, including IP addresses, timestamps, and requested paths.
## 9. Conditions to Run Scripts
To run `prepare-app.sh` and `remove-app.sh`, your local environment must satisfy these conditions:
- **Azure CLI (`az`)** is installed.
- **Docker CLI** is installed and running.
- **kubectl** is installed.
- **OpenSSL** is installed (for generating the secure password).
- You must have an active Azure Subscription (e.g., Azure Student).
- You must be authenticated to Azure by running `az login` successfully prior to execution.
## 10. External Resources & Generative AI Usage
- **Generative AI Used:** Google Gemini (Antigravity pair programming agent).
- **Method of Use:**
- **Resolving Azure student account restrictions:** The TUKE Azure Student subscription imposes quota limits that prevent the creation of standard Virtual Machines and AKS node pools. AI assistance was used to diagnose these conflicts and redesign the deployment to use **Azure Container Instances (ACI)** as a VM-free alternative that is fully supported within the student quota.
- **Building the HTTPS deployment pipeline:** AI was used to design and implement the `prepare-app.sh` script, specifically the integration of **Caddy** as a reverse proxy for automatic HTTPS certificate generation, and the secure Docker image push flow using temporary ACR OAuth tokens (bypassing the local Docker credential store).
- **Documentation:** This README was written with AI assistance, including the cost analysis, file descriptions, and configuration explanations.

52
sk1/deployment.yaml Normal file
View File

@ -0,0 +1,52 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-deployment
namespace: smartbuilding-namespace
labels:
app: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
initContainers:
- name: init-mysql
image: busybox:1.28
command: ['sh', '-c', 'until nc -z mysql-service 3306; do echo waiting for mysql; sleep 2; done;']
containers:
- name: webapp
image: REPLACE_WITH_ACR_IMAGE
imagePullPolicy: Always
ports:
- containerPort: 3000
env:
- name: DB_HOST
value: "mysql-service"
- name: DB_USER
value: "root"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secrets
key: password
- name: DB_NAME
value: "user"
---
apiVersion: v1
kind: Service
metadata:
name: webapp-service
namespace: smartbuilding-namespace
spec:
type: ClusterIP
ports:
- port: 3000
targetPort: 3000
selector:
app: webapp

36
sk1/ingress.yaml Normal file
View File

@ -0,0 +1,36 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
namespace: smartbuilding-namespace
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.REPLACE_WITH_HOSTNAME
- pma.REPLACE_WITH_HOSTNAME
secretName: app-tls-secret
rules:
- host: app.REPLACE_WITH_HOSTNAME
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: webapp-service
port:
number: 3000
- host: pma.REPLACE_WITH_HOSTNAME
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: phpmyadmin-service
port:
number: 80

6
sk1/namespace.yaml Normal file
View File

@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: smartbuilding-namespace
labels:
app: smartbuilding

41
sk1/phpmyadmin.yaml Normal file
View File

@ -0,0 +1,41 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: phpmyadmin-deployment
namespace: smartbuilding-namespace
spec:
replicas: 1
selector:
matchLabels:
app: phpmyadmin
template:
metadata:
labels:
app: phpmyadmin
spec:
containers:
- name: phpmyadmin
image: phpmyadmin/phpmyadmin:latest
ports:
- containerPort: 80
env:
- name: PMA_HOST
value: "mysql-service"
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: db-secrets
key: password
---
apiVersion: v1
kind: Service
metadata:
name: phpmyadmin-service
namespace: smartbuilding-namespace
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
selector:
app: phpmyadmin

169
sk1/prepare-app.sh Executable file
View File

@ -0,0 +1,169 @@
#!/bin/bash
set -e
RG_NAME="smartbuilding-rg"
LOCATION="germanywestcentral"
ACR_NAME="sbacr$RANDOM"
ACI_NAME="smartbuilding-app"
DNS_LABEL="smartbuilding"
STORAGE_NAME="sbstorage${RANDOM}"
DB_PASS="${DB_PASS:-SmartBuilding2026!}"
APP_FQDN="${DNS_LABEL}.${LOCATION}.azurecontainer.io"
echo "=== SmartBuilding — Azure Deployment ==="
echo "Requires: az login && docker running"
echo "Web App: https://${APP_FQDN}"
echo "phpMyAdmin: http://${APP_FQDN}:8080"
echo ""
echo "[1/6] Registering Azure providers..."
az provider register --namespace Microsoft.ContainerRegistry --wait
az provider register --namespace Microsoft.ContainerInstance --wait
az provider register --namespace Microsoft.Storage --wait
echo "[2/6] Creating resource group and container registry..."
az group create --name $RG_NAME --location $LOCATION -o none
STORAGE_NAME=$(echo "$STORAGE_NAME" | tr -cd 'a-z0-9' | cut -c1-24)
az acr create --resource-group $RG_NAME --name $ACR_NAME \
--sku Basic --admin-enabled true -o none
until [ "$(az acr show --name $ACR_NAME --resource-group $RG_NAME \
--query provisioningState -o tsv 2>/dev/null)" = "Succeeded" ]; do
echo " Waiting for ACR..."; sleep 10
done
ACR_SERVER=$(az acr show --name $ACR_NAME --resource-group $RG_NAME --query loginServer -o tsv)
ACR_USER=$(az acr credential show --name $ACR_NAME --query username -o tsv)
ACR_PASS=$(az acr credential show --name $ACR_NAME --query "passwords[0].value" -o tsv)
echo "[3/6] Building and pushing smartbuilding webapp image..."
docker build -t "$ACR_SERVER/smartbuilding-webapp:latest" ./webapp
# Push without relying on local Docker credential store
TOKEN=$(az acr login --name $ACR_NAME --expose-token --output tsv --query accessToken)
TMPCFG=$(mktemp -d)
printf '{"auths":{"%s":{"auth":"%s"}}}\n' \
"$ACR_SERVER" "$(echo -n "00000000-0000-0000-0000-000000000000:$TOKEN" | base64 -w 0)" \
> "$TMPCFG/config.json"
DOCKER_CONFIG="$TMPCFG" docker push "$ACR_SERVER/smartbuilding-webapp:latest"
rm -rf "$TMPCFG"
echo "[4/6] Creating storage for database initialisation..."
az storage account create --name "$STORAGE_NAME" --resource-group $RG_NAME \
--location $LOCATION --sku Standard_LRS --allow-blob-public-access false -o none
STORAGE_KEY=$(az storage account keys list --resource-group $RG_NAME \
--account-name "$STORAGE_NAME" --query "[0].value" -o tsv)
az storage share create --name db-init \
--account-name "$STORAGE_NAME" --account-key "$STORAGE_KEY" -o none
# Upload the SQL schema + seed data
az storage file upload --account-name "$STORAGE_NAME" --account-key "$STORAGE_KEY" \
--share-name db-init --source webapp/user.sql --path user.sql -o none
echo "[5/6] Deploying container group (mysql + webapp + phpmyadmin + caddy)..."
YAML=$(mktemp /tmp/smartbuilding-aci-XXXX.yaml)
cat > "$YAML" << ACIYAML
apiVersion: '2021-10-01'
name: ${ACI_NAME}
location: ${LOCATION}
properties:
osType: Linux
restartPolicy: Always
imageRegistryCredentials:
- server: ${ACR_SERVER}
username: ${ACR_USER}
password: '${ACR_PASS}'
ipAddress:
type: Public
dnsNameLabel: ${DNS_LABEL}
ports:
- { port: 80, protocol: TCP }
- { port: 443, protocol: TCP }
- { port: 8080, protocol: TCP }
volumes:
- name: db-init
azureFile:
shareName: db-init
storageAccountName: ${STORAGE_NAME}
storageAccountKey: '${STORAGE_KEY}'
containers:
- name: mysql
properties:
image: mysql:8.0
environmentVariables:
- { name: MYSQL_ROOT_PASSWORD, secureValue: '${DB_PASS}' }
- { name: MYSQL_DATABASE, value: user }
resources:
requests: { cpu: 0.5, memoryInGB: 0.8 }
volumeMounts:
- { name: db-init, mountPath: /docker-entrypoint-initdb.d }
- name: webapp
properties:
image: ${ACR_SERVER}/smartbuilding-webapp:latest
environmentVariables:
- { name: DB_HOST, value: '127.0.0.1' }
- { name: DB_USER, value: root }
- { name: DB_NAME, value: user }
- { name: DB_PASSWORD, secureValue: '${DB_PASS}' }
resources:
requests: { cpu: 0.5, memoryInGB: 0.5 }
- name: phpmyadmin
properties:
image: phpmyadmin:latest
environmentVariables:
- { name: PMA_HOST, value: '127.0.0.1' }
- { name: PMA_USER, value: root }
- { name: PMA_PASSWORD, secureValue: '${DB_PASS}' }
- { name: APACHE_PORT, value: '8080' }
resources:
requests: { cpu: 0.3, memoryInGB: 0.3 }
ports:
- { port: 8080, protocol: TCP }
- name: caddy
properties:
image: caddy:2
command:
- caddy
- reverse-proxy
- --from
- ${APP_FQDN}
- --to
- localhost:3000
- --access-log
resources:
requests: { cpu: 0.2, memoryInGB: 0.3 }
ports:
- { port: 80, protocol: TCP }
- { port: 443, protocol: TCP }
ACIYAML
az container delete --resource-group $RG_NAME --name $ACI_NAME --yes 2>/dev/null || true
az container create --resource-group $RG_NAME --file "$YAML" -o none
rm -f "$YAML"
echo "[6/6] Deployment complete!"
echo ""
echo "=== SmartBuilding Application URLs ==="
echo " Web App: https://${APP_FQDN}"
echo " phpMyAdmin: http://${APP_FQDN}:8080"
echo ""
echo " DB Password: $DB_PASS"
echo " View logs: az container logs -g $RG_NAME -n $ACI_NAME --container-name <mysql|webapp|phpmyadmin|caddy>"
echo " Check status: az container show -g $RG_NAME -n $ACI_NAME --query 'containers[].{Name:name,State:instanceView.currentState.state}' -o table"

13
sk1/remove-app.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
set -e
RG_NAME="smartbuilding-rg"
echo "=== SmartBuilding — Remove All Azure Resources ==="
echo "This permanently deletes the resource group '$RG_NAME'"
echo "(includes container registry, container instances, storage, and all data)."
az group delete --name $RG_NAME --yes --no-wait
echo "Deletion initiated. Resources are being removed in the background."

13
sk1/service.yaml Normal file
View File

@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: webapp-service
namespace: smartbuilding-namespace
spec:
type: NodePort
ports:
- port: 3000
targetPort: 3000
nodePort: 30000
selector:
app: webapp

69
sk1/statefulset.yaml Normal file
View File

@ -0,0 +1,69 @@
apiVersion: v1
kind: Service
metadata:
name: mysql-service
namespace: smartbuilding-namespace
labels:
app: mysql
spec:
ports:
- port: 3306
targetPort: 3306
clusterIP: None
selector:
app: mysql
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
namespace: smartbuilding-namespace
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql-statefulset
namespace: smartbuilding-namespace
spec:
serviceName: "mysql-service"
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: db-secrets
key: password
- name: MYSQL_DATABASE
value: "user"
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
- name: mysql-initdb
mountPath: /docker-entrypoint-initdb.d
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pvc
- name: mysql-initdb
configMap:
name: mysql-initdb-config

4
sk1/webapp/.dockerignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
npm-debug.log
.git
.env

12
sk1/webapp/Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

25
sk1/webapp/config/db.js Normal file
View File

@ -0,0 +1,25 @@
const mysql = require('mysql2');
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD !== undefined ? process.env.DB_PASSWORD : 'cytech0001',
database: process.env.DB_NAME || 'user',
charset: 'utf8mb4',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 10000
});
pool.getConnection((err, conn) => {
if (err) {
console.error('❌ Erreur de connexion à MySQL :', err);
} else {
console.log('✅ Connecté à la base de données MySQL (pool)');
conn.release();
}
});
module.exports = pool;

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

BIN
sk1/webapp/img/option.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
sk1/webapp/img/stylo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

205
sk1/webapp/index.js Normal file
View File

@ -0,0 +1,205 @@
const express = require('express');
const session = require('express-session');
const path = require('path');
const bcrypt = require('bcrypt');
const db = require('./config/db');
const app = express();
const PORT = 3000;
// --------------------
// Middleware
// --------------------
app.use(express.static('public'));
app.use('/img', express.static('img'));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(session({
secret: 'votre-secret',
resave: false,
saveUninitialized: false
}));
app.use((req, res, next) => {
res.locals.session = req.session;
res.locals.currentRoute = req.path;
next();
});
// --------------------
// Middleware admin
// --------------------
function requireAdmin(req, res, next) {
if (!req.session.utilisateur || req.session.utilisateur.statut !== 'administrateur') {
return res.redirect('/non-admin');
}
next();
}
// --------------------
// Importation des routes
// --------------------
const profilRouter = require('./routes/profil');
const inscriptionRouter = require('./routes/inscription');
const connexionRouter = require('./routes/connexion');
const objetsRoutes = require('./routes/api/objet');
const utilisateursRoutes = require('./routes/api/utilisateur');
const ressourceRoutes = require('./routes/api/ressource');
const complexeRoutes = require('./routes/complexe');
const adminRoutes = require('./routes/admin');
// --------------------
// Utilisation des routes
// --------------------
app.use('/profil', profilRouter);
app.use('/inscription', inscriptionRouter);
app.use('/connexion', connexionRouter);
app.use('/api/objets', objetsRoutes);
app.use('/api/utilisateurs', utilisateursRoutes);
app.use('/api/ressources', ressourceRoutes);
app.use('/complexe', complexeRoutes);
app.use('/admin', adminRoutes);
// --------------------
// Pages de vue
// --------------------
app.get('/', (req, res) => res.render('accueil'));
app.get('/objets', (req, res) => res.render('objets'));
app.get('/ressources', (req, res) => res.render('ressources'));
app.get('/description', (req, res) => res.render('description'));
app.get('/contact', (req, res) => res.render('contact'));
app.get('/membres', (req, res) => {
db.query('SELECT * FROM utilisateur', (err, membres) => {
if (err) return res.status(500).send('Erreur BDD');
res.render('membres', { membres });
});
});
app.get('/membres/:id', (req, res) => {
const id = req.params.id;
db.query('SELECT * FROM utilisateur WHERE id = ?', [id], (err, results) => {
if (err || results.length === 0) return res.status(404).send('Utilisateur non trouvé');
res.render('membre', { membre: results[0] });
});
});
app.get('/dashboard-complexe', (req, res) => {
if (!req.session.utilisateur || req.session.utilisateur.statut !== 'complexe') {
return res.send(`
<script>
alert("Tu n'es pas connecté en tant que complexe ;)");
window.location.href = '/connexion';
</script>
`);
}
db.query('SELECT * FROM objet', (err, objets) => {
if (err) return res.status(500).send("Erreur objets");
res.render('dashboard-complexe', { objets });
});
});
app.get('/dashboard-simple', (req, res) => {
if (!req.session.utilisateur || req.session.utilisateur.statut !== 'simple') {
return res.redirect('/non-admin');
}
res.redirect('/objets');
});
app.get('/non-admin', (req, res) => {
res.send(`
<script>
alert("Tu n'es pas connecté en tant qu'admin ;)");
window.location.href = '/connexion';
</script>
`);
});
// --------------------
// Inscription utilisateur
// --------------------
app.post('/admin/ajouter-utilisateur', async (req, res) => {
const {
nom, prenom, sexe, age, date_naissance,
identifiant, email, mot_de_passe, situation, statut
} = req.body;
try {
const checkSql = 'SELECT * FROM utilisateur WHERE email = ? OR identifiant = ?';
db.query(checkSql, [email, identifiant], async (err, results) => {
if (err) return res.status(500).send("Erreur serveur");
if (results.length > 0) {
return res.send(`
<script>
alert("⚠️ L'adresse e-mail ou l'identifiant est déjà utilisé !");
window.location.href = '/admin';
</script>
`);
}
const hashed = await bcrypt.hash(mot_de_passe, 10);
const insertSql = `
INSERT INTO utilisateur
(nom, prenom, sexe, age, date_naissance, identifiant, email, mot_de_passe, situation, statut, etat)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'en attente')
`;
db.query(insertSql, [
nom, prenom, sexe, age, date_naissance,
identifiant, email, hashed, situation, statut
], (err) => {
if (err) return res.status(500).send("Erreur lors de l'ajout de l'utilisateur");
res.redirect('/admin');
});
});
} catch (error) {
res.status(500).send('Erreur serveur');
}
});
// --------------------
// Ajout objet (admin)
// --------------------
app.post('/admin/ajouter-objet', (req, res) => {
const { denomination, adresse_ip, type, niveau, etat } = req.body;
const sql = `
INSERT INTO objet (denomination, adresse_ip, type, niveau, etat)
VALUES (?, ?, ?, ?, ?)`;
db.query(sql, [denomination, adresse_ip, type, niveau, etat], (err) => {
if (err) return res.status(500).send("Erreur lors de l'ajout de l'objet");
res.redirect('/admin');
});
});
// --------------------
// Envoi de message contact
// --------------------
app.post('/contact', (req, res) => {
const { nom, email, message } = req.body;
const sql = 'INSERT INTO contact (nom, email, message) VALUES (?, ?, ?)';
db.query(sql, [nom, email, message], (err) => {
if (err) return res.status(500).send("Erreur lors de l'envoi du message");
res.redirect('/');
});
});
// --------------------
// Déconnexion
// --------------------
app.get('/deconnexion', (req, res) => {
req.session.destroy(() => {
res.redirect('/');
});
});
// --------------------
// Lancement du serveur
// --------------------
app.listen(PORT, () => {
console.log(`✅ Serveur lancé sur http://localhost:${PORT}`);
});

View File

@ -0,0 +1,10 @@
function ensureAuthenticated(req, res, next) {
if (req.session.utilisateur) {
return next();
} else {
return res.redirect('/connexion');
}
}
module.exports = ensureAuthenticated;

2208
sk1/webapp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
sk1/webapp/package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "mon-projet",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.2",
"connect-flash": "^0.1.1",
"ejs": "^3.1.10",
"express": "^5.1.0",
"express-session": "^1.18.1",
"multer": "^1.4.5-lts.2",
"mysql2": "^3.14.0"
},
"devDependencies": {
"nodemon": "^3.1.9"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
sk1/webapp/public/images/tour.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

419
sk1/webapp/public/style.css Normal file
View File

@ -0,0 +1,419 @@
/* ------------- */
/* HTML */
/* ------------- */
html,
body {
font-family: "Inter", sans-serif;
background-color: #1e1e1e;
color: #eff1f3;
margin: 0;
padding: 0;
overflow: auto;
height: 100%; /* Définit la hauteur de la page à 100% */
display: flex; /* Flexbox sur le body */
flex-direction: column; /* Aligne le contenu du body en colonne */
}
/* --------------- */
/* Header */
/* --------------- */
header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 10px 40px;
}
header a {
text-decoration: none;
color: #eff1f3;
}
header ul {
list-style: none;
display: flex;
align-items: center;
}
header li {
cursor: pointer;
}
.header_text {
display: flex;
flex-grow: 1; /* Le conteneur des liens prend toute la place disponible */
justify-content: center; /* Centre les liens horizontalement */
gap: 15%; /* Espace entre les liens */
font-weight: 600;
}
.header_text a {
padding: 0 10px;
}
.header_text li a:hover {
color: #00a8e8;
}
.bouton_connexion {
padding: 10px 20px;
border-radius: 900px;
font-size: 16px; /* Taille de la police */
border-color: #00a8e8;
color: #eff1f3;
background-color: #00a8e8;
margin-top: 0;
cursor: pointer;
}
.bouton_connexion:hover {
border-color: #eff1f3;
color: #00a8e8;
background-color: #eff1f3; /* Changer la couleur au survol */
}
#smart_building {
width: 13%;
margin-left: -40px;
height: auto;
}
/* ------------- */
/* Main */
/* ------------- */
main {
display: flex;
flex-grow: 1; /* Permet à la section principale d'occuper l'espace restant */
justify-content: center; /* Centre verticalement le contenu */
align-items: center; /* Centre horizontalement le contenu */
padding-bottom: 50px;
gap: 30px; /* Espace entre les 2 sections */
}
.left-panel {
flex: 3; /* 30% de la largeur totale */
display: flex;
justify-content: center; /* Centrer l'image horizontalement */
align-items: center; /* Centrer l'image verticalement */
}
.left-panel img {
width: 100%; /* L'image prend toute la largeur de son conteneur */
max-width: 450px; /* Maximum de 300px de largeur */
height: auto; /* La hauteur de l'image s'ajuste automatiquement */
border-radius: 32px;
}
.right-panel {
flex: 7; /* 70% de la largeur totale */
}
.right-panel h1 {
font-size: 60px;
margin-bottom: 60px;
padding-left: 70px; /* Ajoute un espacement à gauche du titre */
}
.features {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 4 ronds par ligne */
gap: 30px;
justify-items: center;
}
.feature {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.features a{
text-decoration: none;
color: #eff1f3;
}
.circle {
width: 150px;
height: 150px;
background-color: #eff1f3;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s, color 0.3s;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.circle:hover {
background-color: #2c2c2c;
cursor: pointer;
}
.circle i {
font-size: 70px;
fill: #2c2c2c;
color: #2c2c2c;
transition: fill 0.3s, color 0.3s;
}
.circle:hover i {
fill: #eff1f3;
color: #eff1f3;
}
/* --------------- */
/* Footer */
/* --------------- */
footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
font-size: 14px;
text-transform: uppercase;
padding: 20px;
margin-top: auto; /* Cela force le footer à se pousser vers le bas */
}
.copyright {
padding: 10px 20px;
}
footer a {
text-decoration: none;
color: #eff1f3;
}
.footer_link {
display: flex;
flex-grow: 1;
gap: 10%;
}
.footer_link a {
padding: 0 10px;
}
.copyright {
padding: 10px 20px;
}
/* ------------------------- */
/* Responsive Design */
/* ------------------------- */
/* GRAND ÉCRAN (> 1440px) */
@media (min-width: 1440px) {
.right-panel h1 {
font-size: 72px;
}
.circle {
width: 180px;
height: 180px;
}
.circle i {
font-size: 80px;
}
}
/* TABLETTE - Moyenne (1025px à 1440px) */
@media (max-width: 1440px) and (min-width: 1025px) {
.features {
grid-template-columns: repeat(3, 1fr);
}
.right-panel h1 {
font-size: 48px;
padding-left: 30px;
}
.circle {
width: 130px;
height: 130px;
}
.circle i {
font-size: 50px;
}
}
/* TABLETTE - Petite (769px à 1024px) */
@media (max-width: 1024px) and (min-width: 769px) {
header {
flex-wrap: wrap;
justify-content: center;
padding: 20px;
gap: 20px;
}
#smart_building {
width: 100px;
margin: 0;
}
.header_text {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 30px;
}
.bouton_connexion {
padding: 8px 16px;
font-size: 14px;
}
main {
flex-direction: column;
padding: 40px;
}
.left-panel,
.right-panel {
width: 100%;
flex: none;
}
.features {
grid-template-columns: repeat(2, 1fr);
}
.right-panel h1 {
font-size: 42px;
padding-left: 0;
text-align: center;
}
}
/* MOBILE (≤ 768px) */
@media (max-width: 768px) {
header {
flex-direction: column;
align-items: center;
gap: 15px;
}
#smart_building {
width: 90px;
height: auto;
margin: 0 auto;
}
.header_text {
flex-direction: column;
align-items: center;
gap: 10px;
font-size: 16px;
}
.bouton_connexion {
padding: 10px 20px;
width: auto;
text-align: center;
}
main {
flex-direction: column;
padding: 20px;
}
.left-panel,
.right-panel {
width: 100%;
flex: unset;
}
.right-panel h1 {
font-size: 36px;
padding-left: 0;
text-align: center;
margin-bottom: 30px;
}
.features {
grid-template-columns: 1fr;
gap: 20px;
}
.circle {
width: 100px;
height: 100px;
}
.circle i {
font-size: 40px;
}
footer {
flex-direction: column;
text-align: center;
gap: 10px;
align-items: center;
}
.footer_link {
flex-direction: column;
align-items: center;
gap: 10px;
}
.copyright {
padding: 0;
}
}
/* TÉLÉPHONE TRÈS PETIT (≤ 480px) */
@media (max-width: 480px) {
main {
padding: 20px 10px;
}
.right-panel h1 {
font-size: 28px;
margin-bottom: 20px;
}
.circle {
width: 80px;
height: 80px;
}
.circle i {
font-size: 32px;
}
.form-control-inscription {
font-size: 14px;
padding: 8px;
}
#smart_building {
width: 70px;
}
.bouton_connexion {
font-size: 14px;
padding: 8px 16px;
}
}
/* ------------------------- */
/* Transitions douces */
/* ------------------------- */
* {
transition: all 0.3s ease-in-out;
}

View File

@ -0,0 +1,530 @@
/* ---------------- */
/* GLOBAL */
/* ---------------- */
body {
margin: 0;
padding: 0;
font-family: 'Inter', sans-serif;
background-color: #1e1e1e;
color: #eff1f3;
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
padding: 40px 60px;
}
/* --------------- */
/* Header */
/* --------------- */
header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 10px 40px;
}
header a {
text-decoration: none;
color: #eff1f3;
}
header ul {
list-style: none;
display: flex;
align-items: center;
}
header li {
cursor: pointer;
}
.header_text li a:hover {
color: #00a8e8;
}
.header_text {
display: flex;
flex-grow: 1; /* Le conteneur des liens prend toute la place disponible */
justify-content: center; /* Centre les liens horizontalement */
gap: 15%; /* Espace entre les liens */
font-weight: 600;
}
.header_text a {
padding: 0 10px;
}
.bouton_connexion {
padding: 10px 20px;
border-radius: 900px;
font-size: 16px; /* Taille de la police */
border-color: #00a8e8;
color: #eff1f3;
background-color: #00a8e8;
margin-top: 0;
cursor: pointer;
}
.bouton_connexion:hover {
border-color: #eff1f3;
color: #00a8e8;
background-color: #eff1f3; /* Changer la couleur au survol */
}
#smart_building {
width: 13%;
margin-left: -40px;
height: auto;
}
/* ---------------- */
/* TITRES */
/* ---------------- */
h1 {
font-size: 42px;
margin-bottom: 30px;
color: #00a8e8;
}
/* ---------------- */
/* TABLES */
/* ---------------- */
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 60px;
background-color: #2c2c2c;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
th,
td {
padding: 15px;
text-align: center;
border-bottom: 1px solid #444;
}
th {
background-color: #1e1e1e;
color: #00a8e8;
font-size: 18px;
}
tr:hover {
background-color: #333;
}
select {
padding: 6px 10px;
border-radius: 20px;
border: none;
background-color: #eff1f3;
color: #1e1e1e;
font-weight: bold;
}
button {
background-color: #00a8e8;
color: #eff1f3;
border: none;
padding: 8px 20px;
border-radius: 20px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s ease;
}
button:hover {
background-color: #0077b6;
}
.toggle-btn {
display: inline-block; /* Assurer que l'élément peut être transformé */
transition: transform 0.2s ease; /* Transition pour un zoom fluide */
}
.toggle-btn:hover {
cursor: pointer;
transform: scale(1.4); /* Agrandir de 1.5x lors du survol */
}
/* ---------------- */
/* FORMULAIRE */
/* ---------------- */
/* Si tu veux aussi centrer directement dans la section d'ajout */
.ajout-section {
display: flex;
flex-direction: column; /* Permet de garder la structure de la section */
justify-content: center;
align-items: center;
text-align: center;
margin: 0;
}
#form-ajout-objet,
#form-ajout-user {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
border-radius: 24px;
background-color: #2c2c2c;
padding: 40px 100px; /* Augmente l'espace intérieur */
width: 100%; /* Prend toute la largeur disponible */
max-width: 400px; /* Définit une largeur max pour éviter qu'elle ne devienne trop étroite */
box-shadow: 0 4px 8px 0 rgba(21, 21, 21, 0.2);
margin-bottom: 40px;
}
label {
font-weight: bold;
font-size: 20px;
color: #eff1f3;
white-space: nowrap;
}
select {
appearance: none; /* Supprime le style natif */
background: white;
cursor: pointer;
}
.form-group {
width: 100%;
position: relative; /* Position relative */
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.form-group .icon {
position: absolute;
left: 15px; /* Ajuste la position des icônes */
top: 50%;
transform: translateY(-50%);
}
.form-group .icon svg {
width: 24px;
height: 24px;
fill: #191717;
}
.form-control-admin {
width: 100%;
padding-left: 50px; /* Remplissage à gauche de 55px */
padding-right: 40px;
height: 48px; /* Hauteur de 40px */
border-radius: 34px; /* Bordure arrondie */
font-size: 16px; /* Taille de la police */
letter-spacing: 0.5px; /* Espacement des lettres */
background-color: #eff1f3; /* Couleur de fond */
border: none; /* Supprimer la bordure */
}
.form-row {
display: flex;
align-items: center;
gap: 10px; /* espace entre label et champ */
flex-wrap: nowrap; /* évite le retour à la ligne */
}
.bouton-admin {
padding: 10px 20px;
border-radius: 900px;
font-size: 16px; /* Taille de la police */
border-color: #00a8e8;
color: #eff1f3;
background-color: #00a8e8;
margin-top: 0; /* Centrage vertical */
cursor: pointer;
}
.bouton-admin:hover {
border-color: #eff1f3;
color: #00a8e8;
background-color: #eff1f3; /* Changer la couleur au survol */
}
/* ---------------- */
/* FOOTER */
/* ---------------- */
footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
font-size: 14px;
text-transform: uppercase;
padding: 20px;
margin-top: auto; /* Cela force le footer à se pousser vers le bas */
}
.copyright {
padding: 10px 20px;
}
footer a {
text-decoration: none;
color: #eff1f3;
}
.footer_link {
display: flex;
flex-grow: 1;
gap: 10%;
}
.footer_link a {
padding: 0 10px;
}
.copyright {
padding: 10px 20px;
}
/* ------------------------- */
/* Responsive Design */
/* ------------------------- */
/* GRAND ÉCRAN (> 1440px) */
@media (min-width: 1440px) {
header {
padding: 20px 40px;
}
.header_text {
gap: 25%; /* Plus d'espace entre les liens */
}
h1 {
font-size: 50px;
}
table {
margin-bottom: 80px;
}
.footer_link {
gap: 20%;
}
footer {
padding: 30px;
}
.bouton_connexion {
font-size: 18px;
padding: 12px 25px;
}
#smart_building {
width: 15%;
}
}
/* TABLETTE - Moyenne (1025px à 1440px) */
@media (max-width: 1440px) and (min-width: 1025px) {
header {
padding: 20px 30px;
}
.header_text {
gap: 20%; /* Moins d'espace entre les liens */
}
h1 {
font-size: 48px;
}
table {
margin-bottom: 60px;
}
footer {
padding: 25px;
}
.footer_link {
gap: 15%;
}
.bouton_connexion {
font-size: 16px;
padding: 10px 22px;
}
#smart_building {
width: 14%;
}
}
/* TABLETTE - Petite (769px à 1024px) */
@media (max-width: 1024px) and (min-width: 769px) {
header {
flex-wrap: wrap;
justify-content: center;
padding: 20px;
gap: 20px;
}
.header_text {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 30px;
}
.bouton_connexion {
padding: 8px 16px;
font-size: 14px;
}
main {
flex-direction: column;
padding: 40px;
}
table {
margin-bottom: 50px;
}
.footer_link {
gap: 10%;
}
#smart_building {
width: 100px;
margin: 0;
}
footer {
padding: 20px;
}
.footer_link {
gap: 10%;
}
}
/* MOBILE (≤ 768px) */
@media (max-width: 768px) {
header {
flex-direction: column;
align-items: center;
gap: 15px;
}
#smart_building {
width: 90px;
height: auto;
margin: 0 auto;
}
.header_text {
flex-direction: column;
align-items: center;
gap: 10px;
font-size: 16px;
}
.bouton_connexion {
padding: 10px 20px;
width: auto;
text-align: center;
}
main {
flex-direction: column;
padding: 20px;
}
footer {
flex-direction: column;
text-align: center;
gap: 10px;
align-items: center;
}
.footer_link {
flex-direction: column;
align-items: center;
gap: 10px;
}
.copyright {
padding: 0;
}
table {
margin-bottom: 30px;
}
}
/* TÉLÉPHONE TRÈS PETIT (≤ 480px) */
@media (max-width: 480px) {
main {
padding: 20px 10px;
}
h1 {
font-size: 28px;
margin-bottom: 20px;
}
#smart_building {
width: 70px;
}
.bouton_connexion {
font-size: 14px;
padding: 8px 16px;
}
table {
margin-bottom: 20px;
}
.footer_link {
flex-direction: column;
gap: 8%;
}
footer {
padding: 15px;
}
.footer_link a {
padding: 5px 10px;
}
.header_text {
font-size: 14px;
}
.bouton_connexion {
font-size: 12px;
padding: 8px 15px;
}
}

View File

@ -0,0 +1,354 @@
/* ------------- */
/* HTML */
/* ------------- */
html,
body {
font-family: "Inter", sans-serif;
background-color: #1e1e1e;
color: #eff1f3;
margin: 0;
padding: 0;
overflow: auto;
height: 100%; /* Définit la hauteur de la page à 100% */
display: flex; /* Flexbox sur le body */
flex-direction: column; /* Aligne le contenu du body en colonne */
}
/* --------------- */
/* Header */
/* --------------- */
header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 10px 40px;
}
header a {
text-decoration: none;
color: #eff1f3;
}
header ul {
list-style: none;
display: flex;
align-items: center;
}
header li {
cursor: pointer;
}
.header_text {
display: flex;
flex-grow: 1; /* Le conteneur des liens prend toute la place disponible */
justify-content: center; /* Centre les liens horizontalement */
gap: 15%; /* Espace entre les liens */
font-weight: 600;
}
.header_text a {
padding: 0 10px;
}
.header_text li a:hover {
color: #00a8e8;
}
.bouton_connexion {
padding: 10px 20px;
border-radius: 900px;
font-size: 16px; /* Taille de la police */
border-color: #00a8e8;
color: #eff1f3;
background-color: #00a8e8;
margin-top: 0;
cursor: pointer;
}
.bouton_connexion:hover {
border-color: #eff1f3;
color: #00a8e8;
background-color: #eff1f3; /* Changer la couleur au survol */
}
#smart_building {
width: 13%;
margin-left: -40px;
height: auto;
}
/* ----------------- */
/* Complexe */
/* ----------------- */
body {
background-color: #1e1e1e;
color: #eff1f3;
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
main.admin-container {
padding: 40px;
max-width: 1000px;
margin: auto;
}
h1 {
text-align: center;
margin-bottom: 30px;
font-size: 36px;
color: #00a8e8;
}
table {
width: 100%;
border-collapse: collapse;
background-color: #2c2c2c;
border-radius: 12px;
overflow: hidden;
}
th,
td {
padding: 12px 16px;
text-align: center;
border-bottom: 1px solid #444;
}
th {
background-color: #0077b6;
color: #fff;
}
tr:nth-child(even) {
background-color: #353535;
}
select,
button {
padding: 6px 10px;
border-radius: 6px;
border: none;
}
button {
background-color: #00a8e8;
color: white;
cursor: pointer;
}
button:hover {
background-color: #0077b6;
}
/* --------------- */
/* Footer */
/* --------------- */
footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
font-size: 14px;
text-transform: uppercase;
padding: 20px;
margin-top: auto; /* Cela force le footer à se pousser vers le bas */
}
.copyright {
padding: 10px 20px;
}
footer a {
text-decoration: none;
color: #eff1f3;
}
.footer_link {
display: flex;
flex-grow: 1;
gap: 10%;
}
.footer_link a {
padding: 0 10px;
}
.copyright {
padding: 10px 20px;
}
/* ------------------------- */
/* Responsive Design */
/* ------------------------- */
/* GRAND ÉCRAN (> 1440px) */
@media (min-width: 1440px) {
main.admin-container {
padding: 60px;
max-width: 1200px;
}
h1 {
font-size: 48px;
}
table th, table td {
padding: 20px;
}
.bouton_connexion {
padding: 12px 24px;
font-size: 18px;
}
}
/* TABLETTE - Moyenne (1025px à 1440px) */
@media (max-width: 1440px) and (min-width: 1025px) {
table th, table td {
padding: 15px;
}
h1 {
font-size: 40px;
}
}
/* TABLETTE - Petite (769px à 1024px) */
@media (max-width: 1024px) and (min-width: 769px) {
header {
flex-wrap: wrap;
justify-content: center;
padding: 20px;
gap: 20px;
}
#smart_building {
width: 100px;
margin: 0;
}
.header_text {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 30px;
}
.bouton_connexion {
padding: 8px 16px;
font-size: 14px;
}
main.admin-container {
padding: 30px;
}
h1 {
font-size: 36px;
margin-bottom: 20px;
}
table th, table td {
padding: 10px;
}
}
/* MOBILE (≤ 768px) */
@media (max-width: 768px) {
header {
flex-direction: column;
align-items: center;
gap: 15px;
}
#smart_building {
width: 90px;
height: auto;
margin: 0 auto;
}
.header_text {
flex-direction: column;
align-items: center;
gap: 10px;
font-size: 16px;
}
.bouton_connexion {
padding: 10px 20px;
width: auto;
text-align: center;
}
main.admin-container {
padding: 20px;
}
h1 {
font-size: 28px;
margin-bottom: 20px;
}
table th, table td {
padding: 8px;
}
footer {
flex-direction: column;
text-align: center;
gap: 10px;
align-items: center;
}
.footer_link {
flex-direction: column;
align-items: center;
gap: 10px;
}
.copyright {
padding: 0;
}
}
/* TÉLÉPHONE TRÈS PETIT (≤ 480px) */
@media (max-width: 480px) {
main.admin-container {
padding: 10px;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
.bouton_connexion {
padding: 8px 16px;
font-size: 14px;
}
#smart_building {
width: 70px;
}
table th, table td {
padding: 6px;
}
}
/* ------------------------- */
/* Transitions douces */
/* ------------------------- */
* {
transition: all 0.3s ease-in-out;
}

View File

@ -0,0 +1,453 @@
/* ------------- */
/* HTML */
/* ------------- */
html,
body {
background-color: #1e1e1e;
color: #eff1f3;
margin: 0;
padding: 0;
overflow: auto;
height: 100%;
display: flex;
flex-direction: column;
}
/* --------------- */
/* Header */
/* --------------- */
header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 10px 40px;
}
header a {
text-decoration: none;
color: #eff1f3;
}
header ul {
list-style: none;
display: flex;
align-items: center;
}
header li {
cursor: pointer;
}
.header_text {
display: flex;
flex-grow: 1; /* Le conteneur des liens prend toute la place disponible */
justify-content: center; /* Centre les liens horizontalement */
gap: 15%; /* Espace entre les liens */
font-weight: 600;
}
.header_text a {
padding: 0 10px;
}
.header_text li a:hover {
color: #00a8e8;
}
.bouton_connexion {
padding: 10px 20px;
border-radius: 900px;
font-size: 16px; /* Taille de la police */
border-color: #00a8e8;
color: #eff1f3;
background-color: #00a8e8;
margin-top: 0;
cursor: pointer;
}
.bouton_connexion:hover {
border-color: #eff1f3;
color: #00a8e8;
background-color: #eff1f3; /* Changer la couleur au survol */
}
#smart_building {
width: 13%;
margin-left: -40px;
height: auto;
}
/* ------------------- */
/* Connexion */
/* ------------------- */
h1 {
font-size: 45px; /* Taille de la police */
color: #eff1f3;
white-space: nowrap;
}
#connexion {
display: flex; /* Ajouté pour aligner les enfants */
justify-content: center; /* Centrer les éléments verticalement */
align-items: center; /* Centrer les éléments horizontalement */
flex: 1;; /* Prend toute la hauteur de l'écran */
}
.building {
width: 23%;
height: 640px;
border-radius: 33px;
padding-left: 8%;
background-image: url("/images/building.jpg");
background-size: cover;
}
.page-connexion {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
border-radius: 24px;
background-color: #2c2c2c;
padding: 40px 60px; /* Augmente l'espace intérieur */
width: 100%; /* Prend toute la largeur disponible */
max-width: 400px; /* Définit une largeur max pour éviter qu'elle ne devienne trop étroite */
box-shadow: 0 4px 8px 0 rgba(21, 21, 21, 0.2);
}
.form-group {
width: 100%;
position: relative; /* Position relative */
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.form-group .icon {
position: absolute;
left: 15px; /* Ajuste la position des icônes */
top: 50%;
transform: translateY(-50%);
}
.form-group .icon svg {
width: 24px;
height: 24px;
fill: #191717;
}
.form-control-connexion {
width: 100%;
padding-left: 50px; /* Remplissage à gauche de 55px */
padding-right: 80px;
height: 48px; /* Hauteur de 40px */
border-radius: 34px; /* Bordure arrondie */
font-size: 16px; /* Taille de la police */
letter-spacing: 0.5px; /* Espacement des lettres */
background-color: #eff1f3; /* Couleur de fond */
border: none; /* Supprimer la bordure */
}
.bouton-connexion {
padding: 10px 20px;
border-radius: 900px;
font-size: 16px; /* Taille de la police */
border-color: #00a8e8;
color: #eff1f3;
background-color: #00a8e8;
margin-top: 10px; /* Centrage vertical */
cursor: pointer;
}
.bouton-connexion:hover {
border-color: #eff1f3;
color: #00a8e8;
background-color: #eff1f3; /* Changer la couleur au survol */
}
.lien {
display: inline-block; /* Permet de mieux gérer la taille du soulignement */
text-align: center;
margin-bottom: 15px;
font-size: 20px; /* Taille de la police */
color: #eff1f3;
text-decoration: none; /* Supprime le soulignement par défaut */
text-underline-offset: 10px; /* Augmente l'espace entre le texte et le soulignement */
cursor: pointer;
padding-bottom: 10px; /* Ajoute un petit espace sous le texte */
position: relative; /* Positionner l'élément par rapport à son parent */
}
.lien::after {
content: "";
display: block;
width: 150%; /* La ligne est maintenant 1.5 fois plus grande que le texte */
height: 2px; /* Épaisseur du soulignement */
background-color: #eff1f3; /* Couleur du soulignement */
position: absolute;
left: 50%; /* Centre la ligne horizontalement */
transform: translateX(
-50%
); /* Déplace la ligne pour qu'elle soit exactement centrée */
bottom: 0;
}
.lien:hover {
color: #0077b6;
}
.cellule {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
margin-left: 9%;
}
/* --------------- */
/* Footer */
/* --------------- */
footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
font-size: 14px;
text-transform: uppercase;
padding: 20px;
margin-top: auto; /* Cela force le footer à se pousser vers le bas */
}
.copyright {
padding: 10px 20px;
}
footer a {
text-decoration: none;
color: #eff1f3;
}
.footer_link {
display: flex;
flex-grow: 1;
gap: 10%;
}
.footer_link a {
padding: 0 10px;
}
.copyright {
padding: 10px 20px;
}
/* ------------------------- */
/* Responsive Design */
/* ------------------------- */
/* GRAND ÉCRAN (> 1440px) */
@media (min-width: 1440px) {
h1 {
font-size: 60px;
}
.building {
height: 700px;
}
.form-control-connexion {
font-size: 18px;
height: 52px;
}
}
/* TABLETTE - Moyenne (1025px à 1440px) */
@media (max-width: 1440px) and (min-width: 1025px) {
h1 {
font-size: 48px;
}
.building {
height: 600px;
width: 28%;
padding-left: 5%;
}
.page-connexion {
padding: 40px 40px;
}
.form-control-connexion {
font-size: 16px;
}
}
/* TABLETTE - Petite (769px à 1024px) */
@media (max-width: 1024px) and (min-width: 769px) {
header {
flex-wrap: wrap;
justify-content: center;
padding: 20px;
gap: 20px;
}
#smart_building {
width: 100px;
margin: 0;
}
.header_text {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 30px;
}
.bouton_connexion {
padding: 8px 16px;
font-size: 14px;
}
h1 {
font-size: 40px;
text-align: center;
}
#connexion {
flex-direction: column;
padding: 40px 20px;
}
.building {
width: 100%;
height: 300px;
border-radius: 20px;
padding: 0;
margin-bottom: 30px;
}
.page-connexion {
max-width: 100%;
}
.form-control-connexion {
font-size: 15px;
}
}
/* MOBILE (≤ 768px) */
@media (max-width: 768px) {
header {
flex-direction: column;
align-items: center;
gap: 15px;
padding: 20px;
}
.header_text {
flex-direction: column;
align-items: center;
gap: 10px;
font-size: 16px;
}
.bouton_connexion {
width: auto;
padding: 10px 16px;
font-size: 14px;
}
#smart_building {
width: 80px;
margin-left: 0;
}
h1 {
font-size: 32px;
text-align: center;
}
#connexion {
flex-direction: column;
padding: 20px;
}
.building {
width: 100%;
height: 250px;
padding: 0;
margin-bottom: 20px;
}
.page-connexion {
padding: 30px 20px;
max-width: 100%;
}
.form-control-connexion {
font-size: 14px;
height: 44px;
}
footer {
flex-direction: column;
gap: 10px;
text-align: center;
align-items: center;
}
.footer_link {
flex-direction: column;
gap: 10px;
}
.copyright {
padding: 0;
}
}
/* TÉLÉPHONE TRÈS PETIT (≤ 480px) */
@media (max-width: 480px) {
h1 {
font-size: 26px;
}
.form-control-connexion {
font-size: 13px;
padding-left: 40px;
height: 40px;
}
.form-group .icon svg {
width: 20px;
height: 20px;
}
.page-connexion {
padding: 20px 10px;
}
.bouton-connexion {
font-size: 14px;
padding: 8px 14px;
}
.lien {
font-size: 16px;
}
#smart_building {
width: 60px;
}
}

View File

@ -0,0 +1,240 @@
/* --------------- */
/* Header */
/* --------------- */
header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 10px 40px;
}
header a {
text-decoration: none;
color: #eff1f3;
}
header ul {
list-style: none;
display: flex;
align-items: center;
}
header li {
cursor: pointer;
}
.header_text {
display: flex;
flex-grow: 1; /* Le conteneur des liens prend toute la place disponible */
justify-content: center; /* Centre les liens horizontalement */
gap: 15%; /* Espace entre les liens */
font-weight: 600;
}
.header_text a {
padding: 0 10px;
}
.bouton_connexion {
padding: 10px 20px;
border-radius: 900px;
font-size: 16px; /* Taille de la police */
border-color: #00a8e8;
color: #eff1f3;
background-color: #00a8e8;
margin-top: 0;
cursor: pointer;
}
.bouton_connexion:hover {
border-color: #eff1f3;
color: #00a8e8;
background-color: #eff1f3; /* Changer la couleur au survol */
}
#smart_building {
width: 13%;
margin-left: -40px;
height: auto;
}
body {
margin: 0;
font-family: 'Segoe UI', sans-serif;
background-color: #121212;
color: #f1f1f1;
}
.contact-container {
max-width: 600px;
margin: 60px auto;
padding: 20px;
background-color: #1e1e1e;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0,0,0,0.4);
}
.contact-container h1 {
text-align: center;
margin-bottom: 10px;
font-size: 2em;
}
.contact-container p {
text-align: center;
margin-bottom: 30px;
color: #cccccc;
}
.contact-form {
display: flex;
flex-direction: column;
}
.contact-form label {
margin-bottom: 5px;
font-weight: bold;
color: #ffffff;
}
.contact-form input,
.contact-form textarea {
padding: 10px;
margin-bottom: 20px;
background-color: #2a2a2a;
color: #f1f1f1;
border: 1px solid #444;
border-radius: 5px;
}
.contact-form input:focus,
.contact-form textarea:focus {
outline: none;
border: 1px solid #00bcd4;
}
.contact-form button {
background-color: #00bcd4;
color: #121212;
padding: 12px;
border: none;
border-radius: 5px;
font-weight: bold;
cursor: pointer;
transition: 0.3s;
}
.contact-form button:hover {
background-color: #0097a7;
}
/* ---------------- */
/* FOOTER */
/* ---------------- */
footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
font-size: 14px;
text-transform: uppercase;
padding: 20px;
margin-top: auto; /* Cela force le footer à se pousser vers le bas */
}
.copyright {
padding: 10px 20px;
}
footer a {
text-decoration: none;
color: #eff1f3;
}
.footer_link {
display: flex;
flex-grow: 1;
gap: 10%;
}
.footer_link a {
padding: 0 10px;
}
.copyright {
padding: 10px 20px;
}
@media (max-width: 768px) {
/* HEADER */
header {
flex-direction: column;
align-items: flex-start;
padding: 15px 20px;
}
.header_text {
flex-direction: column;
align-items: center;
gap: 10px;
width: 100%;
margin-top: 10px;
}
.bouton_connexion {
width: 100%;
text-align: center;
margin-top: 10px;
}
#smart_building {
width: 40%;
margin-left: 0;
align-self: center;
}
/* CONTACT */
.contact-container {
width: 90%;
margin: 40px auto;
padding: 15px;
}
.contact-container h1 {
font-size: 1.5em;
}
.contact-form input,
.contact-form textarea {
font-size: 1rem;
}
.contact-form button {
font-size: 1rem;
}
/* FOOTER */
footer {
flex-direction: column;
align-items: center;
text-align: center;
gap: 10px;
}
.footer_link {
flex-direction: column;
gap: 8px;
}
.footer_link a {
padding: 0;
}
}

View File

@ -0,0 +1,484 @@
/* ------------- */
/* HTML */
/* ------------- */
html,
body {
background-color: #1e1e1e;
color: #eff1f3;
margin: 0;
padding: 0;
overflow: auto;
height: 100%;
display: flex;
flex-direction: column;
}
/* --------------- */
/* Header */
/* --------------- */
header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 10px 40px;
}
header a {
text-decoration: none;
color: #eff1f3;
}
header ul {
list-style: none;
display: flex;
align-items: center;
}
header li {
cursor: pointer;
}
.header_text {
display: flex;
flex-grow: 1; /* Le conteneur des liens prend toute la place disponible */
justify-content: center; /* Centre les liens horizontalement */
gap: 15%; /* Espace entre les liens */
font-weight: 600;
}
.header_text a {
padding: 0 10px;
}
.header_text li a:hover {
color: #00a8e8;
}
.bouton_connexion {
padding: 10px 20px;
border-radius: 900px;
font-size: 16px; /* Taille de la police */
border-color: #00a8e8;
color: #eff1f3;
background-color: #00a8e8;
margin-top: 0;
cursor: pointer;
}
.bouton_connexion:hover {
border-color: #eff1f3;
color: #00a8e8;
background-color: #eff1f3; /* Changer la couleur au survol */
}
#smart_building {
width: 13%;
margin-left: -40px;
height: auto;
}
/* --------------------- */
/* Inscription */
/* --------------------- */
h1 {
font-size: 45px; /* Taille de la police */
color: #eff1f3;
white-space: nowrap;
}
label {
font-weight: bold;
font-size: 20px;
color: #eff1f3;
white-space: nowrap;
}
select {
appearance: none; /* Supprime le style natif */
background: white;
cursor: pointer;
}
#inscription {
display: flex; /* Ajouté pour aligner les enfants */
justify-content: center; /* Centrer les éléments verticalement */
align-items: center; /* Centrer les éléments horizontalement */
flex: 1; /* Prend toute la hauteur de l'écran */
margin-bottom: 50px;
}
.tour {
width: 23%;
height: 640px;
border-radius: 33px;
padding-left: 8%;
background-image: url("/images/tour.jpg");
background-size: cover;
}
.page-inscription {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
border-radius: 24px;
background-color: #2c2c2c;
padding: 40px 100px; /* Augmente l'espace intérieur */
width: 100%; /* Prend toute la largeur disponible */
max-width: 400px; /* Définit une largeur max pour éviter qu'elle ne devienne trop étroite */
box-shadow: 0 4px 8px 0 rgba(21, 21, 21, 0.2);
}
.form-group {
width: 100%;
position: relative; /* Position relative */
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.form-group .icon {
position: absolute;
left: 15px; /* Ajuste la position des icônes */
top: 50%;
transform: translateY(-50%);
}
.form-group .icon svg {
width: 24px;
height: 24px;
fill: #191717;
}
.form-control-inscription {
width: 100%;
padding-left: 50px; /* Remplissage à gauche de 50px */
padding-right: 40px;
height: 48px; /* Hauteur de 48px */
border-radius: 34px; /* Bordure arrondie */
font-size: 16px; /* Taille de la police */
letter-spacing: 0.5px; /* Espacement des lettres */
background-color: #eff1f3; /* Couleur de fond */
border: none; /* Supprimer la bordure */
}
.form-row {
display: flex;
align-items: center;
gap: 10px; /* espace entre label et champ */
flex-wrap: nowrap; /* évite le retour à la ligne */
}
.bouton-inscription {
padding: 10px 20px;
border-radius: 900px;
font-size: 16px; /* Taille de la police */
border-color: #00a8e8;
color: #eff1f3;
background-color: #00a8e8;
margin-top: 0; /* Centrage vertical */
cursor: pointer;
}
.bouton-inscription:hover {
border-color: #eff1f3;
color: #00a8e8;
background-color: #eff1f3; /* Changer la couleur au survol */
}
.boutons {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-top: 20px;
}
/* Alignement parfait du bouton "Photo" */
.custom-file-upload {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
background-color: #004fa9;
color: #eff1f3;
padding: 10px 20px;
border-radius: 900px;
cursor: pointer;
font-size: 16px;
line-height: 1;
}
.custom-file-upload:hover {
background-color: #eff1f3;
color: #004fa9;
border: 1px solid #004fa9;
}
/* Alignement du SVG */
.custom-file-upload svg {
width: 20px;
height: 20px;
vertical-align: middle;
padding-right: 4px;
}
.case {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
margin-right: 9%;
}
.lien {
background: none;
border: none;
font-family: inherit; /* pour garder la cohérence du texte */
display: inline-block; /* Permet de mieux gérer la taille du soulignement */
text-align: center;
margin-bottom: 15px;
font-size: 23px; /* Taille de la police */
color: #eff1f3;
text-decoration: none; /* Supprime le soulignement par défaut */
text-underline-offset: 10px; /* Augmente l'espace entre le texte et le soulignement */
cursor: pointer;
padding-bottom: 10px; /* Ajoute un petit espace sous le texte */
position: relative; /* Positionner l'élément par rapport à son parent */
}
.lien::after {
content: "";
display: block;
width: 120%; /* La ligne est maintenant 1.5 fois plus grande que le texte */
height: 2px; /* Épaisseur du soulignement */
background-color: #eff1f3; /* Couleur du soulignement */
position: absolute;
left: 50%; /* Centre la ligne horizontalement */
transform: translateX(
-50%
); /* Déplace la ligne pour qu'elle soit exactement centrée */
bottom: 0;
}
.lien:hover {
color: #0077b6;
}
/* --------------- */
/* Footer */
/* --------------- */
footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
font-size: 14px;
text-transform: uppercase;
padding: 20px;
margin-top: auto; /* Cela force le footer à se pousser vers le bas */
}
.copyright {
padding: 10px 20px;
}
footer a {
text-decoration: none;
color: #eff1f3;
}
.footer_link {
display: flex;
flex-grow: 1;
gap: 10%;
}
.footer_link a {
padding: 0 10px;
}
.copyright {
padding: 10px 20px;
}
/* ---------------------- */
/* Media Queries */
/* ---------------------- */
/* GRAND ÉCRAN (> 1440px) */
@media (min-width: 1440px) {
h1 {
font-size: 48px;
}
.tour {
height: 700px;
}
.form-control-inscription {
font-size: 18px;
height: 52px;
}
}
/* TABLETTE - Moyenne (1025px à 1440px) */
@media (max-width: 1440px) and (min-width: 1025px) {
h1 {
font-size: 48px;
}
.tour {
height: 600px;
}
.page-inscription {
padding: 40px;
}
.form-control-inscription {
font-size: 16px;
}
}
/* TABLETTE - Petite (769px à 1024px) */
@media (max-width: 1024px) and (min-width: 769px) {
header {
flex-wrap: wrap;
justify-content: center;
padding: 20px;
gap: 20px;
}
#smart_building {
width: 100px;
margin: 0;
}
.header_text {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 30px;
}
.bouton_connexion {
padding: 8px 16px;
font-size: 14px;
}
h1 {
font-size: 40px;
text-align: center;
}
.tour {
width: 100%;
height: 300px;
}
.form-control-inscription {
font-size: 15px;
}
}
/* MOBILE (≤ 768px) */
@media (max-width: 768px) {
header {
flex-direction: column;
align-items: center;
gap: 15px;
padding: 20px;
}
.header_text {
flex-direction: column;
align-items: center;
gap: 10px;
font-size: 16px;
}
.bouton_connexion {
width: auto;
padding: 10px 16px;
font-size: 14px;
}
#smart_building {
width: 80px;
margin-left: 0;
}
h1 {
font-size: 32px;
text-align: center;
}
.tour {
height: 250px;
margin-bottom: 20px;
}
.form-control-inscription {
font-size: 14px;
height: 44px;
}
footer {
flex-direction: column;
gap: 10px;
text-align: center;
align-items: center;
}
.footer_link {
flex-direction: column;
gap: 10px;
}
.copyright {
padding: 0;
}
}
/* TÉLÉPHONE TRÈS PETIT (≤ 480px) */
@media (max-width: 480px) {
h1 {
font-size: 26px;
}
.form-control-inscription {
font-size: 13px;
padding-left: 40px;
height: 40px;
}
.form-group .icon svg {
width: 20px;
height: 20px;
}
.page-inscription {
padding: 20px 10px;
}
.bouton-inscription {
font-size: 14px;
padding: 8px 14px;
}
.lien {
font-size: 16px;
}
.tour {
width: 60px;
}
}

View File

@ -0,0 +1,576 @@
/* ------------- */
/* HTML */
/* ------------- */
html,
body {
font-family: "Inter", sans-serif;
background-color: #1e1e1e;
color: #eff1f3;
margin: 0;
padding: 0;
overflow: auto;
height: 100%; /* Définit la hauteur de la page à 100% */
display: flex; /* Flexbox sur le body */
flex-direction: column; /* Aligne le contenu du body en colonne */
}
/* --------------- */
/* Header */
/* --------------- */
header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 10px 40px;
}
header a {
text-decoration: none;
color: #eff1f3;
}
header ul {
list-style: none;
display: flex;
align-items: center;
}
header li {
cursor: pointer;
}
.header_text {
display: flex;
flex-grow: 1; /* Le conteneur des liens prend toute la place disponible */
justify-content: center; /* Centre les liens horizontalement */
gap: 15%; /* Espace entre les liens */
font-weight: 600;
}
.header_text a {
padding: 0 10px;
}
.header_text li a:hover {
color: #00a8e8;
}
.bouton_connexion {
padding: 10px 20px;
border-radius: 900px;
font-size: 16px; /* Taille de la police */
border-color: #00a8e8;
color: #eff1f3;
background-color: #00a8e8;
margin-top: 0;
cursor: pointer;
}
.bouton_connexion:hover {
border-color: #eff1f3;
color: #00a8e8;
background-color: #eff1f3; /* Changer la couleur au survol */
}
#smart_building {
width: 13%;
margin-left: -40px;
height: auto;
}
/* ---------------- */
/* Membres */
/* ---------------- */
main {
display: flex;
flex-grow: 1; /* Permet à la section principale d'occuper l'espace restant */
justify-content: center; /* Centre verticalement le contenu */
align-items: center; /* Centre horizontalement le contenu */
padding-bottom: 50px;
gap: 30px; /* Espace entre les 2 sections */
}
.left-panel {
flex: 3; /* 30% de la largeur totale */
display: flex;
justify-content: center; /* Centrer l'image horizontalement */
align-items: center; /* Centrer l'image verticalement */
}
.left-panel img {
width: 100%; /* L'image prend toute la largeur de son conteneur */
max-width: 450px; /* Maximum de 300px de largeur */
height: auto; /* La hauteur de l'image s'ajuste automatiquement */
border-radius: 32px;
}
.right-panel {
flex: 7; /* 70% de la largeur totale */
}
.right-panel h1 {
font-size: 60px;
margin-bottom: 60px;
padding-left: 50px; /* Ajoute un espacement à gauche du titre */
}
#membres-container {
display: grid;
grid-template-columns: repeat(2, 1fr); /* 2 cartes par ligne */
gap: 30px;
justify-items: center;
padding: 20px 60px;
}
.membre {
width: 100%; /* occupe toute la colonne */
max-width: 400px; /* limite visuelle de largeur */
padding: 20px;
background-color: #2c2c2c;
color: #eff1f3;
border-radius: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: transform 0.2s ease, background-color 0.3s ease;
text-align: center;
}
.membre:hover {
background-color: #dfe3e6;
transform: translateY(-5px);
color: #2c2c2c;
cursor: pointer;
}
.membre h3 {
font-size: 22px;
margin: 10px 0 5px;
}
.membre h4 {
font-size: 18px;
margin: 5px 0;
font-weight: 500;
}
.membre p {
margin: 6px 0;
font-size: 16px;
}
/* --------------- */
/* Footer */
/* --------------- */
footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
font-size: 14px;
text-transform: uppercase;
padding: 20px;
margin-top: auto; /* Cela force le footer à se pousser vers le bas */
}
.copyright {
padding: 10px 20px;
}
footer a {
text-decoration: none;
color: #eff1f3;
}
.footer_link {
display: flex;
flex-grow: 1;
gap: 10%;
}
.footer_link a {
padding: 0 10px;
}
.copyright {
padding: 10px 20px;
}
/* -------------------- */
/* Barre Recherche */
/* -------------------- */
.form {
width: 300px;
margin: 0 50px;
font-size: 0.9rem;
display: flex;
gap: 0.5rem;
align-items: center;
position: relative;
isolation: isolate;
--input-text-color: #363636;
--input-bg-color: #eff1f3;
--focus-input-bg-color: transparent;
--text-color: #2a2a2a;
--active-color: #eff1f3;
--width-of-input: 250px;
--inline-padding-of-input: 1.2em;
--gap: 0.9rem;
}
.fancy-bg {
position: absolute;
width: 100%;
inset: 0;
background: var(--input-bg-color);
border-radius: 30px;
height: 100%;
z-index: -1;
pointer-events: none;
box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px;
}
label {
padding: 0.2em;
height: 40px;
padding-inline: var(--inline-padding-of-input);
display: flex;
align-items: center;
}
.search,
.close-btn {
position: absolute;
}
.search {
fill: var(--text-color);
left: var(--inline-padding-of-input);
}
svg {
width: 17px;
display: block;
}
.input {
color: var(--input-text-color);
width: 100%;
background: none;
border: none;
font-size: 1rem;
margin-inline: min(2em, calc(var(--inline-padding-of-input) + var(--gap)));
}
.input:focus {
outline: none;
color: #eff1f3;
}
.input:focus ~ .fancy-bg {
border: 1px solid var(--active-color);
background: var(--focus-input-bg-color);
}
.input:focus ~ .search {
fill: var(--active-color);
}
.input:valid ~ .close-btn {
visibility: visible;
color: #eff1f3;
}
.close-btn {
right: var(--inline-padding-of-input);
background: none;
border: none;
box-shadow: none;
width: auto;
height: auto;
padding: 0;
opacity: 1;
color: #eff1f3;
visibility: hidden;
font-size: 20px;
cursor: pointer;
}
.close-btn:hover,
.close-btn:focus {
background: none;
font-weight: bold;
transform: scale(1.2);
}
.input:not(:focus):not(:placeholder-shown) {
color: #2a2a2a;
}
.input:not(:focus):not(:placeholder-shown) ~ .search {
fill: #2a2a2a;
}
.input:not(:focus):not(:placeholder-shown) ~ .close-btn {
color: #2a2a2a;
}
.header-membres {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 70px;
margin-bottom: 60px;
flex-wrap: wrap;
}
.header-membres h1 {
font-size: 60px;
margin: 0;
color: #eff1f3;
}
/* ----------------------------- */
/* Responsive Design */
/* ----------------------------- */
/* GRAND ÉCRAN (> 1440px) */
@media (min-width: 1440px) {
.right-panel h1,
.header-membres h1 {
font-size: 72px;
}
.left-panel img {
max-width: 500px;
}
}
/* TABLETTE LARGE (1025px à 1440px) */
@media (max-width: 1440px) and (min-width: 1025px) {
.right-panel h1,
.header-membres h1 {
font-size: 56px;
padding-left: 80px;
}
.header-membres {
padding: 0 50px;
}
#membres-container {
padding: 20px 40px;
}
.left-panel img {
max-width: 400px;
}
}
/* TABLETTE MOYENNE (769px à 1024px) */
@media (max-width: 1024px) and (min-width: 769px) {
header {
flex-wrap: wrap;
justify-content: center;
padding: 20px;
gap: 20px;
}
#smart_building {
width: 100px;
margin: 0;
}
.header_text {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 30px;
}
.bouton_connexion {
padding: 8px 16px;
font-size: 14px;
}
main {
flex-direction: column;
padding: 40px;
}
.right-panel,
.left-panel {
width: 100%;
flex: none;
}
.features {
grid-template-columns: repeat(2, 1fr);
}
.right-panel h1,
.header-membres h1 {
font-size: 42px;
padding-left: 0;
text-align: center;
}
.header-membres {
padding: 0 30px;
justify-content: center;
gap: 30px;
}
.form {
margin: 0 auto;
}
#membres-container {
grid-template-columns: repeat(2, 1fr);
padding: 20px;
}
}
/* MOBILE (≤ 768px) */
@media (max-width: 768px) {
header {
flex-direction: column;
align-items: center;
gap: 15px;
}
#smart_building {
width: 90px;
height: auto;
margin: 0 auto;
}
.header_text {
flex-direction: column;
align-items: center;
gap: 10px;
font-size: 16px;
}
.bouton_connexion {
padding: 10px 20px;
width: auto;
text-align: center;
}
main {
flex-direction: column;
padding: 20px;
}
.left-panel,
.right-panel {
width: 100%;
flex: unset;
}
.right-panel h1,
.header-membres h1 {
font-size: 36px;
padding-left: 0;
text-align: center;
margin-bottom: 30px;
}
.header-membres {
flex-direction: column;
align-items: center;
gap: 20px;
padding: 0 20px;
}
.form {
width: 100%;
max-width: 300px;
}
#membres-container {
grid-template-columns: 1fr;
padding: 10px;
}
.membre {
max-width: 100%;
}
footer {
flex-direction: column;
text-align: center;
gap: 10px;
align-items: center;
}
.footer_link {
flex-direction: column;
gap: 10px;
}
.copyright {
padding: 0;
}
}
/* MOBILE TRÈS PETIT (≤ 480px) */
@media (max-width: 480px) {
.right-panel h1,
.header-membres h1 {
font-size: 28px;
margin-bottom: 20px;
}
.form {
width: 90%;
max-width: 280px;
}
.input {
font-size: 0.9rem;
}
.close-btn {
font-size: 16px;
}
#membres-container {
gap: 20px;
}
.membre {
padding: 16px;
font-size: 14px;
}
.membre h3 {
font-size: 20px;
}
.membre h4 {
font-size: 16px;
}
.membre p {
font-size: 14px;
}
}
/* ------------------------ */
/* Transitions Générales */
/* ------------------------ */
* {
transition: all 0.3s ease-in-out;
}

View File

@ -0,0 +1,472 @@
/* ------------- */
/* HTML */
/* ------------- */
html,
body {
font-family: "Inter", sans-serif;
background-color: #1e1e1e;
color: #eff1f3;
margin: 0;
padding: 0;
overflow: auto;
height: 100%; /* Définit la hauteur de la page à 100% */
display: flex; /* Flexbox sur le body */
flex-direction: column; /* Aligne le contenu du body en colonne */
}
/* --------------- */
/* Header */
/* --------------- */
header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 10px 40px;
}
header a {
text-decoration: none;
color: #eff1f3;
}
header ul {
list-style: none;
display: flex;
align-items: center;
}
header li {
cursor: pointer;
}
.header_text {
display: flex;
flex-grow: 1; /* Le conteneur des liens prend toute la place disponible */
justify-content: center; /* Centre les liens horizontalement */
gap: 15%; /* Espace entre les liens */
font-weight: 600;
}
.header_text a {
padding: 0 10px;
}
.bouton_connexion {
padding: 10px 20px;
border-radius: 900px;
font-size: 16px; /* Taille de la police */
border-color: #00a8e8;
color: #eff1f3;
background-color: #00a8e8;
margin-top: 0;
cursor: pointer;
}
.bouton_connexion:hover {
border-color: #eff1f3;
color: #00a8e8;
background-color: #eff1f3; /* Changer la couleur au survol */
}
#smart_building {
width: 13%;
margin-left: -40px;
height: auto;
}
/* --------------- */
/* Objets */
/* --------------- */
main {
display: flex;
flex-grow: 1; /* Permet à la section principale d'occuper l'espace restant */
justify-content: center; /* Centre verticalement le contenu */
align-items: center; /* Centre horizontalement le contenu */
padding-bottom: 50px;
gap: 30px; /* Espace entre les 2 sections */
}
.left-panel {
flex: 3; /* 30% de la largeur totale */
display: flex;
justify-content: center; /* Centrer l'image horizontalement */
align-items: center; /* Centrer l'image verticalement */
}
.left-panel img {
width: 100%; /* L'image prend toute la largeur de son conteneur */
max-width: 450px; /* Maximum de 300px de largeur */
height: auto; /* La hauteur de l'image s'ajuste automatiquement */
border-radius: 32px;
}
.right-panel {
flex: 7; /* 70% de la largeur totale */
}
.right-panel h1 {
font-size: 60px;
margin-bottom: 60px;
}
.header-objets {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 70px;
margin-bottom: 60px;
flex-wrap: wrap;
}
.header-objets h1 {
font-size: 60px;
margin: 0;
color: #eff1f3;
}
#objets-container {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 3 cartes par ligne */
gap: 20px;
justify-items: center;
padding: 20px 60px;
}
.objet {
width: 100%;
max-width: 300px; /* Limite la largeur des cartes pour que ça soit plus joli */
padding: 20px;
background-color: #2c2c2c;
color: #eff1f3;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: transform 0.2s ease, background-color 0.3s ease;
text-align: center;
}
.objet:hover {
transform: translateY(-5px);
background-color: #dfe3e6;
color: #2c2c2c;
cursor: pointer;
}
.objet h3 {
font-size: 22px;
margin: 10px 0 5px;
}
.etat-point {
width: 12px;
height: 12px;
border-radius: 50%;
position: absolute;
bottom: 5px;
right: 5px;
}
.etat-point.actif {
background-color: rgb(100, 236, 100);
}
.etat-point.inactif {
background-color: rgb(240, 84, 84);
}
.setting-btn {
background: none;
border: none;
text-align: center;
cursor: pointer;
}
.setting-btn i {
font-size: 20px;
color: #a8a3a3;
}
.setting-btn:hover i {
transform: scale(1.4);
transition: transform 0.2s ease;
}
.objet:hover i {
color: #2c2c2c;
}
.info_objet{
display: flex;
align-items: center;
justify-content: space-between;
}
.info_objet h2{
margin-bottom: 100px;
}
.info_objet h4{
margin:0px;
}
.actions{
display: flex;
align-items: center;
justify-content: space-between;
}
.edit-button {
background: none;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
display: flex;
align-items: center;
}
.edit-button i {
font-size: 14px;
color: #eff1f3;
transition: transform 0.2s ease;
transform: translateY(-2px);
}
.edit-button:hover i {
transform: scale(1.3) translateY(-2px);
}
.inline-edit {
display: flex;
align-items: center;
gap: 10px;
}
.inline-edit h2,
.inline-edit p {
margin: 0;
}
[contenteditable="true"] {
border: 1px solid #eff1f3; /* Bordure fine autour de l'élément modifiable */
border-radius: 4px; /* Bords arrondis pour un style plus doux */
padding: 3px 5px; /* Un peu de padding pour l'esthétique */
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); /* Ombre légère autour de l'élément */
}
/* Style modal */
.modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(60, 60, 60, 0.98); /* Plus clair que le fond de base */
backdrop-filter: blur(12px); /* Plus de flou = plus de séparation */
padding: 30px;
border-radius: 20px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.8); /* Ombre plus forte */
color: #eff1f3;
max-width: 450px;
width: 90%;
z-index: 1000;
animation: fadeIn 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1); /* Léger contour clair */
}
.modal-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.modal h2 {
font-size: 24px;
margin-bottom: 10px;
color: #eff1f3;
}
.modal p {
font-size: 16px;
line-height: 1.4;
}
.modal .close {
position: absolute;
top: 10px;
right: 15px;
font-size: 22px;
font-weight: bold;
color: #eff1f3;
cursor: pointer;
transition: transform 0.2s ease, color 0.2s ease;
}
.modal .close:hover {
color: #ff4d4d;
transform: scale(1.2);
}
/* -------------------- */
/* Barre Recherche */
/* -------------------- */
.form {
width: 300px;
margin: 0;
font-size: 0.9rem;
display: flex;
gap: 0.5rem;
align-items: center;
position: relative;
isolation: isolate;
--input-text-color: #363636;
--input-bg-color: #eff1f3;
--focus-input-bg-color: transparent;
--text-color: #2a2a2a;
--active-color: #eff1f3;
--width-of-input: 250px;
--inline-padding-of-input: 1.2em;
--gap: 0.9rem;
}
.fancy-bg {
position: absolute;
width: 100%;
inset: 0;
background: var(--input-bg-color);
border-radius: 30px;
height: 100%;
z-index: -1;
pointer-events: none;
box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px;
}
label {
padding: 0.2em;
height: 40px;
padding-inline: var(--inline-padding-of-input);
display: flex;
align-items: center;
}
.search,
.close-btn {
position: absolute;
}
.search {
fill: var(--text-color);
left: var(--inline-padding-of-input);
}
svg {
width: 17px;
display: block;
}
.input {
color: var(--input-text-color); /* La couleur du texte par défaut */
width: 100%;
margin-inline: min(2em, calc(var(--inline-padding-of-input) + var(--gap)));
background: none;
border: none;
}
.input:focus {
outline: none;
color: #eff1f3;
}
/* input background change in focus */
.input:focus ~ .fancy-bg {
border: 1px solid var(--active-color);
background: var(--focus-input-bg-color);
}
/* search icon color change in focus */
.input:focus ~ .search {
fill: var(--active-color);
}
/* showing close button when typing */
.input:valid ~ .close-btn {
visibility: visible; /* Si l'input est valide, rendre le bouton visible */
color: #eff1f3; /* Garde l'icône en blanc */
}
.close-btn {
right: var(--inline-padding-of-input);
background: none;
border: none;
box-shadow: none;
width: auto;
height: auto;
padding: 0;
opacity: 1;
color: #eff1f3;
visibility: hidden;
font-size: 20px;
cursor: pointer;
}
.close-btn:hover,
.close-btn:focus {
background: none;
font-weight: bold;
transform: scale(1.2); /* Légèrement agrandit l'icône pour la rendre plus évidente */
}
.input:not(:focus):not(:placeholder-shown) {
color: #2a2a2a; /* Couleur texte sombre si champ rempli mais pas focus */
}
.input:not(:focus):not(:placeholder-shown) ~ .search {
fill: #2a2a2a; /* Loupe sombre quand input rempli mais pas focus */
}
.input:not(:focus):not(:placeholder-shown) ~ .close-btn {
color: #2a2a2a; /* Croix sombre également */
}
/* --------------- */
/* Footer */
/* --------------- */
footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
font-size: 14px;
text-transform: uppercase;
padding: 20px;
margin-top: auto; /* Cela force le footer à se pousser vers le bas */
}
.copyright {
padding: 10px 20px;
}
footer a {
text-decoration: none;
color: #eff1f3;
}
.footer_link {
display: flex;
flex-grow: 1;
gap: 10%;
}
.footer_link a {
padding: 0 10px;
}
.copyright {
padding: 10px 20px;
}

View File

@ -0,0 +1,378 @@
/* ------------- */
/* HTML */
/* ------------- */
html,
body {
font-family: "Inter", sans-serif;
background-color: #1e1e1e;
color: #eff1f3;
margin: 0;
padding: 0;
overflow: auto;
height: 100%; /* Définit la hauteur de la page à 100% */
display: flex; /* Flexbox sur le body */
flex-direction: column; /* Aligne le contenu du body en colonne */
}
/* --------------- */
/* Header */
/* --------------- */
header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 10px 40px;
}
header a {
text-decoration: none;
color: #eff1f3;
}
header ul {
list-style: none;
display: flex;
align-items: center;
}
header li {
cursor: pointer;
}
.header_text {
display: flex;
flex-grow: 1; /* Le conteneur des liens prend toute la place disponible */
justify-content: center; /* Centre les liens horizontalement */
gap: 15%; /* Espace entre les liens */
font-weight: 600;
}
.header_text a {
padding: 0 10px;
}
.header_text li a:hover {
color: #00a8e8;
}
.bouton_connexion {
padding: 10px 20px;
border-radius: 900px;
font-size: 16px; /* Taille de la police */
border-color: #00a8e8;
color: #eff1f3;
background-color: #00a8e8;
margin-top: 0;
cursor: pointer;
}
.bouton_connexion:hover {
border-color: #eff1f3;
color: #00a8e8;
background-color: #eff1f3; /* Changer la couleur au survol */
}
#smart_building {
width: 13%;
margin-left: -40px;
height: auto;
}
/* --------------- */
/* Profil */
/* --------------- */
/* Conteneur Profil */
.container {
width: 80%; /* Adaptation à la nouvelle structure, largeur plus grande */
max-width: 800px; /* Limite la taille maximale à 800px */
margin: 50px auto; /* Centre le conteneur et ajoute un espacement */
background-color: #2c2c2c;
padding: 30px 40px;
border-radius: 15px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column; /* Assure un alignement vertical des éléments */
gap: 20px; /* Espacement entre les éléments internes */
z-index: 1; /* Pour s'assurer que ce contenu reste bien en-dessous de l'en-tête */
}
/* Titre */
h1 {
text-align: center;
margin-bottom: 30px;
color: #ffffff;
}
/* Formulaire Profil */
form div {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #ccc;
}
/* Champs de saisie */
input[type="text"],
input[type="password"],
input[type="file"],
select {
width: 100%;
padding: 10px 12px;
font-size: 14px;
background-color: #3a3a3a;
color: #ffffff;
border: 1px solid #555;
border-radius: 8px;
box-sizing: border-box;
transition: border-color 0.3s;
}
/* Champs désactivés */
input:disabled,
select:disabled {
background-color: #2c2c2c;
color: #888;
border: 1px solid #444;
cursor: not-allowed;
}
/* Image de profil */
.container img {
margin-top: 10px;
border-radius: 10px;
border: 2px solid #444;
max-width: 100px;
height: auto;
}
/* Boutons (Editer, Sauvegarder) */
button {
padding: 10px 16px;
border: none;
border-radius: 8px;
font-size: 15px;
cursor: pointer;
transition: background-color 0.3s ease;
color: white;
}
/* Bouton Modifier */
#editBtn {
background-color: #27ae60;
}
#editBtn:hover {
background-color: #219150;
}
/* Bouton Sauvegarder */
#saveBtn {
background-color: #e67e22;
}
#saveBtn:hover {
background-color: #d35400;
}
/* --------------- */
/* Footer */
/* --------------- */
footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
font-size: 14px;
text-transform: uppercase;
padding: 20px;
margin-top: auto; /* Cela force le footer à se pousser vers le bas */
}
.copyright {
padding: 10px 20px;
}
footer a {
text-decoration: none;
color: #eff1f3;
}
.footer_link {
display: flex;
flex-grow: 1;
gap: 10%;
}
.footer_link a {
padding: 0 10px;
}
.copyright {
padding: 10px 20px;
}
/* ------------------------- */
/* Responsive Design */
/* ------------------------- */
/* GRAND ÉCRAN (> 1440px) */
@media (min-width: 1440px) {
main {
padding: 60px;
max-width: 1200px;
}
.container {
width: 70%;
}
h1 {
font-size: 48px;
}
.bouton_connexion {
padding: 12px 24px;
font-size: 18px;
}
}
/* TABLETTE - Moyenne (1025px à 1440px) */
@media (max-width: 1440px) and (min-width: 1025px) {
.container {
width: 75%;
max-width: 1000px;
}
h1 {
font-size: 40px;
}
.bouton_connexion {
padding: 10px 20px;
font-size: 16px;
}
}
/* TABLETTE - Petite (769px à 1024px) */
@media (max-width: 1024px) and (min-width: 769px) {
header {
flex-wrap: wrap;
justify-content: center;
padding: 20px;
gap: 20px;
}
#smart_building {
width: 100px;
margin: 0;
}
.header_text {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 30px;
}
.bouton_connexion {
padding: 8px 16px;
font-size: 14px;
}
.container {
width: 90%;
max-width: 1000px;
}
h1 {
font-size: 36px;
margin-bottom: 20px;
}
}
/* MOBILE (≤ 768px) */
@media (max-width: 768px) {
header {
flex-direction: column;
align-items: center;
gap: 15px;
}
#smart_building {
width: 90px;
height: auto;
margin: 0 auto;
}
.header_text {
flex-direction: column;
align-items: center;
gap: 10px;
font-size: 16px;
}
.bouton_connexion {
padding: 10px 20px;
width: auto;
text-align: center;
}
.container {
width: 100%;
max-width: 100%;
padding: 20px;
}
h1 {
font-size: 28px;
margin-bottom: 20px;
}
footer {
flex-direction: column;
text-align: center;
gap: 10px;
align-items: center;
}
.footer_link {
flex-direction: column;
align-items: center;
gap: 10px;
}
.copyright {
padding: 0;
}
}
/* TÉLÉPHONE TRÈS PETIT (≤ 480px) */
@media (max-width: 480px) {
.container {
width: 100%;
padding: 10px;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
.bouton_connexion {
padding: 8px 16px;
font-size: 14px;
}
#smart_building {
width: 70px;
}
}

1
sk1/webapp/query Normal file
View File

@ -0,0 +1 @@
mysql

115
sk1/webapp/routes/admin.js Normal file
View File

@ -0,0 +1,115 @@
const express = require('express');
const router = express.Router();
const db = require('../config/db');
// Middleware de protection admin (défini ici si non global)
const requireAdmin = (req, res, next) => {
if (!req.session.utilisateur || req.session.utilisateur.statut !== 'administrateur') {
return res.send(`
<script>
alert("Tu n'es pas connecté en tant qu'admin !");
window.location.href = '/connexion';
</script>
`);
}
next();
};
// Applique le middleware à toutes les routes de ce fichier
router.use(requireAdmin);
// Route principale - admin dashboard
router.get('/', (req, res) => {
const usersQuery = 'SELECT * FROM utilisateur';
const objetsQuery = 'SELECT * FROM objet';
const contactsQuery = 'SELECT * FROM contact'; // Récupérer les messages de contact
db.query(usersQuery, (err, utilisateurs) => {
if (err) return res.status(500).send('Erreur récupération utilisateurs');
db.query(objetsQuery, (err2, objets) => {
if (err2) return res.status(500).send('Erreur récupération objets');
db.query(contactsQuery, (err3, contacts) => {
if (err3) return res.status(500).send('Erreur récupération messages');
res.render('admin', { utilisateurs, objets, contacts });
});
});
});
});
// Mise à jour utilisateur
router.post('/update-user', (req, res) => {
const { id, etat, statut } = req.body;
const sql = 'UPDATE utilisateur SET etat = ?, statut = ? WHERE id = ?';
db.query(sql, [etat, statut, id], (err) => {
if (err) {
console.error('Erreur update utilisateur :', err);
return res.status(500).send('Erreur update utilisateur');
}
res.redirect('/admin');
});
});
// Suppression utilisateur
router.post('/supprimer-user', (req, res) => {
const { id } = req.body;
const sql = 'DELETE FROM utilisateur WHERE id = ?';
db.query(sql, [id], (err) => {
if (err) {
console.error('Erreur suppression utilisateur :', err);
return res.status(500).send("Erreur lors de la suppression de l'utilisateur");
}
res.redirect('/admin');
});
});
// Mise à jour objet
router.post('/update-objet', (req, res) => {
const { adresse_ip, niveau, etat, type } = req.body;
const sql = 'UPDATE objet SET niveau = ?, etat = ?, type = ? WHERE adresse_ip = ?';
db.query(sql, [niveau, etat, type, adresse_ip], (err) => {
if (err) {
console.error('Erreur update objet :', err);
return res.status(500).send('Erreur update objet');
}
res.redirect('/admin');
});
});
// Suppression objet + dépendances
router.post('/supprimer-objet', (req, res) => {
const { adresse_ip } = req.body;
const deleteChildTables = ['climatisation_chauffage', 'thermostat', 'appareil_menager', 'securite'];
const deleteFromTable = (table) => {
return new Promise((resolve, reject) => {
const sql = `DELETE FROM ${table} WHERE adresse_ip = ?`;
db.query(sql, [adresse_ip], (err) => {
if (err) return reject(err);
resolve();
});
});
};
Promise.all(deleteChildTables.map(deleteFromTable))
.then(() => {
const sql = 'DELETE FROM objet WHERE adresse_ip = ?';
db.query(sql, [adresse_ip], (err) => {
if (err) {
console.error('Erreur suppression objet :', err);
return res.status(500).send("Erreur lors de la suppression de l'objet");
}
res.redirect('/admin');
});
})
.catch(err => {
console.error("Erreur suppression dépendances : ", err);
res.status(500).send("Erreur lors de la suppression des dépendances liées à l'objet");
});
});
module.exports = router;

View File

@ -0,0 +1,56 @@
const express = require('express');
const router = express.Router();
const db = require('../../config/db');
router.get('/', (req, res) => {
const sql = `
SELECT
objet.adresse_ip,
objet.niveau,
objet.denomination,
objet.etat,
objet.derniere_interaction,
objet.type,
thermostat.temperature_actuelle,
thermostat.temperature_cible,
thermostat.mode,
appareil_menager.depart_differe,
appareil_menager.etat_de_la_tache,
securite.situation AS situation_securite
FROM objet
LEFT JOIN thermostat ON objet.adresse_ip = thermostat.adresse_ip
LEFT JOIN appareil_menager ON objet.adresse_ip = appareil_menager.adresse_ip
LEFT JOIN securite ON objet.adresse_ip = securite.adresse_ip
`;
db.query(sql, (err, result) => {
if (err) {
console.error('Erreur lors de la récupération des objets :', err);
return res.status(500).send('Erreur serveur');
}
res.json(result);
});
});
router.put('/:adresse_ip', (req, res) => {
const adresse_ip = req.params.adresse_ip;
const champs = req.body;
const setValues = Object.keys(champs)
.map(key => `${key} = ?`)
.join(', ');
const sql = `UPDATE objet SET ${setValues} WHERE adresse_ip = ?`;
const values = [...Object.values(champs), adresse_ip];
db.query(sql, values, (err) => {
if (err) {
console.error('Erreur lors de la mise à jour de l\'objet :', err);
return res.status(500).send('Erreur serveur');
}
res.sendStatus(200);
});
});
module.exports = router;

View File

@ -0,0 +1,34 @@
const express = require('express');
const router = express.Router();
const db = require('../../config/db'); // Connexion à la base de données
// Route pour récupérer les ressources
router.get('/', (req, res) => {
const search = req.query.search || '';
if (search) {
const sql = 'SELECT * FROM ressources WHERE nom LIKE ?';
const values = [`%${search}%`];
db.query(sql, values, (err, results) => {
if (err) {
console.error('Erreur lors de la recherche des ressources :', err);
res.status(500).send('Erreur serveur');
} else {
res.json(results);
}
});
} else {
const sql = 'SELECT * FROM ressources';
db.query(sql, (err, results) => {
if (err) {
console.error('Erreur lors de la récupération des ressources :', err);
res.status(500).send('Erreur serveur');
} else {
res.json(results);
}
});
}
});
module.exports = router;

View File

@ -0,0 +1,18 @@
const express = require('express');
const router = express.Router();
const db = require('../../config/db'); // Connexion à la base de données
// Route pour récupérer les objets
router.get('/', (req, res) => {
const sql = 'SELECT * FROM utilisateur';
db.query(sql, (err, results) => {
if (err) {
console.error('Erreur lors de la récupération des objets :', err);
res.status(500).send('Erreur serveur');
} else {
res.json(results); // Retourne les objets sous forme de JSON
}
});
});
module.exports = router;

View File

@ -0,0 +1,77 @@
const express = require('express');
const router = express.Router();
const db = require('../config/db');
// Modifier un objet
router.post('/update-objet', (req, res) => {
const { adresse_ip, niveau, etat } = req.body;
const sql = 'UPDATE objet SET niveau = ?, etat = ? WHERE adresse_ip = ?';
db.query(sql, [niveau, etat, adresse_ip], (err) => {
if (err) {
console.error("Erreur lors de la modification de l'objet :", err);
return res.status(500).send("Erreur lors de la modification de l'objet");
}
res.redirect('/dashboard-complexe');
});
});
// Supprimer un objet (et ses dépendances)
router.post('/supprimer-objet', (req, res) => {
const { adresse_ip } = req.body;
console.log('Adresse IP reçue pour suppression :', adresse_ip);
const deleteChildTables = [
'climatisation_chauffage',
'thermostat',
'appareil_menager',
'securite'
];
const deleteFromTable = (table) => {
return new Promise((resolve, reject) => {
const sql = `DELETE FROM ${table} WHERE adresse_ip = ?`;
db.query(sql, [adresse_ip], (err) => {
if (err) {
console.error(`Erreur suppression dans ${table} :`, err);
return reject(err);
}
resolve();
});
});
};
Promise.all(deleteChildTables.map(deleteFromTable))
.then(() => {
db.query('DELETE FROM objet WHERE adresse_ip = ?', [adresse_ip], (err) => {
if (err) {
console.error('Erreur suppression objet :', err);
return res.status(500).send("Erreur lors de la suppression de l'objet");
}
res.redirect('/dashboard-complexe');
});
})
.catch(err => {
res.status(500).send("Erreur lors de la suppression d'une dépendance liée à l'objet");
});
});
// Ajouter un objet
router.post('/ajouter-objet', (req, res) => {
const { denomination, adresse_ip, type, niveau, etat } = req.body;
const sql = `
INSERT INTO objet (denomination, adresse_ip, type, niveau, etat)
VALUES (?, ?, ?, ?, ?)`;
db.query(sql, [denomination, adresse_ip, type, niveau, etat], (err) => {
if (err) {
console.error('Erreur ajout objet :', err);
return res.status(500).send("Erreur lors de l'ajout de l'objet");
}
res.redirect('/dashboard-complexe');
});
});
module.exports = router;

View File

@ -0,0 +1,52 @@
const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const db = require('../config/db');
// Connexion - Formulaire
router.get('/', (req, res) => {
res.render('connexion');
});
// Connexion - Traitement
router.post('/', (req, res) => {
const { identifiant, mot_de_passe } = req.body;
const sql = 'SELECT * FROM utilisateur WHERE identifiant = ?';
db.query(sql, [identifiant], async (err, results) => {
if (err) {
console.error('Erreur lors de la requête :', err);
return res.send('Erreur serveur');
}
if (results.length === 0) {
return res.send('Identifiant incorrect');
}
const utilisateur = results[0];
const match = await bcrypt.compare(mot_de_passe, utilisateur.mot_de_passe);
if (!match) {
return res.send('Mot de passe incorrect');
}
// Enregistrement en session
req.session.utilisateur = utilisateur;
console.log("Utilisateur connecté :", req.session.utilisateur);
// Redirection selon le statut
switch (utilisateur.statut) {
case 'administrateur':
return res.redirect('/admin');
case 'complexe':
return res.redirect('/dashboard-complexe');
case 'simple':
return res.redirect('/objets'); // 🔄 Redirection mise à jour ici
case 'visiteur':
default:
return res.redirect('/');
}
});
});
module.exports = router;

View File

@ -0,0 +1,44 @@
const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const db = require('../config/db');
const multer = require('multer');
const path = require('path');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = path.join(__dirname, '../img');
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}_${file.originalname}`;
cb(null, uniqueName);
}
});
const upload = multer({ storage });
router.get('/', (req, res) => {
res.render('inscription');
});
router.post('/', upload.single('photo_profil'), async (req, res) => {
const { nom, prenom, sexe, age, date_naissance, identifiant, mot_de_passe, situation, email } = req.body;
const photo = req.file ? req.file.filename : null;
const hashedPassword = await bcrypt.hash(mot_de_passe, 10);
const sql = `INSERT INTO utilisateur (nom, prenom, sexe, age, date_naissance, identifiant, mot_de_passe, photo, situation, email)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
db.query(sql, [nom, prenom, sexe, age, date_naissance, identifiant, hashedPassword, photo, situation, email], (err, result) => {
if (err) {
console.error(err);
return res.send('Erreur lors de linscription');
}
res.redirect('/connexion');
});
});
module.exports = router;

View File

@ -0,0 +1,56 @@
const express = require('express');
const router = express.Router();
const ensureAuthenticated = require('../middleware/auth');
const db = require('../config/db');
const multer = require('multer');
const bcrypt = require('bcrypt');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'photos');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + '-' + file.originalname); // Nom unique pour éviter les conflits
}
});
const upload = multer({
storage: storage,
limits: { fileSize: 5 * 1024 * 1024 }, // Limite de taille (5 Mo)
}).fields([
{ name: 'photo', maxCount: 1 }, // Champ pour le fichier
]);
router.get('/', ensureAuthenticated, (req, res) => {
const utilisateur = req.session.utilisateur;
res.render('profil', { utilisateur });
});
router.post('/update', ensureAuthenticated, upload, (req, res) => {
const mot_de_passe = req.body.mot_de_passe ? bcrypt.hashSync(req.body.mot_de_passe, 10) : req.session.utilisateur.mot_de_passe;
const { identifiant, nom, prenom, sexe, situation } = req.body;
const photo = req.files && req.files.photo ? req.files.photo[0].filename : req.session.utilisateur.photo;
const id = req.session.utilisateur.id;
const sql = 'UPDATE utilisateur SET identifiant = ?, nom = ?, prenom = ?, sexe = ?, situation = ?, photo = ?, mot_de_passe = ? WHERE id = ?';
db.query(sql, [identifiant, nom, prenom, sexe, situation, photo, mot_de_passe, id], (err, result) => {
if (err) {
console.error(err);
return res.status(500).send("Erreur lors de la mise à jour");
}
req.session.utilisateur.identifiant = identifiant;
req.session.utilisateur.nom = nom;
req.session.utilisateur.prenom = prenom;
req.session.utilisateur.sexe = sexe;
req.session.utilisateur.situation = situation;
req.session.utilisateur.photo = photo;
req.session.utilisateur.mot_de_passe = mot_de_passe;
res.redirect('/profil');
});
});
module.exports = router;

156
sk1/webapp/user.sql Normal file
View File

@ -0,0 +1,156 @@
-- Création de la base de données et sélection
CREATE DATABASE IF NOT EXISTS user;
USE user;
SET NAMES utf8mb4;
-- Table `utilisateur`
CREATE TABLE IF NOT EXISTS `utilisateur` (
`id` INT NOT NULL AUTO_INCREMENT,
`nom` VARCHAR(100) NOT NULL,
`prenom` VARCHAR(100) NOT NULL,
`sexe` ENUM('Homme', 'Femme', 'Autre') NOT NULL,
`age` INT DEFAULT NULL,
`date_naissance` DATE NOT NULL,
`identifiant` VARCHAR(50) NOT NULL UNIQUE,
`email` VARCHAR(255) NOT NULL UNIQUE,
`mot_de_passe` VARCHAR(255) NOT NULL,
`photo` VARCHAR(255) DEFAULT NULL,
`situation` VARCHAR(255) DEFAULT NULL,
`etat` ENUM('en attente', 'validé') DEFAULT 'en attente',
`statut` ENUM('visiteur','simple','complexe','administrateur') DEFAULT 'visiteur',
PRIMARY KEY (`id`)
);
INSERT INTO `utilisateur` VALUES
(1, 'Dupont', 'Jean', 'Homme', 30, '1994-05-12', 'jdupont', 'jean.dupont@mail.com', '$2b$10$sOFuc/cZzpK1P8fINfmsAOQ33K0xyCeEXD.OCilFH0SPyuMC.3LNC', './photos/jean.jpg', 'Ingénieur', 'validé', 'complexe'), -- mots de passe dupont
(2, 'Martin', 'Sophie', 'Femme', 25, '1999-08-21', 'smartin', 'sophie.martin@mail.com', '$2b$10$ydWLQyCvL8PVAN18KCWBWe.oMdtm7Y.R4oCVVJenP9fJqfyBLR26C', './photos/sophie.jpg', 'Étudiante', 'en attente', 'visiteur'), -- mots de passe martin
(3, 'Durand', 'Paul', 'Homme', 45, '1979-11-30', 'pdurand', 'paul.durand@mail.com', '$2b$10$HnhXjkKOPNLE.2erAwzg.e5zVVRK/wNeegd8GrNxWv2f3U660xQbW', './photos/paul.jpg', 'Directeur', 'validé', 'administrateur'), -- mots de passe durand
(4, 'Bernard', 'Alice', 'Femme', 28, '1996-02-15', 'aliceb', 'alice.bernard@mail.com', '$2b$10$6BslSk7oJc6fGzQx3YQRHu6Op8xtVQIGFznA0vsmZX40Ix4ywBTfy', './photos/alice.jpg', 'Développeuse', 'validé', 'simple'), -- mots de passe bernard
(5, 'Lemoine', 'Alex', 'Autre', 32, '1992-07-19', 'alemoine', 'alex.lemoine@mail.com', '$2b$10$lZebS.LIRJi9Z5yDaLSNxujY0piysZAb9BRjGOLVW3Hr2wDPoYTgO', './photos/alex.jpg', 'Freelance', 'en attente', 'simple'), -- mots de passe lemoine
(6, 'Arricastres', 'Guillaume', 'Homme', 26, '1998-01-15', 'garricastres', 'guillaume.arricastres@mail.com', '$2b$10$StE4wQ2Mox/CUpuR.o/1nObQMT/rAAndmKXOgAJ7r.V/YGvEs7isy', './photos/guillaume.jpg', 'Étudiant', 'validé', 'simple'), -- mots de passe 1234
(7, 'Chosson', 'Clément', 'Homme', 25, '1999-06-15', 'clement_cx', 'clement.chosson@mail.com', '$2b$10$StE4wQ2Mox/CUpuR.o/1nObQMT/rAAndmKXOgAJ7r.V/YGvEs7isy', NULL, 'Technicien', 'validé', 'complexe'), -- mots de passe 1234
(8, 'Admin', 'Root', 'Homme', 30, '1993-12-31', 'admin', 'admin@smartbuilding.com', '$2b$10$StE4wQ2Mox/CUpuR.o/1nObQMT/rAAndmKXOgAJ7r.V/YGvEs7isy', NULL, 'Administrateur', 'validé', 'administrateur'); -- mots de passe 1234
-- Table `objet`
CREATE TABLE IF NOT EXISTS `objet` (
`adresse_ip` VARCHAR(45) NOT NULL,
`niveau` ENUM('débutant','intermédiaire','avancé','expert') NOT NULL,
`denomination` VARCHAR(255) NOT NULL,
`etat` ENUM('Actif','Inactif') NOT NULL,
`derniere_interaction` DATETIME DEFAULT NULL,
`type` ENUM('Climatisation','Lumière','thermostat','securite','Enceinte connectée','Capteur','appareil_menager','Prise') NOT NULL,
PRIMARY KEY (`adresse_ip`)
);
INSERT INTO `objet` VALUES
('192.168.1.10', 'débutant', 'Ampoule connectée', 'Inactif', '2025-04-08 10:40:50', 'Lumière'),
('192.168.1.11', 'débutant', 'Cuisinière', 'Inactif', '2025-04-08 10:40:50', 'appareil_menager'),
('192.168.1.12', 'débutant', 'Machine à café', 'Inactif', '2025-04-08 10:40:50', 'appareil_menager'),
('192.168.1.20', 'intermédiaire', 'Thermostat intelligent', 'Actif', '2025-04-08 10:40:50', 'thermostat'),
('192.168.1.30', 'avancé', 'Caméra de surveillance', 'Actif', '2025-04-08 10:40:50', 'Capteur'),
('192.168.1.40', 'débutant', 'Machine à laver', 'Inactif', '2025-04-08 10:40:50', 'appareil_menager'),
('192.168.1.50', 'débutant', 'Enceinte intelligente', 'Inactif', '2025-04-08 10:40:50', 'Enceinte connectée'),
('192.168.1.60', 'intermédiaire', 'Prise connectée', 'Actif', '2025-04-08 10:40:50', 'Prise'),
('192.168.1.70', 'avancé', 'Serrure connectée', 'Actif', '2025-04-08 10:40:50', 'securite'),
('192.168.1.80', 'expert', 'Porte connectée salle de contrôle', 'Actif', '2025-04-08 10:40:50', 'securite'),
('192.168.1.90', 'avancé', 'Climatisation', 'Actif', '2025-04-08 10:40:50', 'Climatisation');
-- Table `thermostat`
CREATE TABLE `thermostat` (
`adresse_ip` VARCHAR(45) NOT NULL,
`temperature_actuelle` FLOAT DEFAULT NULL,
`temperature_cible` FLOAT DEFAULT NULL,
`mode` ENUM('Automatique','Manuel') NOT NULL,
PRIMARY KEY (`adresse_ip`),
FOREIGN KEY (`adresse_ip`) REFERENCES `objet` (`adresse_ip`)
);
INSERT INTO `thermostat` VALUES
('192.168.1.20', 17, 20, 'Automatique');
-- Table `climatisation_chauffage`
CREATE TABLE `climatisation_chauffage` (
`adresse_ip` VARCHAR(45) NOT NULL,
`thermostat` VARCHAR(45) NOT NULL,
PRIMARY KEY (`adresse_ip`),
FOREIGN KEY (`adresse_ip`) REFERENCES `objet` (`adresse_ip`),
FOREIGN KEY (`thermostat`) REFERENCES `thermostat` (`adresse_ip`)
);
INSERT INTO `climatisation_chauffage` VALUES
('192.168.1.90', '192.168.1.20');
-- Table `appareil_menager`
CREATE TABLE `appareil_menager` (
`adresse_ip` VARCHAR(45) NOT NULL,
`depart_differe` TIME DEFAULT NULL,
`etat_de_la_tache` ENUM('Complété','En cours','En attente','Aucune') NOT NULL,
PRIMARY KEY (`adresse_ip`),
FOREIGN KEY (`adresse_ip`) REFERENCES `objet` (`adresse_ip`) ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO `appareil_menager` VALUES
('192.168.1.11', NULL, 'Aucune'),
('192.168.1.12', NULL, 'Aucune'),
('192.168.1.40', NULL, 'Aucune');
-- Table `securite`
CREATE TABLE `securite` (
`adresse_ip` VARCHAR(45) NOT NULL,
`situation` ENUM('Verouillé','Ouvert') NOT NULL,
PRIMARY KEY (`adresse_ip`),
FOREIGN KEY (`adresse_ip`) REFERENCES `objet` (`adresse_ip`) ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO `securite` VALUES
('192.168.1.70', 'Ouvert'),
('192.168.1.80', 'Verouillé');
-- Table `gestion`
CREATE TABLE `gestion` (
`id` INT NOT NULL AUTO_INCREMENT,
`id_utilisateur` INT NOT NULL,
`competence` VARCHAR(255) NOT NULL,
`niveau` ENUM('débutant','intermédiaire','avancé','expert') NOT NULL,
`categorie_experience` FLOAT DEFAULT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`id_utilisateur`) REFERENCES `utilisateur` (`id`) ON DELETE CASCADE
);
INSERT INTO `gestion` (`id`, `id_utilisateur`, `competence`, `niveau`, `categorie_experience`) VALUES
(1, 1, 'Informatique', 'expert', 5),
(2, 1, 'Electronique', 'avancé', 3.5),
(3, 2, 'Internet', 'intermédiaire', 2),
(4, 2, 'Assistance numerique', 'débutant', 1),
(5, 3, 'Cuisine', 'expert', 10),
(6, 3, 'Aménagement', 'avancé', 7),
(7, 4, 'Securite', 'avancé', 4),
(8, 4, 'Acceuil', 'intermédiaire', 2.5),
(9, 5, 'Entretien', 'intermédiaire', 3),
(10, 5, 'Securite', 'débutant', 1.5);
CREATE TABLE `ressources` (
`id` int NOT NULL,
`nom` varchar(50) NOT NULL,
`consommation` varchar(50) NOT NULL,
`consommation_max` varchar(50) NOT NULL,
`fournisseur` varchar(100) NOT NULL,
`abonnement` varchar(100) NOT NULL,
`echeance` varchar(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `ressources` (`id`, `nom`, `consommation`, `consommation_max`, `fournisseur`, `abonnement`, `echeance`) VALUES
(0, 'Électricité', '250 kWh', '500 kWh', 'EDF', '50 € / mois', '2025-12-30'),
(1, 'Gaz', '90 m³', '200 m³', 'Engie', '40 € / mois', '2025-06-30'),
(2, 'Eau', '1200 L', '3000 L', 'Veolia', '20 € / mois', '2025-08-15');
CREATE TABLE IF NOT EXISTS contact (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
date_envoi TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Page d'accueil - Smart Building</title>
<link rel="stylesheet" href="/style.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"
rel="stylesheet"
/>
<script
src="https://kit.fontawesome.com/a076d05399.js"
crossorigin="anonymous"
></script>
</head>
<body>
<%- include('partials/header') %>
<main>
<section class="left-panel">
<img src="/images/batiment.png" alt="Bâtiment" />
</section>
<section class="right-panel">
<h1>Accueil</h1>
<div class="features">
<a href="/objets" style="text-decoration: none">
<div class="feature">
<div class="circle">
<i class="fas fa-cogs"></i>
</div>
<p>Objets connectés</p>
</div>
</a>
<a href="/membres" style="text-decoration: none">
<div class="feature">
<div class="circle">
<i class="fas fa-users"></i>
</div>
<p>Membres</p>
</div>
</a>
<a href="/ressources" class="text-decoration: none">
<div class="feature">
<div class="circle">
<i class="fas fa-map-marker-alt"></i>
</div>
<p>Ressources</p>
</div>
</a>
<a href="/description" class="feature">
<div class="circle">
<i class="fas fa-building"></i>
</div>
<p>Description bâtiment</p>
</a>
</div>
</section>
</main>
<%- include('partials/footer') %>
</body>
</html>

401
sk1/webapp/views/admin.ejs Normal file
View File

@ -0,0 +1,401 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Admin - Smart Building</title>
<link rel="stylesheet" href="/styleAdmin.css" />
<script>
function toggleForm(id) {
const form = document.getElementById(id);
form.style.display = form.style.display === 'none' ? 'block' : 'none';
}
</script>
</head>
<body>
<%- include('partials/header') %>
<main class="admin-container">
<h1>Gestion des utilisateurs</h1>
<div class="ajout-section">
<h2>Ajouter un utilisateur <span class="toggle-btn" onclick="toggleForm('form-ajout-user')"></span></h2>
<form id="form-ajout-user" action="/admin/ajouter-utilisateur" method="POST" style="display: none;">
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Zm6-10.125a1.875 1.875 0 1 1-3.75 0 1.875 1.875 0 0 1 3.75 0Zm1.294 6.336a6.721 6.721 0 0 1-3.17.789 6.721 6.721 0 0 1-3.168-.789 3.376 3.376 0 0 1 6.338 0Z"
/>
</svg>
</span>
<input type="text" name="nom" class="form-control-admin" placeholder="Nom" required />
</div>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Zm6-10.125a1.875 1.875 0 1 1-3.75 0 1.875 1.875 0 0 1 3.75 0Zm1.294 6.336a6.721 6.721 0 0 1-3.17.789 6.721 6.721 0 0 1-3.168-.789 3.376 3.376 0 0 1 6.338 0Z"
/>
</svg>
</span>
<input type="text" name="prenom" class="form-control-admin" placeholder="Prénom" required />
</div>
<div class="form-row">
<label>Sexe / Genre :</label>
<div class="form-group">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
<path
d="M176 288a112 112 0 1 0 0-224 112 112 0 1 0 0 224zM352 176c0 86.3-62.1 158.1-144 173.1l0 34.9 32 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-32 0 0 32c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-32-32 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l32 0 0-34.9C62.1 334.1 0 262.3 0 176C0 78.8 78.8 0 176 0s176 78.8 176 176zM271.9 360.6c19.3-10.1 36.9-23.1 52.1-38.4c20 18.5 46.7 29.8 76.1 29.8c61.9 0 112-50.1 112-112s-50.1-112-112-112c-7.2 0-14.3 .7-21.1 2c-4.9-21.5-13-41.7-24-60.2C369.3 66 384.4 64 400 64c37 0 71.4 11.4 99.8 31l20.6-20.6L487 41c-6.9-6.9-8.9-17.2-5.2-26.2S494.3 0 504 0L616 0c13.3 0 24 10.7 24 24l0 112c0 9.7-5.8 18.5-14.8 22.2s-19.3 1.7-26.2-5.2l-33.4-33.4L545 140.2c19.5 28.4 31 62.7 31 99.8c0 97.2-78.8 176-176 176c-50.5 0-96-21.3-128.1-55.4z"
/>
</svg>
</span>
<select name="sexe" class="form-control-admin">
<option value="Homme">Homme</option>
<option value="Femme">Femme</option>
<option value="Autre">Autre</option></select
><br />
</div>
</div>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 0 24 24"
width="24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M12 6c1.11 0 2-.9 2-2 0-.38-.1-.73-.29-1.03L12 0l-1.71 2.97c-.19.3-.29.65-.29 1.03 0 1.1.9 2 2 2zm4.6 9.99l-1.07-1.07-1.08 1.07c-1.3 1.3-3.58 1.31-4.89 0l-1.07-1.07-1.09 1.07C6.75 16.64 5.88 17 4.96 17c-.73 0-1.4-.23-1.96-.61V21c0 .55.45 1 1 1h16c.55 0 1-.45 1-1v-4.61c-.56.38-1.23.61-1.96.61-.92 0-1.79-.36-2.44-1.01zM18 9h-5V7h-2v2H6c-1.66 0-3 1.34-3 3v1.54c0 1.08.88 1.96 1.96 1.96.52 0 1.02-.2 1.38-.57l2.14-2.13 2.13 2.13c.74.74 2.03.74 2.77 0l2.14-2.13 2.13 2.13c.37.37.86.57 1.38.57 1.08 0 1.96-.88 1.96-1.96V12C21 10.34 19.66 9 18 9z"
/>
</svg>
</span>
<input type="number" name="age" class="form-control-admin" placeholder="Âge" required />
</div>
<div class="form-row">
<label>Date de naissance :</label>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e3e3e3"
>
<path
d="M200-80q-33 0-56.5-23.5T120-160v-560q0-33 23.5-56.5T200-800h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840-720v560q0 33-23.5 56.5T760-80H200Zm0-80h560v-400H200v400Zm0-480h560v-80H200v80Zm0 0v-80 80Zm280 240q-17 0-28.5-11.5T440-440q0-17 11.5-28.5T480-480q17 0 28.5 11.5T520-440q0 17-11.5 28.5T480-400Zm-160 0q-17 0-28.5-11.5T280-440q0-17 11.5-28.5T320-480q17 0 28.5 11.5T360-440q0 17-11.5 28.5T320-400Zm320 0q-17 0-28.5-11.5T600-440q0-17 11.5-28.5T640-480q17 0 28.5 11.5T680-440q0 17-11.5 28.5T640-400ZM480-240q-17 0-28.5-11.5T440-280q0-17 11.5-28.5T480-320q17 0 28.5 11.5T520-280q0 17-11.5 28.5T480-240Zm-160 0q-17 0-28.5-11.5T280-280q0-17 11.5-28.5T320-320q17 0 28.5 11.5T360-280q0 17-11.5 28.5T320-240Zm320 0q-17 0-28.5-11.5T600-280q0-17 11.5-28.5T640-320q17 0 28.5 11.5T680-280q0 17-11.5 28.5T640-240Z"
/>
</svg>
</span>
<input type="date" name="date_naissance" class="form-control-admin" required />
</div>
</div>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path
d="M12 12c2.21 0 4-1.79 4-4S14.21 4 12 4 8 5.79 8 8s1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</span>
<input type="text" name="identifiant" class="form-control-admin" placeholder="Identifiant" required />
</div>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path
d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"
/>
</svg>
</span>
<input type="email" name="email" class="form-control-admin" placeholder="Email" required />
</div>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path
d="M12 2C8.691 2 6 4.691 6 8v4c-1.104 0-2 .896-2 2v8c0 1.104.896 2 2 2h12c1.104 0 2-.896 2-2v-8c0-1.104-.896-2-2-2V8c0-3.309-2.691-6-6-6zm0 2c2.205 0 4 1.795 4 4v4H8V8c0-2.205 1.795-4 4-4zm-4 10h8v8H8v-8z"
/>
</svg>
</span>
<input type="password" name="mot_de_passe" class="form-control-admin" placeholder="Mot de passe" required />
</div>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
color="#000000"
stroke-width="1.5"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M20.75 16.7143C20.75 16.7631 20.7453 16.8109 20.7364 16.8571C20.7453 16.9034 20.75 16.9511 20.75 17C20.75 17.4142 20.4142 17.75 20 17.75H6C5.30964 17.75 4.75 18.3096 4.75 19C4.75 19.6904 5.30964 20.25 6 20.25H20C20.4142 20.25 20.75 20.5858 20.75 21C20.75 21.4142 20.4142 21.75 20 21.75H6C4.48122 21.75 3.25 20.5188 3.25 19V5C3.25 3.48122 4.48122 2.25 6 2.25H19.4C20.1456 2.25 20.75 2.85442 20.75 3.6V16.7143ZM9 6.25C8.58579 6.25 8.25 6.58579 8.25 7C8.25 7.41421 8.58579 7.75 9 7.75H15C15.4142 7.75 15.75 7.41421 15.75 7C15.75 6.58579 15.4142 6.25 15 6.25H9Z"
fill="#000000"
></path>
</svg>
</span>
<input type="text" name="situation" class="form-control-admin" placeholder="Situation professionnelle" required />
</div>
<div class="form-row">
<label>Statut :</label>
<div class="form-group">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2a10 10 0 1 0 7.07 2.93A9.95 9.95 0 0 0 12 2zm0 18a8 8 0 1 1 5.66-2.34A7.96 7.96 0 0 1 12 20z"/>
<circle cx="12" cy="12" r="4"/>
</svg>
</span>
<select name="statut" class="form-control-admin">
<option value="visiteur">Visiteur</option>
<option value="simple">Simple</option>
<option value="complexe">Complexe</option>
<option value="administrateur">Administrateur</option></select
><br />
</div>
</div>
<button class="bouton-admin" type="submit">Ajouter</button>
</form>
</div>
<table>
<thead>
<tr>
<th>Nom</th>
<th>Prénom</th>
<th>Statut</th>
<th>État</th>
<th>Modifier</th>
<th>Supprimer</th>
</tr>
</thead>
<tbody>
<% utilisateurs.forEach(user => { %>
<tr>
<form action="/admin/update-user" method="POST">
<input type="hidden" name="id" value="<%= user.id %>" />
<td><%= user.nom %></td>
<td><%= user.prenom %></td>
<td>
<select name="statut">
<option value="visiteur" <%= user.statut === 'visiteur' ? 'selected' : '' %>>Visiteur</option>
<option value="simple" <%= user.statut === 'simple' ? 'selected' : '' %>>Simple</option>
<option value="complexe" <%= user.statut === 'complexe' ? 'selected' : '' %>>Complexe</option>
<option value="administrateur" <%= user.statut === 'administrateur' ? 'selected' : '' %>>Administrateur</option>
</select>
</td>
<td>
<select name="etat">
<option value="en attente" <%= user.etat === 'en attente' ? 'selected' : '' %>>En attente</option>
<option value="validé" <%= user.etat === 'validé' ? 'selected' : '' %>>Validé</option>
</select>
</td>
<td><button type="submit">Modifier</button></td>
</form>
<td>
<form action="/admin/supprimer-user" method="POST" onsubmit="return confirm('Supprimer cet utilisateur ?')">
<input type="hidden" name="id" value="<%= user.id %>">
<button type="submit" style="background-color: #e74c3c; color: white;">Supprimer</button>
</form>
</td>
</tr>
<% }) %>
</tbody>
</table>
<h1>Gestion des objets</h1>
<div class="ajout-section">
<h2>Ajouter un objet <span class="toggle-btn" onclick="toggleForm('form-ajout-objet')"></span></h2>
<form id="form-ajout-objet" action="/admin/ajouter-objet" method="POST" style="display: none;">
<div class="ajout-section">
<div class="form-group">
<span class="icon">
<svg id="Layer_1" height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m19 4h-4v-1a3 3 0 0 0 -6 0v1h-4a5.006 5.006 0 0 0 -5 5v10a5.006 5.006 0 0 0 5 5h14a5.006 5.006 0 0 0 5-5v-10a5.006 5.006 0 0 0 -5-5zm-8-1a1 1 0 0 1 2 0v2a1 1 0 0 1 -2 0zm11 16a3 3 0 0 1 -3 3h-14a3 3 0 0 1 -3-3v-10a3 3 0 0 1 3-3h4.184a2.982 2.982 0 0 0 5.632 0h4.184a3 3 0 0 1 3 3zm-12-9h-5a1 1 0 0 0 -1 1v8a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-8a1 1 0 0 0 -1-1zm-1 8h-3v-6h3zm11-3a1 1 0 0 1 -1 1h-5a1 1 0 0 1 0-2h5a1 1 0 0 1 1 1zm0-4a1 1 0 0 1 -1 1h-5a1 1 0 0 1 0-2h5a1 1 0 0 1 1 1zm-2 8a1 1 0 0 1 -1 1h-3a1 1 0 0 1 0-2h3a1 1 0 0 1 1 1z"/></svg>
</span>
<input type="text" name="denomination" class="form-control-admin" placeholder="Nom" required />
</div>
<div class="form-group">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24">
<path d="m19.07,2.929c-1.889-1.889-4.399-2.929-7.07-2.929s-5.183,1.04-7.071,2.929c-3.899,3.899-3.899,10.243.008,14.149l7.063,6.909,7.07-6.917c1.89-1.889,2.93-4.4,2.93-7.071s-1.04-5.182-2.93-7.071Zm-1.406,12.72l-5.664,5.541-5.657-5.533c-1.511-1.511-2.343-3.52-2.343-5.657s.832-4.146,2.343-5.657,3.521-2.343,5.657-2.343,4.146.832,5.656,2.343c1.512,1.511,2.344,3.52,2.344,5.657s-.832,4.146-2.336,5.649ZM8.9,6h1.6v8h-1.6V6Zm5.6,0h-2.5v8h1.6v-3h.9c1.381,0,2.5-1.119,2.5-2.5s-1.119-2.5-2.5-2.5Zm0,3.4h-.9v-1.801h.9c.497,0,.9.403.9.9s-.403.9-.9.9Z"/>
</svg>
</span>
<input type="text" name="adresse_ip" class="form-control-admin" placeholder="Adresse IP" required />
</div>
<div class="form-row">
<label>Type :</label>
<div class="form-group">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512" height="512"><g id="_01_align_center" data-name="01 align center"><path d="M15,24H9V20.487a9,9,0,0,1-2.849-1.646L3.107,20.6l-3-5.2L3.15,13.645a9.1,9.1,0,0,1,0-3.29L.107,8.6l3-5.2L6.151,5.159A9,9,0,0,1,9,3.513V0h6V3.513a9,9,0,0,1,2.849,1.646L20.893,3.4l3,5.2L20.85,10.355a9.1,9.1,0,0,1,0,3.29L23.893,15.4l-3,5.2-3.044-1.758A9,9,0,0,1,15,20.487Zm-4-2h2V18.973l.751-.194A6.984,6.984,0,0,0,16.994,16.9l.543-.553,2.623,1.515,1-1.732-2.62-1.513.206-.746a7.048,7.048,0,0,0,0-3.75l-.206-.746,2.62-1.513-1-1.732L17.537,7.649,16.994,7.1a6.984,6.984,0,0,0-3.243-1.875L13,5.027V2H11V5.027l-.751.194A6.984,6.984,0,0,0,7.006,7.1l-.543.553L3.84,6.134l-1,1.732L5.46,9.379l-.206.746a7.048,7.048,0,0,0,0,3.75l.206.746L2.84,16.134l1,1.732,2.623-1.515.543.553a6.984,6.984,0,0,0,3.243,1.875l.751.194Zm1-6a4,4,0,1,1,4-4A4,4,0,0,1,12,16Zm0-6a2,2,0,1,0,2,2A2,2,0,0,0,12,10Z"/></g></svg> </span>
<select name="type" class="form-control-admin" required>
<option value="Climatisation">Climatisation</option>
<option value="Lumière">Lumière</option>
<option value="thermostat">Thermostat</option>
<option value="securite">Sécurité</option>
<option value="Enceinte connectée">Enceinte connectée</option>
<option value="Capteur">Capteur</option>
<option value="appareil_menager">Appareil ménager</option>
<option value="Prise">Prise</option></select
><br />
</div>
</div>
<div class="form-row">
<label>Niveau :</label>
<div class="form-group">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" id="Outline" viewBox="0 0 24 24" width="512" height="512"><path d="M1,4.75H3.736a3.728,3.728,0,0,0,7.195,0H23a1,1,0,0,0,0-2H10.931a3.728,3.728,0,0,0-7.195,0H1a1,1,0,0,0,0,2ZM7.333,2a1.75,1.75,0,1,1-1.75,1.75A1.752,1.752,0,0,1,7.333,2Z"/><path d="M23,11H20.264a3.727,3.727,0,0,0-7.194,0H1a1,1,0,0,0,0,2H13.07a3.727,3.727,0,0,0,7.194,0H23a1,1,0,0,0,0-2Zm-6.333,2.75A1.75,1.75,0,1,1,18.417,12,1.752,1.752,0,0,1,16.667,13.75Z"/><path d="M23,19.25H10.931a3.728,3.728,0,0,0-7.195,0H1a1,1,0,0,0,0,2H3.736a3.728,3.728,0,0,0,7.195,0H23a1,1,0,0,0,0-2ZM7.333,22a1.75,1.75,0,1,1,1.75-1.75A1.753,1.753,0,0,1,7.333,22Z"/></svg> </span>
<select name="niveau" class="form-control-admin" required>
<option value="débutant">Débutant</option>
<option value="intermédiaire">Intermédiaire</option>
<option value="avancé">Avancé</option>
<option value="expert">Expert</option></select
><br />
</div>
</div>
<div class="form-row">
<label>État :</label>
<div class="form-group">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2a10 10 0 1 0 7.07 2.93A9.95 9.95 0 0 0 12 2zm0 18a8 8 0 1 1 5.66-2.34A7.96 7.96 0 0 1 12 20z"/>
<circle cx="12" cy="12" r="4"/>
</svg>
</span>
<select name="etat" class="form-control-admin">
<option value="Actif">Actif</option>
<option value="Inactif">Inactif</option></select
><br />
</div>
</div>
<button class="bouton-admin" type="submit">Ajouter</button>
</form>
</div>
<table>
<thead>
<tr>
<th>Nom</th>
<th>Adresse IP</th>
<th>Type</th>
<th>Niveau</th>
<th>État</th>
<th>Modifier</th>
<th>Supprimer</th>
</tr>
</thead>
<tbody>
<% objets.forEach(objet => { %>
<tr>
<form action="/admin/update-objet" method="POST">
<input type="hidden" name="adresse_ip" value="<%= objet.adresse_ip %>" />
<td><%= objet.denomination %></td>
<td><%= objet.adresse_ip %></td>
<td><%= objet.type %></td>
<td>
<select name="niveau">
<option value="débutant" <%= objet.niveau === 'débutant' ? 'selected' : '' %>>Débutant</option>
<option value="intermédiaire" <%= objet.niveau === 'intermédiaire' ? 'selected' : '' %>>Intermédiaire</option>
<option value="avancé" <%= objet.niveau === 'avancé' ? 'selected' : '' %>>Avancé</option>
<option value="expert" <%= objet.niveau === 'expert' ? 'selected' : '' %>>Expert</option>
</select>
</td>
<td>
<select name="etat">
<option value="Actif" <%= objet.etat === 'Actif' ? 'selected' : '' %>>Actif</option>
<option value="Inactif" <%= objet.etat === 'Inactif' ? 'selected' : '' %>>Inactif</option>
</select>
</td>
<td><button type="submit">Modifier</button></td>
</form>
<td>
<form action="/admin/supprimer-objet" method="POST" onsubmit="return confirm('Supprimer cet objet ?')">
<input type="hidden" name="adresse_ip" value="<%= objet.adresse_ip %>">
<button type="submit" style="background-color: #e74c3c; color: white;">Supprimer</button>
</form>
</td>
</tr>
<% }) %>
</tbody>
</table>
<h1>Messages de contact</h1>
<table>
<thead>
<tr>
<th>Nom</th>
<th>Email</th>
<th>Message</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<% contacts.forEach(c => { %>
<tr>
<td><%= c.nom %></td>
<td><%= c.email %></td>
<td><%= c.message %></td>
<td><%= new Date(c.date_envoi).toLocaleString('fr-FR') %></td>
</tr>
<% }) %>
</tbody>
</table>
</main>
<%- include('partials/footer') %>
</body>
</html>

View File

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Page d'accueil - Smart Building</title>
<link rel="stylesheet" href="/styleConnexion.css" />
</head>
<body>
<%- include('partials/header') %>
<div id="connexion">
<div class="building"></div>
<div class="cellule">
<div class="page-connexion">
<h1>Connexion</h1>
<form action="/connexion" method="POST">
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path
d="M12 12c2.21 0 4-1.79 4-4S14.21 4 12 4 8 5.79 8 8s1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</span>
<input
class="form-control-connexion"
type="text"
id="identifiant"
name="identifiant"
placeholder="Identifiant"
required
/><br /><br />
</div>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path
d="M12 2C8.691 2 6 4.691 6 8v4c-1.104 0-2 .896-2 2v8c0 1.104.896 2 2 2h12c1.104 0 2-.896 2-2v-8c0-1.104-.896-2-2-2V8c0-3.309-2.691-6-6-6zm0 2c2.205 0 4 1.795 4 4v4H8V8c0-2.205 1.795-4 4-4zm-4 10h8v8H8v-8z"
/>
</svg>
</span>
<input
class="form-control-connexion"
type="password"
id="mot_de_passe"
name="mot_de_passe"
placeholder="Mot de passe"
required
/><br /><br />
</div>
<input class="bouton-connexion" type="submit" value="Connexion" />
</form>
</div>
</div>
</div>
<%- include('partials/footer') %>
</body>
</html>

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Contact - Smart Building</title>
<link rel="stylesheet" href="/styleContact.css" />
</head>
<body>
<%- include('partials/header') %>
<main class="contact-container">
<h1>Nous contacter</h1>
<p>Une question ? Une suggestion ? N'hésitez pas à nous écrire !</p>
<form action="/contact" method="POST" class="contact-form">
<label for="nom">Nom</label>
<input type="text" id="nom" name="nom" required />
<label for="email">Adresse e-mail</label>
<input type="email" id="email" name="email" required />
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
<button type="submit">Envoyer</button>
</form>
</main>
<%- include('partials/footer') %>
</body>
</html>

View File

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Dashboard Complexe - Smart Building</title>
<link rel="stylesheet" href="/styleComplexe.css" />
<script>
function toggleForm(id) {
const form = document.getElementById(id);
form.style.display = form.style.display === 'none' ? 'block' : 'none';
}
</script>
</head>
<body>
<%- include('partials/header') %>
<main class="admin-container">
<h1>Gestion des objets</h1>
<div class="ajout-section">
<h2>Ajouter un objet <span class="toggle-btn" onclick="toggleForm('form-ajout-objet')"></span></h2>
<form id="form-ajout-objet" action="/complexe/ajouter-objet" method="POST" style="display: none;">
<input type="text" name="denomination" placeholder="Nom" required />
<input type="text" name="adresse_ip" placeholder="Adresse IP" required />
<select name="type" required>
<option value="Climatisation">Climatisation</option>
<option value="Lumière">Lumière</option>
<option value="thermostat">Thermostat</option>
<option value="securite">Sécurité</option>
<option value="Enceinte connectée">Enceinte connectée</option>
<option value="Capteur">Capteur</option>
<option value="appareil_menager">Appareil ménager</option>
<option value="Prise">Prise</option>
</select>
<select name="niveau">
<option value="débutant">Débutant</option>
<option value="intermédiaire">Intermédiaire</option>
<option value="avancé">Avancé</option>
<option value="expert">Expert</option>
</select>
<select name="etat">
<option value="Actif">Actif</option>
<option value="Inactif">Inactif</option>
</select>
<button type="submit">Ajouter</button>
</form>
</div>
<table>
<thead>
<tr>
<th>Nom</th>
<th>Adresse IP</th>
<th>Type</th>
<th>Niveau</th>
<th>État</th>
<th>Modifier</th>
<th>Supprimer</th>
</tr>
</thead>
<tbody>
<% objets.forEach(objet => { %>
<tr>
<form action="/complexe/update-objet" method="POST">
<input type="hidden" name="adresse_ip" value="<%= objet.adresse_ip %>" />
<td><%= objet.denomination %></td>
<td><%= objet.adresse_ip %></td>
<td><%= objet.type %></td>
<td>
<select name="niveau">
<option value="débutant" <%= objet.niveau === 'débutant' ? 'selected' : '' %>>Débutant</option>
<option value="intermédiaire" <%= objet.niveau === 'intermédiaire' ? 'selected' : '' %>>Intermédiaire</option>
<option value="avancé" <%= objet.niveau === 'avancé' ? 'selected' : '' %>>Avancé</option>
<option value="expert" <%= objet.niveau === 'expert' ? 'selected' : '' %>>Expert</option>
</select>
</td>
<td>
<select name="etat">
<option value="Actif" <%= objet.etat === 'Actif' ? 'selected' : '' %>>Actif</option>
<option value="Inactif" <%= objet.etat === 'Inactif' ? 'selected' : '' %>>Inactif</option>
</select>
</td>
<td>
<button type="submit">Modifier</button>
</td>
</form>
<td>
<form action="/complexe/supprimer-objet" method="POST" onsubmit="return confirm('Supprimer cet objet ?')">
<input type="hidden" name="adresse_ip" value="<%= objet.adresse_ip %>">
<button type="submit" style="background-color: #e74c3c; color: white;">Supprimer</button>
</form>
</td>
</tr>
<% }) %>
</tbody>
</table>
</main>
<%- include('partials/footer') %>
</body>
</html>

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Présentation de la maison - Smart Building</title>
<link rel="stylesheet" href="/styleObjets.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet" />
</head>
<body>
<%- include('partials/header') %>
<main>
<section class="left-panel">
<img src="/images/batiment.png" alt="Image de la maison connectée" />
</section>
<section class="right-panel">
<div class="header-objets">
<h1>Présentation de la maison connectée</h1>
</div>
<div style="padding: 0 70px; font-size: 18px; line-height: 1.8;">
<p>
Notre maison connectée est conçue pour offrir confort, sécurité et efficacité énergétique. Équipée de capteurs intelligents, dun système de gestion centralisée et dune connectivité en temps réel, elle permet une surveillance continue des équipements et une optimisation automatique des ressources.
</p>
<p>
Avec une surface habitable estimée à environ 300 m² répartie sur 4 à 5 niveaux, la maison connectée propose de vastes espaces ouverts et lumineux. Chaque étage est conçu pour maximiser lapport en lumière naturelle grâce aux nombreuses baies vitrées. Les terrasses végétalisées apportent un lien direct avec la nature, tout en favorisant lisolation thermique.
</p>
<p>
Le bâtiment comprend de nombreux équipements connectés tels que léclairage intelligent, le chauffage programmable, des outils ménager et une surveillance vidéo connectée. Toutes les données sont centralisées via une interface web qui permet un contrôle à distance via son smartphone ou son ordinateur. De plus en fonction des droits que possèdes votre compte vous avez accés à différentes fonctionnalités.
</p>
</div>
</section>
</main>
<%- include('partials/footer') %>
</body>
</html>

View File

@ -0,0 +1,273 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Page d'accueil - Smart Building</title>
<link rel="stylesheet" href="/styleInscription.css" />
</head>
<body>
<%- include('partials/header') %>
<div id="inscription">
<div class="case">
<form action="/connexion" method="GET">
<button class="lien" type="submit">
Déjà inscrit ? Se connecter
</button>
</form>
<div class="page-inscription">
<h1>Inscription</h1>
<form
action="/inscription"
method="POST"
enctype="multipart/form-data"
>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Zm6-10.125a1.875 1.875 0 1 1-3.75 0 1.875 1.875 0 0 1 3.75 0Zm1.294 6.336a6.721 6.721 0 0 1-3.17.789 6.721 6.721 0 0 1-3.168-.789 3.376 3.376 0 0 1 6.338 0Z"
/>
</svg>
</span>
<input
type="text"
class="form-control-inscription"
name="nom"
placeholder="Nom"
required
/>
</div>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Zm6-10.125a1.875 1.875 0 1 1-3.75 0 1.875 1.875 0 0 1 3.75 0Zm1.294 6.336a6.721 6.721 0 0 1-3.17.789 6.721 6.721 0 0 1-3.168-.789 3.376 3.376 0 0 1 6.338 0Z"
/>
</svg>
</span>
<input
type="text"
class="form-control-inscription"
name="prenom"
placeholder="Prénom"
required
/>
</div>
<div class="form-row">
<label>Sexe / Genre :</label>
<div class="form-group">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
<path
d="M176 288a112 112 0 1 0 0-224 112 112 0 1 0 0 224zM352 176c0 86.3-62.1 158.1-144 173.1l0 34.9 32 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-32 0 0 32c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-32-32 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l32 0 0-34.9C62.1 334.1 0 262.3 0 176C0 78.8 78.8 0 176 0s176 78.8 176 176zM271.9 360.6c19.3-10.1 36.9-23.1 52.1-38.4c20 18.5 46.7 29.8 76.1 29.8c61.9 0 112-50.1 112-112s-50.1-112-112-112c-7.2 0-14.3 .7-21.1 2c-4.9-21.5-13-41.7-24-60.2C369.3 66 384.4 64 400 64c37 0 71.4 11.4 99.8 31l20.6-20.6L487 41c-6.9-6.9-8.9-17.2-5.2-26.2S494.3 0 504 0L616 0c13.3 0 24 10.7 24 24l0 112c0 9.7-5.8 18.5-14.8 22.2s-19.3 1.7-26.2-5.2l-33.4-33.4L545 140.2c19.5 28.4 31 62.7 31 99.8c0 97.2-78.8 176-176 176c-50.5 0-96-21.3-128.1-55.4z"
/>
</svg>
</span>
<select name="sexe" class="form-control-inscription">
<option value="Homme">Homme</option>
<option value="Femme">Femme</option>
<option value="Autre">Autre</option></select
><br />
</div>
</div>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
height="24"
viewBox="0 0 24 24"
width="24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M12 6c1.11 0 2-.9 2-2 0-.38-.1-.73-.29-1.03L12 0l-1.71 2.97c-.19.3-.29.65-.29 1.03 0 1.1.9 2 2 2zm4.6 9.99l-1.07-1.07-1.08 1.07c-1.3 1.3-3.58 1.31-4.89 0l-1.07-1.07-1.09 1.07C6.75 16.64 5.88 17 4.96 17c-.73 0-1.4-.23-1.96-.61V21c0 .55.45 1 1 1h16c.55 0 1-.45 1-1v-4.61c-.56.38-1.23.61-1.96.61-.92 0-1.79-.36-2.44-1.01zM18 9h-5V7h-2v2H6c-1.66 0-3 1.34-3 3v1.54c0 1.08.88 1.96 1.96 1.96.52 0 1.02-.2 1.38-.57l2.14-2.13 2.13 2.13c.74.74 2.03.74 2.77 0l2.14-2.13 2.13 2.13c.37.37.86.57 1.38.57 1.08 0 1.96-.88 1.96-1.96V12C21 10.34 19.66 9 18 9z"
/>
</svg>
</span>
<input
type="number"
class="form-control-inscription"
name="age"
min="0"
max="120"
placeholder="Âge"
required
/>
</div>
<div class="form-row">
<label>Date de naissance :</label>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e3e3e3"
>
<path
d="M200-80q-33 0-56.5-23.5T120-160v-560q0-33 23.5-56.5T200-800h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840-720v560q0 33-23.5 56.5T760-80H200Zm0-80h560v-400H200v400Zm0-480h560v-80H200v80Zm0 0v-80 80Zm280 240q-17 0-28.5-11.5T440-440q0-17 11.5-28.5T480-480q17 0 28.5 11.5T520-440q0 17-11.5 28.5T480-400Zm-160 0q-17 0-28.5-11.5T280-440q0-17 11.5-28.5T320-480q17 0 28.5 11.5T360-440q0 17-11.5 28.5T320-400Zm320 0q-17 0-28.5-11.5T600-440q0-17 11.5-28.5T640-480q17 0 28.5 11.5T680-440q0 17-11.5 28.5T640-400ZM480-240q-17 0-28.5-11.5T440-280q0-17 11.5-28.5T480-320q17 0 28.5 11.5T520-280q0 17-11.5 28.5T480-240Zm-160 0q-17 0-28.5-11.5T280-280q0-17 11.5-28.5T320-320q17 0 28.5 11.5T360-280q0 17-11.5 28.5T320-240Zm320 0q-17 0-28.5-11.5T600-280q0-17 11.5-28.5T640-320q17 0 28.5 11.5T680-280q0 17-11.5 28.5T640-240Z"
/>
</svg>
</span>
<input
type="date"
class="form-control-inscription"
name="date_naissance"
required
/>
</div>
</div>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path
d="M12 12c2.21 0 4-1.79 4-4S14.21 4 12 4 8 5.79 8 8s1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
/>
</svg>
</span>
<input
type="text"
class="form-control-inscription"
name="identifiant"
placeholder="Identifiant"
required
/>
</div>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path
d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"
/>
</svg>
</span>
<input
type="email"
class="form-control-inscription"
name="email"
placeholder="Adresse mail"
required
/>
</div>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path
d="M12 2C8.691 2 6 4.691 6 8v4c-1.104 0-2 .896-2 2v8c0 1.104.896 2 2 2h12c1.104 0 2-.896 2-2v-8c0-1.104-.896-2-2-2V8c0-3.309-2.691-6-6-6zm0 2c2.205 0 4 1.795 4 4v4H8V8c0-2.205 1.795-4 4-4zm-4 10h8v8H8v-8z"
/>
</svg>
</span>
<input
type="password"
class="form-control-inscription"
name="mot_de_passe"
placeholder="Mot de Passe"
required
/>
</div>
<div class="form-group">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
color="#000000"
stroke-width="1.5"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M20.75 16.7143C20.75 16.7631 20.7453 16.8109 20.7364 16.8571C20.7453 16.9034 20.75 16.9511 20.75 17C20.75 17.4142 20.4142 17.75 20 17.75H6C5.30964 17.75 4.75 18.3096 4.75 19C4.75 19.6904 5.30964 20.25 6 20.25H20C20.4142 20.25 20.75 20.5858 20.75 21C20.75 21.4142 20.4142 21.75 20 21.75H6C4.48122 21.75 3.25 20.5188 3.25 19V5C3.25 3.48122 4.48122 2.25 6 2.25H19.4C20.1456 2.25 20.75 2.85442 20.75 3.6V16.7143ZM9 6.25C8.58579 6.25 8.25 6.58579 8.25 7C8.25 7.41421 8.58579 7.75 9 7.75H15C15.4142 7.75 15.75 7.41421 15.75 7C15.75 6.58579 15.4142 6.25 15 6.25H9Z"
fill="#000000"
></path>
</svg>
</span>
<input
type="text"
class="form-control-inscription"
name="situation"
placeholder="Situation professionnelle"
required
/>
</div>
<div class="boutons">
<label for="photo_profil" class="custom-file-upload">
<span class="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
class="upload-icon"
viewBox="0 0 24 24"
>
<path
d="M19 15v4H5v-4H3v4a2 2 0 002 2h14a2 2 0 002-2v-4h-2zm-7-1l5-5h-3V4h-4v5H7l5 5z"
/>
</svg>
Photo de profil (facultatif)
</span>
</label>
<input
id="photo_profil"
type="file"
name="photo_profil"
accept="image/*"
hidden
/>
<button class="bouton-inscription" type="submit">
S'inscrire
</button>
</div>
</form>
</div>
</div>
<div class="tour"></div>
</div>
<%- include('partials/footer') %>
</body>
</html>

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Page d'accueil - Smart Building</title>
<link rel="stylesheet" href="/styleMembres.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter&display=swap"
rel="stylesheet"
/>
<script
src="https://kit.fontawesome.com/a076d05399.js"
crossorigin="anonymous"
></script>
</head>
<body>
<%- include('partials/header') %>
<main>
<h1>Member Details</h1>
<p>Member ID: <%= id %></p>
<!-- Display the ID passed from the route -->
<!-- More member details would go here, once you fetch them -->
</main>
<%- include('partials/footer') %>
</body>
</html>

View File

@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Page d'accueil - Smart Building</title>
<link rel="stylesheet" href="/styleMembres.css" />
<link
href="https://fonts.googleapis.com/css2?family=Inter&display=swap"
rel="stylesheet"
/>
<script
src="https://kit.fontawesome.com/a076d05399.js"
crossorigin="anonymous"
></script>
</head>
<body>
<%- include('partials/header') %>
<main>
<section class="left-panel">
<img src="/images/batiment.png" alt="Bâtiment" />
</section>
<section class="right-panel">
<div class="header-membres">
<h1>Membres</h1>
<form class="form">
<label for="search">
<input
class="input"
type="text"
placeholder="Rechercher"
id="search"
/>
<div class="fancy-bg"></div>
<div class="search">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M21.53 20.47l-3.66-3.66C19.19 15.24 20 13.21 20 11c0-4.97-4.03-9-9-9s-9 4.03-9 9 4.03 9 9 9c2.21 0 4.24-.8 5.81-2.13l3.66 3.66c.29.29.76.29 1.06 0s.29-.76 0-1.06zM3.5 11c0-4.14 3.36-7.5 7.5-7.5s7.5 3.36 7.5 7.5-3.36 7.5-7.5 7.5S3.5 15.14 3.5 11z"
/>
</svg>
</div>
<button class="close-btn" type="reset" onClick="clearInput()">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</button>
</label>
</form>
</div>
<div id="membres-container"></div>
</section>
</main>
<%- include('partials/footer') %>
</body>
<script>
const membresContainer = document.getElementById("membres-container");
async function fetchMembres() {
try {
const response = await fetch("/api/utilisateurs");
const membres = await response.json();
const statutColor = {
visiteur: "#ffbe0b",
simple: "#4ecdc4",
complexe: "#f72585",
administrateur: "#e63946",
};
membresContainer.innerHTML = "";
membres.forEach((membre) => {
const div = document.createElement("div");
div.classList.add("membre");
div.innerHTML = `
<h3>${membre.nom}</h3>
<h4>${membre.prenom}</h4>
<p>${membre.age} ans</p>
<p style="color: ${
statutColor[membre.statut] || "#888"
}; font-weight: bold;">${membre.statut}</p>
`;
membresContainer.appendChild(div);
});
} catch (err) {
console.error("Erreur lors du chargement des membres :", err);
}
}
// Fonction pour réinitialiser l'input et les objets affichés
function clearInput() {
const inputField = document.getElementById("search"); // Sélectionne l'input par son id
inputField.value = ""; // Vide l'input
inputField.dispatchEvent(new Event("input")); // Déclenche l'événement 'input' pour actualiser l'affichage des objets
}
document.getElementById("search").addEventListener("input", function (e) {
const val = e.target.value.toLowerCase();
document.querySelectorAll(".membre").forEach((div) => {
const content = div.innerText.toLowerCase();
div.style.display = content.includes(val) ? "block" : "none";
});
});
window.onload = fetchMembres;
</script>
</html>

200
sk1/webapp/views/objets.ejs Normal file
View File

@ -0,0 +1,200 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Page d'accueil - Smart Building</title>
<link rel="stylesheet" href="/styleObjets.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet" />
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<style>
.setting-btn.disabled {
opacity: 0.4;
pointer-events: none;
cursor: not-allowed;
}
</style>
</head>
<body>
<%- include('partials/header') %>
<main>
<section class="left-panel">
<img src="/images/batiment.png" alt="Bâtiment" />
</section>
<section class="right-panel">
<div class="header-objets">
<h1>Objets Connectés</h1>
<form class="form">
<label for="search">
<input class="input" type="text" required placeholder="Rechercher" id="search" />
<div class="fancy-bg"></div>
<div class="search">
<svg viewBox="0 0 24 24" aria-hidden="true" class="r-yyyyoo">
<g>
<path d="M21.53 20.47l-3.66-3.66C19.195 15.24 20 13.214 20 11c0-4.97-4.03-9-9-9s-9 4.03-9 9 4.03 9 9 9c2.215 0 4.24-.804 5.808-2.13l3.66 3.66c.147.146.34.22.53.22s.385-.073.53-.22c.295-.293.295-.767.002-1.06zM3.5 11c0-4.135 3.365-7.5 7.5-7.5s7.5 3.365 7.5 7.5-3.365 7.5-7.5 7.5-7.5-3.365-7.5-7.5z"></path>
</g>
</svg>
</div>
<span class="close-btn" aria-label="Reset" onClick="clearInput()">&times;</span>
</label>
</form>
</div>
<div id="objets-container" data-statut="<%= session.utilisateur ? session.utilisateur.statut : '' %>"></div>
</section>
<div id="myModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<div id="modal-info"></div>
</div>
</div>
</main>
<%- include('partials/footer') %>
</body>
</html>
<script>
const span = document.getElementsByClassName("close")[0];
span.onclick = () => document.getElementById('myModal').style.display = "none";
function openModal(objet) {
const modal = document.getElementById('myModal');
const modalInfo = document.getElementById('modal-info');
modalInfo.innerHTML = `
<div class='info_objet'>
<h2 id='denomination-edit'>${objet.denomination}</h2>
<button type="submit" onClick="toggleEdit('${objet.adresse_ip}','edit-icon_denomination', 'denomination')">
<i id="edit-icon_denomination" class="fas fa-pen"></i>
</button>
</div>
<div class='info_objet'>
<p id='adresse_ip-edit'>${objet.adresse_ip}</p>
<button type="submit" onClick="toggleEdit('${objet.adresse_ip}','edit-icon_adresse_ip', 'adresse_ip')">
<i id="edit-icon_adresse_ip" class="fas fa-pen"></i>
</button>
</div>
<div class='info_objet'>
<p id='niveau-edit'>${objet.niveau}</p>
<button type="submit" onClick="toggleEdit('${objet.adresse_ip}','edit-icon_niveau', 'niveau')">
<i id="edit-icon_niveau" class="fas fa-pen"></i>
</button>
</div>
<div class='info_objet'>
<p id='type-edit'>${objet.type}</p>
<button type="submit" onClick="toggleEdit('${objet.adresse_ip}','edit-icon_type', 'type')">
<i id="edit-icon_type" class="fas fa-pen"></i>
</button>
</div>
<div class='info_objet'>
<p><strong>Dernière interaction:</strong> ${objet.derniere_interaction}</p>
</div>
<div class='info_objet'>
<p><strong>État:</strong> ${objet.etat}</p>
</div>
`;
modal.style.display = 'block';
}
function toggleEdit(adresse_ip, edit_icon, field) {
const editIcon = document.getElementById(edit_icon);
const paragraph = document.getElementById(field + "-edit");
if (editIcon.className === "fas fa-pen") {
editIcon.className = "fas fa-check";
paragraph.contentEditable = true;
paragraph.style.backgroundColor = "transparent";
} else {
editIcon.className = "fas fa-pen";
paragraph.contentEditable = false;
paragraph.style.backgroundColor = "rgba(60, 60, 60, 0.98)";
saveChanges(adresse_ip, field, paragraph.innerText);
}
}
async function saveChanges(adresse_ip, field, newValue) {
document.getElementById('h3_' + adresse_ip).innerText = newValue;
try {
const response = await fetch('/api/objets/' + encodeURIComponent(adresse_ip), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [field]: newValue })
});
if (response.ok) {
console.log('Objet mis à jour avec succès !');
} else {
console.error('Erreur serveur :', response.statusText);
}
} catch (error) {
console.error('Erreur réseau :', error);
}
}
function clearInput() {
const input = document.getElementById("search");
input.value = "";
input.dispatchEvent(new Event("input"));
fetchObjets();
}
async function fetchObjets() {
try {
const response = await fetch('/api/objets');
const objets = await response.json();
const container = document.getElementById('objets-container');
const userStatut = container.getAttribute("data-statut");
container.innerHTML = "";
const niveauColor = {
débutant: "#2ecc71",
intermédiaire: "#3498db",
avancé: "#f39c12",
expert: "#e74c3c",
};
const peutModifier = ['administrateur', 'complexe', 'simple'].includes(userStatut);
objets.forEach(objet => {
console.log(objet);
const div = document.createElement('div');
div.className = 'objet';
div.innerHTML = `
<button class="setting-btn ${peutModifier ? '' : 'disabled'}"
${peutModifier ? `onclick='openModal(${JSON.stringify(objet)})'` : ''}>
<i class="fas fa-cog"></i>
</button>
<h3 id='h3_${objet.adresse_ip}'>${objet.denomination}</h3>
<p>${objet.adresse_ip}</p>
<p><strong>Type</strong> : ${objet.type}</p>
${objet.temperature_actuelle !== null ? `<p><strong>Temperature Actuelle</strong> : ${objet.temperature_actuelle}°</p>` : ''}
${objet.temperature_cible !== null ? `<p><strong>Temperature Cible</strong> : ${objet.temperature_cible}°</p>` : ''}
${objet.mode !== null ? `<p><strong>Mode</strong> : ${objet.mode}</p>` : ''}
<p><strong>Niveau:</strong> <span style="color: ${niveauColor[objet.niveau]}">${objet.niveau}</span></p>
<div class="etat-point ${objet.etat === 'Actif' ? 'actif' : 'inactif'}"></div>
`;
container.appendChild(div);
});
} catch (error) {
console.error('Erreur fetchObjets :', error);
}
}
document.getElementById('search').addEventListener('input', function (e) {
const searchTerm = e.target.value.toLowerCase();
document.querySelectorAll('.objet').forEach(objet => {
const text = objet.innerText.toLowerCase();
objet.style.display = text.includes(searchTerm) ? 'block' : 'none';
});
});
window.onload = fetchObjets;
</script>

View File

@ -0,0 +1,9 @@
<footer>
<div class="footer_link">
<a href="/description">À PROPOS</a>
<a href="/contact">CONTACT</a>
</div>
<div class="copyright">© 404 NOT FOUND | CY-TECH</div>
</footer>

View File

@ -0,0 +1,39 @@
<header>
<a href="/">
<img id="smart_building" alt="smart_building" src="/images/smart_building.png" />
</a>
<ul class="header_text">
<% if (!session.utilisateur) { %>
<li><a href="/">Accueil</a></li>
<li><a href="/inscription">Inscription</a></li>
<% } else if (session.utilisateur.statut === 'administrateur') { %>
<% if (currentRoute === '/admin') { %>
<li><a href="/">Accueil</a></li>
<li><a href="/profil">Profil</a></li>
<% } else { %>
<li><a href="/admin">Dashboard</a></li>
<li><a href="/">Accueil</a></li>
<li><a href="/profil">Profil</a></li>
<% } %>
<% } else if (session.utilisateur.statut === 'complexe') { %>
<% if (currentRoute === '/dashboard-complexe') { %>
<li><a href="/">Accueil</a></li>
<li><a href="/profil">Profil</a></li>
<% } else { %>
<li><a href="/dashboard-complexe">Dashboard</a></li>
<li><a href="/">Accueil</a></li>
<li><a href="/profil">Profil</a></li>
<% } %>
<% } else { %>
<li><a href="/">Accueil</a></li>
<li><a href="/profil">Profil</a></li>
<% } %>
</ul>
<% if (!session.utilisateur) { %>
<a class="bouton_connexion" href="/connexion">Connexion</a>
<% } else { %>
<a class="bouton_connexion" href="/deconnexion">Déconnexion</a>
<% } %>
</header>

View File

@ -0,0 +1,70 @@
<!-- views/profil.ejs -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Mon Profil</title>
<link rel="stylesheet" href="/styleProfil.css">
</head>
<body>
<%- include("partials/header") %>
<div class="container">
<h1>Mon Profil</h1>
<form id="profilForm" action="/profil/update" method="POST" enctype="multipart/form-data">
<div>
<label>Login :</label>
<input type="text" name="identifiant" value="<%= session.utilisateur.identifiant %>" disabled>
</div>
<div>
<label>Nom :</label>
<input type="text" name="nom" value="<%= session.utilisateur.nom %>" disabled>
</div>
<div>
<label>Prénom :</label>
<input type="text" name="prenom" value="<%= session.utilisateur.prenom %>" disabled>
</div>
<div>
<label>Sexe :</label>
<select name="sexe" disabled>
<option value="Homme" <%= session.utilisateur.sexe === 'Homme' ? 'selected' : '' %>>Homme</option>
<option value="Femme" <%= session.utilisateur.sexe === 'Femme' ? 'selected' : '' %>>Femme</option>
<option value="Autre" <%= session.utilisateur.sexe === 'Autre' ? 'selected' : '' %>>Autre</option>
</select>
</div>
<div>
<label>Situation professionnelle :</label>
<input type="text" name="situation" value="<%= session.utilisateur.situation %>" disabled>
</div>
<div>
<label>Photo de profil :</label><br>
<img src="../photos/<%= session.utilisateur.photo %>" width="100" alt=""><br>
<input type="file" name="photo" disabled>
</div>
<div>
<label>Mot de passe :</label>
<input type="password" name="mot_de_passe" placeholder="nouveau mot de passe si désiré" disabled>
</div>
<button type="button" id="editBtn">Modifier mon profil</button>
<button type="submit" id="saveBtn" style="display: none;">Enregistrer</button>
</form>
</div>
<script>
const editBtn = document.getElementById('editBtn');
const saveBtn = document.getElementById('saveBtn');
const inputs = document.querySelectorAll('#profilForm input, #profilForm select');
editBtn.addEventListener('click', () => {
inputs.forEach(input => {
input.disabled = false;
});
editBtn.style.display = 'none';
saveBtn.style.display = 'inline-block';
});
</script>
<%- include("partials/footer") %>
</body>
</html>

View File

@ -0,0 +1,162 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Page d'accueil - Smart Building</title>
<link rel="stylesheet" href="/styleObjets.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet" />
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
</head>
<body>
<%- include('partials/header') %>
<main>
<section class="left-panel">
<img src="/images/batiment.png" alt="Bâtiment" />
</section>
<section class="right-panel">
<div class="header-objets">
<h1>Gestion des ressources</h1>
</div>
<div id="objets-container"></div>
</section>
<div id="myModal" class="modal">
<!-- Modal content -->
<div class="modal-content">
<span class="close">&times;</span>
<div id="modal-info"></div>
<!-- Conteneur pour les informations -->
</div>
</div>
</main>
<%- include('partials/footer') %>
</body>
</html>
<script>
let objetsContainer = document.getElementById('objets-container');
function openModal(objet) {
const modal = document.getElementById('myModal');
const modalInfo = document.getElementById('modal-info');
modalInfo.innerHTML = `
<div class='info_objet'>
<h2 id='denomination-edit'>${objet.denomination}</h2>
<button type="submit" id="edit-button" onClick="toggleEdit('edit-icon_denomination', 'denomination-edit')">
<i id="edit-icon_denomination" class="fas fa-pen"></i>
</button>
</div>
<div class='info_objet'>
<p id='address_ip-edit'>${objet.adresse_ip}</p>
<button type="submit" id="edit-button" onClick="toggleEdit('edit-icon_adresse_ip', 'address_ip-edit')">
<i id="edit-icon_adresse_ip" class="fas fa-pen"></i>
</button>
</div>
<div class='info_objet'>
<p id='etat-edit'>${objet.etat}</p>
<button type="submit" id="edit-button" onClick="toggleEdit('edit-icon_etat', 'etat-edit')">
<i id="edit-icon_etat" class="fas fa-pen"></i>
</button>
</div>
<div class='info_objet'>
<p id='type-edit'>${objet.type}</p>
<button type="submit" id="edit-button" onClick="toggleEdit('edit-icon_type', 'type-edit')">
<i id="edit-icon_type" class="fas fa-pen"></i>
</button>
</div>
<div class='info_objet'>
<p><strong>Consommation actuelle :</strong> ${objet.consommation}</p>
</div>
<div class='info_objet'>
<p><strong>Consommation maximale :</strong> ${objet.consommation_max}</p>
</div>
<div class='info_objet'>
<p><strong>Fournisseur :</strong> ${objet.fournisseur}</p>
</div>
<div class='info_objet'>
<p><strong>Abonnement :</strong> ${objet.abonnement}</p>
</div>
<div class='info_objet'>
<p><strong>Échéance :</strong> ${objet.echeance}</p>
</div>
`;
modal.style.display = 'block';
}
function toggleEdit(edit_icon, field) {
const editIcon = document.getElementById(edit_icon);
const paragraph = document.getElementById(field);
if (editIcon.className == "fas fa-pen") {
editIcon.className = "fas fa-check";
editIcon.alt = "Check";
paragraph.contentEditable = true;
paragraph.style.backgroundColor = "#cfcfcf";
} else {
if (editIcon.className == "fas fa-check") {
editIcon.className = "fas fa-pen";
editIcon.alt = "Stylo";
paragraph.contentEditable = false;
paragraph.style.backgroundColor = "#666666";
}
}
}
var span = document.getElementsByClassName("close")[0];
span.onclick = function () {
const modal = document.getElementById('myModal');
modal.style.display = "none";
}
// Fonction pour réinitialiser l'input et les objets affichés
function clearInput() {
const inputField = document.getElementById("search"); // Sélectionne l'input par son id
inputField.value = ""; // Vide l'input
inputField.dispatchEvent(new Event("input")); // Déclenche l'événement 'input' pour actualiser l'affichage des objets
fetchObjets(); // Recharge les objets
}
async function fetchObjets() {
const objetsContainer = document.getElementById('objets-container');
objetsContainer.innerHTML = "";
try {
const response = await fetch('/api/ressources');
const ressources = await response.json();
ressources.forEach(r => {
const div = document.createElement('div');
div.classList.add('objet');
div.innerHTML = `
<h3>${r.nom}</h3>
<p><strong>Consommation actuelle :</strong> ${r.consommation}</p>
<p><strong>Max atteignable :</strong> ${r.consommation_max}</p>
<p><strong>Fournisseur :</strong> ${r.fournisseur}</p>
<p><strong>Abonnement :</strong> ${r.abonnement}</p>
<p><strong>Date paiement :</strong> ${r.echeance}</p>
`;
objetsContainer.appendChild(div);
});
} catch (err) {
console.error("Erreur lors du chargement des ressources :", err);
}
}
// Charger les objets au premier chargement de la page
window.onload = function () {
fetchObjets(); // Charge les objets au premier chargement
};
</script>