This commit is contained in:
lnino
2025-05-19 19:36:52 +02:00
5 changed files with 203 additions and 19 deletions

1
web/annonces.json Normal file
View File

@@ -0,0 +1 @@
[]

View File

@@ -1,10 +1,12 @@
import os import os
import uuid 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 flask_sqlalchemy import SQLAlchemy
from authlib.integrations.flask_client import OAuth from authlib.integrations.flask_client import OAuth
app = Flask(__name__) 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_DATABASE_URI'] = 'mysql+pymysql://flaskuser:flaskpass@mariadb/flaskdb'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.secret_key = os.environ.get("SECRET_KEY", "dev-key") app.secret_key = os.environ.get("SECRET_KEY", "dev-key")
@@ -45,13 +47,89 @@ def auth():
nonce = session.pop('nonce', None) nonce = session.pop('nonce', None)
userinfo = keycloak.parse_id_token(token, nonce=nonce) userinfo = keycloak.parse_id_token(token, nonce=nonce)
session['user'] = userinfo session['user'] = userinfo
session["id_token"] = token.get("id_token")
app.logger.debug(f"User info: {userinfo}") app.logger.debug(f"User info: {userinfo}")
return redirect('/') return redirect('/')
@app.route('/logout') @app.route("/logout")
def logout(): def logout():
session.pop('user', None) id_token = session.get("id_token")
return redirect('/') 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__': if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True) app.run(host='0.0.0.0', debug=True)

View File

@@ -389,3 +389,14 @@
.dark .right-column { .dark .right-column {
background-color: #2d2d2d; 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;
}

View File

@@ -81,3 +81,73 @@
contactModal.style.display = 'none'; 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());
}
}

View File

@@ -22,6 +22,10 @@
<div class="nav-link"> <div class="nav-link">
<a href="https://discord.com" target="_blank">Accéder au serveur Discord</a> <a href="https://discord.com" target="_blank">Accéder au serveur Discord</a>
<a href="https://mattermost.ninolbt.com/boards/team/8xj6d4ukwigk7rznqi3w339x7e/b3kmbqfwd33dmdy9g9g3ezaoxza/va5xp53m6spbi8qo6qnng7711me" target="_blank">Board</a>
<a href="https://mattermost.ninolbt.com/boards/team/8xj6d4ukwigk7rznqi3w339x7e/b3kmbqfwd33dmdy9g9g3ezaoxza/va5xp53m6spbi8qo6qnng7711me" target="_blank">Board</a>
</div> </div>
<div class="nav-link dropdown"> <div class="nav-link dropdown">
@@ -38,12 +42,13 @@
</div> </div>
</div> </div>
</div> </div>
<a href="https://mattermost.ninolbt.com/gesthub/channels/town-square" target="_blank">Chat</a>
</div> </div>
<div class="profile-menu"> <div class="profile-menu">
<span id="profile-name">{{ user['preferred_username'] }} <i class="fa-solid fa-caret-down"></i></span> <span id="profile-name">{{ user['preferred_username'] }} <i class="fa-solid fa-caret-down"></i></span>
<div class="profile-dropdown"> <div class="profile-dropdown">
<p>Mon profil</p> <!-- <a class="simple-btn" href="" type="button">Profil</a> -->
<a class="simple-btn" href="/logout" type="button">Déconnexion</a> <a class="simple-btn" href="/logout" type="button">Déconnexion</a>
</div> </div>
</div> </div>
@@ -65,14 +70,20 @@
<main> <main>
<!-- clonne gauche pr widget planning --> <!-- clonne gauche pr widget planning -->
<aside class="left-column"> <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> </aside>
<!-- Centre pr widget trello --> <!-- Centre pr widget trello -->
<section class="center-column"> <section class="center-column">
<h1 class="main-title">Gesthub</h1> <h1 class="main-title">Gesthub</h1>
<div class="trello">Zone Trello <div class="trello">
<iframe src="https://mattermost.ninolbt.com/boards/team/8xj6d4ukwigk7rznqi3w339x7e/b3kmbqfwd33dmdy9g9g3ezaoxza/va5xp53m6spbi8qo6qnng7711me" width="100%" height="600" frameborder="0"></iframe> <iframe src="https://mattermost.ninolbt.com/boards/team/8xj6d4ukwigk7rznqi3w339x7e/b3kmbqfwd33dmdy9g9g3ezaoxza/va5xp53m6spbi8qo6qnng7711me" width="100%" height="680" frameborder="0"></iframe>
</div> </div>
</section> </section>
@@ -88,7 +99,20 @@
<button class="simple-btn">Bible GameDev</button> <button class="simple-btn">Bible GameDev</button>
<button class="simple-btn">Réglement du HUB</button> <button class="simple-btn">Réglement du HUB</button>
</div> </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> </main>
<script src="{{ url_for('static', filename='assets/js/index.js') }}"></script> <script src="{{ url_for('static', filename='assets/js/index.js') }}"></script>
</body> </body>