website fonctional / front=ok / back=ok / docker=ok
This commit is contained in:
1
web/annonces.json
Normal file
1
web/annonces.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
86
web/app.py
86
web/app.py
@@ -1,10 +1,12 @@
|
||||
import os
|
||||
import uuid
|
||||
from flask import Flask, redirect, url_for, session, render_template
|
||||
import json
|
||||
from flask import Flask, redirect, url_for, jsonify, session, render_template
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from authlib.integrations.flask_client import OAuth
|
||||
|
||||
app = Flask(__name__)
|
||||
ANNOUNCE_FILE = os.path.join(os.path.dirname(__file__), "annonces.json")
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://flaskuser:flaskpass@mariadb/flaskdb'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.secret_key = os.environ.get("SECRET_KEY", "dev-key")
|
||||
@@ -45,13 +47,89 @@ def auth():
|
||||
nonce = session.pop('nonce', None)
|
||||
userinfo = keycloak.parse_id_token(token, nonce=nonce)
|
||||
session['user'] = userinfo
|
||||
session["id_token"] = token.get("id_token")
|
||||
app.logger.debug(f"User info: {userinfo}")
|
||||
return redirect('/')
|
||||
|
||||
@app.route('/logout')
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
session.pop('user', None)
|
||||
return redirect('/')
|
||||
id_token = session.get("id_token")
|
||||
print("ID Token Hint:", id_token)
|
||||
session.clear()
|
||||
return redirect(
|
||||
f"https://keycloak.ninolbt.com/realms/gesthub/protocol/openid-connect/logout"
|
||||
f"?post_logout_redirect_uri=https://dashboard.ninolbt.com"
|
||||
f"&id_token_hint={id_token}"
|
||||
)
|
||||
def load_announces():
|
||||
try:
|
||||
with open(ANNOUNCE_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return []
|
||||
|
||||
def save_announces(announces):
|
||||
with open(ANNOUNCE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(announces, f, ensure_ascii=False, indent=2)
|
||||
|
||||
def get_next_id(announces):
|
||||
return max((a.get("id", 0) for a in announces), default=0) + 1
|
||||
|
||||
@app.route("/api/annonces", methods=["GET"])
|
||||
def get_announces():
|
||||
return jsonify(load_announces())
|
||||
|
||||
@app.route("/api/annonces", methods=["POST"])
|
||||
def create_annonce():
|
||||
user = session.get("user")
|
||||
if not user or "/admin" not in user.get("groups", []):
|
||||
return jsonify({"error": "unauthorized"}), 403
|
||||
data = request.json
|
||||
if not data.get("text"):
|
||||
return jsonify({"error": "missing text"}), 400
|
||||
|
||||
announces = load_announces()
|
||||
new_announce = {
|
||||
"id": get_next_id(announces),
|
||||
"text": data["text"],
|
||||
"author": user.get("preferred_username", "admin")
|
||||
}
|
||||
announces.append(new_announce)
|
||||
save_announces(announces)
|
||||
return jsonify({"status": "ok", "announce": new_announce})
|
||||
|
||||
@app.route("/api/annonces/<int:annonce_id>", methods=["DELETE"])
|
||||
def delete_annonce(annonce_id):
|
||||
user = session.get("user")
|
||||
if not user or "/admin" not in user.get("groups", []):
|
||||
return jsonify({"error": "unauthorized"}), 403
|
||||
announces = load_announces()
|
||||
announces = [a for a in announces if a["id"] != annonce_id]
|
||||
save_announces(announces)
|
||||
return jsonify({"status": "deleted"})
|
||||
|
||||
@app.route("/api/annonces/<int:annonce_id>", methods=["PUT"])
|
||||
def edit_annonce(annonce_id):
|
||||
user = session.get("user")
|
||||
if not user or "/admin" not in user.get("groups", []):
|
||||
return jsonify({"error": "unauthorized"}), 403
|
||||
data = request.json
|
||||
announces = load_announces()
|
||||
found = False
|
||||
for a in announces:
|
||||
if a["id"] == annonce_id:
|
||||
a["text"] = data.get("text", a["text"])
|
||||
found = True
|
||||
if not found:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
save_announces(announces)
|
||||
return jsonify({"status": "updated"})
|
||||
|
||||
@app.route("/api/is_admin")
|
||||
def is_admin():
|
||||
user = session.get("user")
|
||||
return jsonify({"admin": "/admin" in user.get("groups", [])})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', debug=True)
|
||||
|
||||
@@ -389,3 +389,14 @@
|
||||
.dark .right-column {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
.postit {
|
||||
background-color: #fff475;
|
||||
color: #333;
|
||||
padding: 0.8rem;
|
||||
margin-bottom: 0.8rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 2px 2px 6px rgba(0,0,0,0.15);
|
||||
font-family: 'Comic Sans MS', sans-serif;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
@@ -81,3 +81,73 @@
|
||||
contactModal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Gestion des annonces
|
||||
|
||||
let isAdmin = false;
|
||||
|
||||
fetch("/api/is_admin")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
isAdmin = data.admin;
|
||||
if (isAdmin) {
|
||||
document.getElementById("admin-tools").style.display = "block";
|
||||
}
|
||||
});
|
||||
|
||||
fetch("/api/annonces")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const container = document.getElementById("annonces-container");
|
||||
container.innerHTML = "";
|
||||
|
||||
data.forEach(item => {
|
||||
const div = document.createElement("div");
|
||||
div.className = "postit";
|
||||
div.dataset.id = item.id;
|
||||
div.innerHTML = `
|
||||
<span class="annonce-text">${item.text}</span>
|
||||
<br><small>${item.author}</small>
|
||||
`;
|
||||
|
||||
if (isAdmin) {
|
||||
const editBtn = document.createElement("button");
|
||||
editBtn.textContent = "✏️";
|
||||
editBtn.onclick = () => editAnnonce(item.id, item.text);
|
||||
div.appendChild(editBtn);
|
||||
|
||||
const delBtn = document.createElement("button");
|
||||
delBtn.textContent = "🗑️";
|
||||
delBtn.onclick = () => deleteAnnonce(item.id);
|
||||
div.appendChild(delBtn);
|
||||
}
|
||||
|
||||
container.appendChild(div);
|
||||
});
|
||||
});
|
||||
|
||||
function submitAnnonce() {
|
||||
const txt = document.getElementById("annonce-text").value;
|
||||
fetch("/api/annonces", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text: txt })
|
||||
}).then(() => location.reload());
|
||||
}
|
||||
|
||||
function deleteAnnonce(id) {
|
||||
fetch(`/api/annonces/${id}`, {
|
||||
method: "DELETE"
|
||||
}).then(() => location.reload());
|
||||
}
|
||||
|
||||
function editAnnonce(id, oldText) {
|
||||
const newText = prompt("Modifier l'annonce :", oldText);
|
||||
if (newText && newText !== oldText) {
|
||||
fetch(`/api/annonces/${id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text: newText })
|
||||
}).then(() => location.reload());
|
||||
}
|
||||
}
|
||||
@@ -40,13 +40,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://mattermost.ninolbt.com/gesthub/channels/town-square" target="_blank">chat</a>
|
||||
<a href="https://mattermost.ninolbt.com/gesthub/channels/town-square" target="_blank">Chat</a>
|
||||
</div>
|
||||
|
||||
<div class="profile-menu">
|
||||
<span id="profile-name">{{ user['preferred_username'] }}<i class="fa-solid fa-caret-down"></i></span>
|
||||
<div class="profile-dropdown">
|
||||
<a class="simple-btn" href="" type="button">Profil</a>
|
||||
<!-- <a class="simple-btn" href="" type="button">Profil</a> -->
|
||||
<a class="simple-btn" href="/logout" type="button">Déconnexion</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,14 +68,20 @@
|
||||
<main>
|
||||
<!-- clonne gauche pr widget planning -->
|
||||
<aside class="left-column">
|
||||
<div class="planning">Planning / Agenda</div>
|
||||
<div class="planning">Planning / Agenda
|
||||
<iframe
|
||||
src="https://mattermost.ninolbt.com/boards/team/8xj6d4ukwigk7rznqi3w339x7e/b3kmbqfwd33dmdy9g9g3ezaoxza/vu4nuxhf73ircznrkrcgzonno8a"
|
||||
style="width: 100%; height: 100%; border: none;"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Centre pr widget trello -->
|
||||
<section class="center-column">
|
||||
<h1 class="main-title">Gesthub</h1>
|
||||
<div class="trello">
|
||||
<iframe src="https://mattermost.ninolbt.com/boards/team/8xj6d4ukwigk7rznqi3w339x7e/b3kmbqfwd33dmdy9g9g3ezaoxza/va5xp53m6spbi8qo6qnng7711me" width="100%" height="700" frameborder="0"></iframe>
|
||||
<iframe src="https://mattermost.ninolbt.com/boards/team/8xj6d4ukwigk7rznqi3w339x7e/b3kmbqfwd33dmdy9g9g3ezaoxza/va5xp53m6spbi8qo6qnng7711me" width="100%" height="680" frameborder="0"></iframe>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -91,7 +97,20 @@
|
||||
<button class="simple-btn">Bible GameDev</button>
|
||||
<button class="simple-btn">Réglement du HUB</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Bloc annonces admin
|
||||
<div id="admin-announcements" style="margin-top: 2rem;">
|
||||
<h3 style="margin-bottom: 1rem;">📌 Annonces</h3>
|
||||
<div id="annonces-container"></div>
|
||||
|
||||
Formulaire admin caché par défaut-
|
||||
<div id="admin-tools" style="display: none; margin-top: 1rem;">
|
||||
<textarea id="annonce-text" placeholder="Nouvelle annonce..." style="width: 100%; padding: 0.5rem; margin-bottom: 0.5rem;"></textarea>
|
||||
<button onclick="submitAnnonce()" style="width: 100%;">📤 Publier</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside> -->
|
||||
|
||||
</main>
|
||||
<script src="{{ url_for('static', filename='assets/js/index.js') }}"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user