wip, claude + docker-compose modifications
This commit is contained in:
11
app.py
11
app.py
@@ -12,9 +12,15 @@ SUPPORTED_LANGS = ["fr", "en", "de"]
|
|||||||
|
|
||||||
|
|
||||||
def _select_locale():
|
def _select_locale():
|
||||||
|
# 1. Explicit session override (set via flag switcher)
|
||||||
lang = flask_session.get("lang")
|
lang = flask_session.get("lang")
|
||||||
if lang in SUPPORTED_LANGS:
|
if lang in SUPPORTED_LANGS:
|
||||||
return lang
|
return lang
|
||||||
|
# 2. Authenticated user's stored preference
|
||||||
|
if current_user.is_authenticated and current_user.language in SUPPORTED_LANGS:
|
||||||
|
flask_session["lang"] = current_user.language
|
||||||
|
return current_user.language
|
||||||
|
# 3. Browser Accept-Language header
|
||||||
return request.accept_languages.best_match(SUPPORTED_LANGS) or "en"
|
return request.accept_languages.best_match(SUPPORTED_LANGS) or "en"
|
||||||
|
|
||||||
|
|
||||||
@@ -74,11 +80,13 @@ def create_app(config_class=Config):
|
|||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
return db.session.get(User, int(user_id))
|
return db.session.get(User, int(user_id))
|
||||||
|
|
||||||
|
from blueprints.admin import admin_bp
|
||||||
from blueprints.auth import auth_bp
|
from blueprints.auth import auth_bp
|
||||||
from blueprints.dashboard import dashboard_bp
|
from blueprints.dashboard import dashboard_bp
|
||||||
from blueprints.equipment import equipment_bp
|
from blueprints.equipment import equipment_bp
|
||||||
from blueprints.sessions import sessions_bp
|
from blueprints.sessions import sessions_bp
|
||||||
from blueprints.analyses import analyses_bp
|
from blueprints.analyses import analyses_bp
|
||||||
|
app.register_blueprint(admin_bp)
|
||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
app.register_blueprint(dashboard_bp)
|
app.register_blueprint(dashboard_bp)
|
||||||
app.register_blueprint(equipment_bp)
|
app.register_blueprint(equipment_bp)
|
||||||
@@ -116,6 +124,9 @@ def create_app(config_class=Config):
|
|||||||
def set_lang(lang: str):
|
def set_lang(lang: str):
|
||||||
if lang in SUPPORTED_LANGS:
|
if lang in SUPPORTED_LANGS:
|
||||||
flask_session["lang"] = lang
|
flask_session["lang"] = lang
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
current_user.language = lang
|
||||||
|
db.session.commit()
|
||||||
return redirect(request.referrer or "/")
|
return redirect(request.referrer or "/")
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|||||||
100
blueprints/admin.py
Normal file
100
blueprints/admin.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
from flask import Blueprint, abort, flash, redirect, render_template, request, url_for
|
||||||
|
from flask_babel import _
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
|
||||||
|
from extensions import db
|
||||||
|
from models import User
|
||||||
|
|
||||||
|
admin_bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||||
|
|
||||||
|
ROLES = ["user", "admin"]
|
||||||
|
|
||||||
|
|
||||||
|
def _require_admin():
|
||||||
|
if not current_user.is_authenticated or current_user.role != "admin":
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# User list
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@admin_bp.route("/")
|
||||||
|
@login_required
|
||||||
|
def index():
|
||||||
|
_require_admin()
|
||||||
|
users = db.session.scalars(
|
||||||
|
db.select(User).order_by(User.created_at.desc())
|
||||||
|
).all()
|
||||||
|
return render_template("admin/users.html", users=users, roles=ROLES)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Change role
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@admin_bp.route("/users/<int:user_id>/role", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def change_role(user_id: int):
|
||||||
|
_require_admin()
|
||||||
|
user = db.session.get(User, user_id)
|
||||||
|
if user is None:
|
||||||
|
abort(404)
|
||||||
|
new_role = request.form.get("role", "user")
|
||||||
|
if new_role not in ROLES:
|
||||||
|
flash(_("Invalid role."), "error")
|
||||||
|
return redirect(url_for("admin.index"))
|
||||||
|
# Prevent removing the last admin
|
||||||
|
if user.role == "admin" and new_role != "admin":
|
||||||
|
admin_count = db.session.scalar(
|
||||||
|
db.select(db.func.count()).select_from(User).where(User.role == "admin")
|
||||||
|
)
|
||||||
|
if admin_count <= 1:
|
||||||
|
flash(_("Cannot remove the last admin."), "error")
|
||||||
|
return redirect(url_for("admin.index"))
|
||||||
|
user.role = new_role
|
||||||
|
db.session.commit()
|
||||||
|
flash(_("Role updated for %(email)s.", email=user.email), "success")
|
||||||
|
return redirect(url_for("admin.index"))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Reset password
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@admin_bp.route("/users/<int:user_id>/password", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def reset_password(user_id: int):
|
||||||
|
_require_admin()
|
||||||
|
user = db.session.get(User, user_id)
|
||||||
|
if user is None:
|
||||||
|
abort(404)
|
||||||
|
new_pw = request.form.get("new_password", "").strip()
|
||||||
|
if len(new_pw) < 8:
|
||||||
|
flash(_("Password must be at least 8 characters."), "error")
|
||||||
|
return redirect(url_for("admin.index"))
|
||||||
|
user.set_password(new_pw)
|
||||||
|
db.session.commit()
|
||||||
|
flash(_("Password reset for %(email)s.", email=user.email), "success")
|
||||||
|
return redirect(url_for("admin.index"))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Delete user
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@admin_bp.route("/users/<int:user_id>/delete", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def delete_user(user_id: int):
|
||||||
|
_require_admin()
|
||||||
|
if user_id == current_user.id:
|
||||||
|
flash(_("You cannot delete your own account."), "error")
|
||||||
|
return redirect(url_for("admin.index"))
|
||||||
|
user = db.session.get(User, user_id)
|
||||||
|
if user is None:
|
||||||
|
abort(404)
|
||||||
|
email = user.email
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
flash(_("User %(email)s deleted.", email=email), "success")
|
||||||
|
return redirect(url_for("admin.index"))
|
||||||
@@ -12,6 +12,7 @@ from flask import (
|
|||||||
render_template,
|
render_template,
|
||||||
request,
|
request,
|
||||||
send_from_directory,
|
send_from_directory,
|
||||||
|
session as flask_session,
|
||||||
url_for,
|
url_for,
|
||||||
)
|
)
|
||||||
from flask_babel import _
|
from flask_babel import _
|
||||||
@@ -28,6 +29,13 @@ auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
|
|||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _login(user: "User") -> None:
|
||||||
|
"""Log in user and restore their language preference into the session."""
|
||||||
|
login_user(user)
|
||||||
|
if user.language:
|
||||||
|
flask_session["lang"] = user.language
|
||||||
|
|
||||||
|
|
||||||
def _safe_next() -> str:
|
def _safe_next() -> str:
|
||||||
target = request.args.get("next") or ""
|
target = request.args.get("next") or ""
|
||||||
if target and urlparse(target).netloc == "":
|
if target and urlparse(target).netloc == "":
|
||||||
@@ -107,7 +115,7 @@ def login():
|
|||||||
|
|
||||||
user.last_login_at = datetime.now(timezone.utc)
|
user.last_login_at = datetime.now(timezone.utc)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
login_user(user)
|
_login(user)
|
||||||
return redirect(_safe_next())
|
return redirect(_safe_next())
|
||||||
|
|
||||||
return render_template("auth/login.html")
|
return render_template("auth/login.html")
|
||||||
@@ -167,7 +175,7 @@ def register():
|
|||||||
_dispatch_confirmation(user)
|
_dispatch_confirmation(user)
|
||||||
return render_template("auth/confirm_pending.html", email=email)
|
return render_template("auth/confirm_pending.html", email=email)
|
||||||
|
|
||||||
login_user(user)
|
_login(user)
|
||||||
flash(_("Account created! Welcome."), "success")
|
flash(_("Account created! Welcome."), "success")
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
@@ -186,7 +194,7 @@ def confirm_email(token: str):
|
|||||||
user.email_confirmed = True
|
user.email_confirmed = True
|
||||||
user.email_confirm_token = None
|
user.email_confirm_token = None
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
login_user(user)
|
_login(user)
|
||||||
flash(_("Email confirmed! Welcome."), "success")
|
flash(_("Email confirmed! Welcome."), "success")
|
||||||
return redirect(url_for("dashboard.index"))
|
return redirect(url_for("dashboard.index"))
|
||||||
|
|
||||||
@@ -243,7 +251,7 @@ def callback_google():
|
|||||||
flash(_("This email is already registered with a different login method."), "error")
|
flash(_("This email is already registered with a different login method."), "error")
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
login_user(user)
|
_login(user)
|
||||||
return redirect(_safe_next())
|
return redirect(_safe_next())
|
||||||
|
|
||||||
|
|
||||||
@@ -293,7 +301,7 @@ def callback_github():
|
|||||||
flash(_("This email is already registered with a different login method."), "error")
|
flash(_("This email is already registered with a different login method."), "error")
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
login_user(user)
|
_login(user)
|
||||||
return redirect(_safe_next())
|
return redirect(_safe_next())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ services:
|
|||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- ${NETWORK}
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
@@ -30,7 +30,14 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- app_storage:/app/storage
|
- app_storage:/app/storage
|
||||||
- .:/app # bind-mount source so code changes are live without a rebuild
|
- .:/app # bind-mount source so code changes are live without a rebuild
|
||||||
|
networks:
|
||||||
|
- ${NETWORK}
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
app_storage:
|
app_storage:
|
||||||
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
${NETWORK}:
|
||||||
|
external: true
|
||||||
|
|||||||
44
migrations/versions/6818f37f4124_user_role_and_language.py
Normal file
44
migrations/versions/6818f37f4124_user_role_and_language.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""user_role_and_language
|
||||||
|
|
||||||
|
Revision ID: 6818f37f4124
|
||||||
|
Revises: bf96ceb7f076
|
||||||
|
Create Date: 2026-03-19 15:51:15.091825
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '6818f37f4124'
|
||||||
|
down_revision = 'bf96ceb7f076'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('role', sa.String(length=20), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('language', sa.String(length=10), nullable=True))
|
||||||
|
|
||||||
|
op.execute("UPDATE users SET role = 'user' WHERE role IS NULL")
|
||||||
|
|
||||||
|
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('role', nullable=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('language')
|
||||||
|
batch_op.drop_column('role')
|
||||||
|
|
||||||
|
op.execute("UPDATE users SET role = 'user' WHERE role IS NULL")
|
||||||
|
|
||||||
|
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('role', nullable=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -38,6 +38,8 @@ class User(UserMixin, db.Model):
|
|||||||
email_confirm_token: Mapped[str | None] = mapped_column(String(128))
|
email_confirm_token: Mapped[str | None] = mapped_column(String(128))
|
||||||
show_equipment_public: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
show_equipment_public: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
bio: Mapped[str | None] = mapped_column(Text)
|
bio: Mapped[str | None] = mapped_column(Text)
|
||||||
|
role: Mapped[str] = mapped_column(String(20), nullable=False, default="user")
|
||||||
|
language: Mapped[str | None] = mapped_column(String(10))
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||||
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||||
|
|
||||||
|
|||||||
124
templates/admin/users.html
Normal file
124
templates/admin/users.html
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ _('Admin — Users') }} — The Shooter's Network{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem;">
|
||||||
|
<h1 style="margin:0;">{{ _('User Management') }}</h1>
|
||||||
|
<span style="font-size:0.85rem;color:#888;">{{ users|length }} {{ _('users') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="overflow-x:auto;">
|
||||||
|
<table style="min-width:900px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ _('User') }}</th>
|
||||||
|
<th>{{ _('Provider') }}</th>
|
||||||
|
<th>{{ _('Role') }}</th>
|
||||||
|
<th>{{ _('Language') }}</th>
|
||||||
|
<th>{{ _('Joined') }}</th>
|
||||||
|
<th>{{ _('Last login') }}</th>
|
||||||
|
<th>{{ _('Actions') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for u in users %}
|
||||||
|
<tr>
|
||||||
|
{# User info #}
|
||||||
|
<td>
|
||||||
|
<div style="display:flex;align-items:center;gap:.6rem;">
|
||||||
|
{% if u.effective_avatar_url %}
|
||||||
|
<img src="{{ u.effective_avatar_url }}" style="width:28px;height:28px;border-radius:50%;object-fit:cover;" alt="">
|
||||||
|
{% else %}
|
||||||
|
<div style="width:28px;height:28px;border-radius:50%;background:#e0e4f0;display:flex;align-items:center;justify-content:center;font-size:.75rem;color:#666;font-weight:700;">
|
||||||
|
{{ (u.display_name or u.email)[0].upper() }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600;font-size:.9rem;">{{ u.display_name or '—' }}</div>
|
||||||
|
<div style="font-size:.78rem;color:#888;">{{ u.email }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Provider #}
|
||||||
|
<td style="font-size:.82rem;color:#666;">
|
||||||
|
{% if u.provider == 'google' %}🔵 Google
|
||||||
|
{% elif u.provider == 'github' %}⚫ GitHub
|
||||||
|
{% else %}🔑 {{ _('Local') }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Role badge + change form #}
|
||||||
|
<td>
|
||||||
|
<form method="post" action="{{ url_for('admin.change_role', user_id=u.id) }}"
|
||||||
|
style="display:flex;gap:.4rem;align-items:center;">
|
||||||
|
<select name="role" style="padding:.2rem .5rem;font-size:.82rem;border:1px solid #ccc;border-radius:4px;background:#fff;">
|
||||||
|
{% for r in roles %}
|
||||||
|
<option value="{{ r }}" {% if u.role == r %}selected{% endif %}>{{ r }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button type="submit"
|
||||||
|
style="background:#f0f4ff;color:#1a1a2e;border:1px solid #c8d4f0;border-radius:4px;padding:.2rem .6rem;font-size:.78rem;cursor:pointer;">
|
||||||
|
{{ _('Set') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Language #}
|
||||||
|
<td style="font-size:.85rem;color:#666;">
|
||||||
|
{{ u.language or '—' }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Dates #}
|
||||||
|
<td style="font-size:.8rem;color:#888;white-space:nowrap;">{{ u.created_at.strftime('%d %b %Y') }}</td>
|
||||||
|
<td style="font-size:.8rem;color:#888;white-space:nowrap;">
|
||||||
|
{{ u.last_login_at.strftime('%d %b %Y') if u.last_login_at else '—' }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{# Actions #}
|
||||||
|
<td>
|
||||||
|
<div style="display:flex;gap:.5rem;flex-wrap:wrap;align-items:center;">
|
||||||
|
|
||||||
|
{# Reset password (local accounts only) #}
|
||||||
|
{% if u.provider == 'local' %}
|
||||||
|
<details style="display:inline;">
|
||||||
|
<summary style="display:inline-block;padding:.2rem .6rem;background:#f0f4ff;color:#1a1a2e;
|
||||||
|
border:1px solid #c8d4f0;border-radius:4px;font-size:.78rem;cursor:pointer;list-style:none;">
|
||||||
|
🔑 {{ _('Reset pwd') }}
|
||||||
|
</summary>
|
||||||
|
<form method="post" action="{{ url_for('admin.reset_password', user_id=u.id) }}"
|
||||||
|
style="display:flex;gap:.4rem;align-items:center;margin-top:.35rem;flex-wrap:wrap;">
|
||||||
|
<input type="password" name="new_password" required minlength="8"
|
||||||
|
placeholder="{{ _('New password (min 8)') }}"
|
||||||
|
style="padding:.3rem .6rem;border:1px solid #ccc;border-radius:4px;font-size:.82rem;width:180px;">
|
||||||
|
<button type="submit"
|
||||||
|
style="background:#1a1a2e;color:#fff;border:none;border-radius:4px;padding:.3rem .7rem;font-size:.78rem;cursor:pointer;">
|
||||||
|
{{ _('Save') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{# Delete — cannot delete yourself #}
|
||||||
|
{% if u.id != current_user.id %}
|
||||||
|
<form method="post" action="{{ url_for('admin.delete_user', user_id=u.id) }}"
|
||||||
|
onsubmit="return confirm('{{ _('Delete user %(email)s? All their data will be permanently removed.', email=u.email) | e }}');">
|
||||||
|
<button type="submit"
|
||||||
|
style="background:#fff0f0;color:#c0392b;border:1px solid #f5c6c6;border-radius:4px;
|
||||||
|
padding:.2rem .6rem;font-size:.78rem;cursor:pointer;">
|
||||||
|
{{ _('Delete') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<span style="font-size:.75rem;color:#aaa;">{{ _('(you)') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -250,6 +250,7 @@
|
|||||||
<a href="{{ url_for('equipment.index') }}">{{ _('Equipment') }}</a>
|
<a href="{{ url_for('equipment.index') }}">{{ _('Equipment') }}</a>
|
||||||
<a href="{{ url_for('sessions.index') }}">{{ _('Sessions') }}</a>
|
<a href="{{ url_for('sessions.index') }}">{{ _('Sessions') }}</a>
|
||||||
<a href="{{ url_for('dashboard.index') }}">{{ _('Dashboard') }}</a>
|
<a href="{{ url_for('dashboard.index') }}">{{ _('Dashboard') }}</a>
|
||||||
|
{% if current_user.role == 'admin' %}<a href="{{ url_for('admin.index') }}" style="color:#f9a825;">{{ _('Admin') }}</a>{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -304,6 +305,7 @@
|
|||||||
<a href="{{ url_for('equipment.index') }}">{{ _('Equipment') }}</a>
|
<a href="{{ url_for('equipment.index') }}">{{ _('Equipment') }}</a>
|
||||||
<a href="{{ url_for('sessions.index') }}">{{ _('Sessions') }}</a>
|
<a href="{{ url_for('sessions.index') }}">{{ _('Sessions') }}</a>
|
||||||
<a href="{{ url_for('dashboard.index') }}">{{ _('Dashboard') }}</a>
|
<a href="{{ url_for('dashboard.index') }}">{{ _('Dashboard') }}</a>
|
||||||
|
{% if current_user.role == 'admin' %}<a href="{{ url_for('admin.index') }}" style="color:#f9a825;">{{ _('Admin') }}</a>{% endif %}
|
||||||
<a href="{{ url_for('auth.profile') }}">{{ _('Profile') }}</a>
|
<a href="{{ url_for('auth.profile') }}">{{ _('Profile') }}</a>
|
||||||
<form method="post" action="{{ url_for('auth.logout') }}" style="padding:0;border:none;">
|
<form method="post" action="{{ url_for('auth.logout') }}" style="padding:0;border:none;">
|
||||||
<button type="submit">{{ _('Logout') }}</button>
|
<button type="submit">{{ _('Logout') }}</button>
|
||||||
|
|||||||
Binary file not shown.
@@ -651,3 +651,69 @@ msgstr "Notiz gespeichert."
|
|||||||
|
|
||||||
msgid "Please log in to access this page."
|
msgid "Please log in to access this page."
|
||||||
msgstr "Bitte melden Sie sich an, um auf diese Seite zuzugreifen."
|
msgstr "Bitte melden Sie sich an, um auf diese Seite zuzugreifen."
|
||||||
|
|
||||||
|
msgid "Admin — Users"
|
||||||
|
msgstr "Admin — Benutzer"
|
||||||
|
|
||||||
|
msgid "User Management"
|
||||||
|
msgstr "Benutzerverwaltung"
|
||||||
|
|
||||||
|
msgid "users"
|
||||||
|
msgstr "Benutzer"
|
||||||
|
|
||||||
|
msgid "Provider"
|
||||||
|
msgstr "Anbieter"
|
||||||
|
|
||||||
|
msgid "Role"
|
||||||
|
msgstr "Rolle"
|
||||||
|
|
||||||
|
msgid "Language"
|
||||||
|
msgstr "Sprache"
|
||||||
|
|
||||||
|
msgid "Joined"
|
||||||
|
msgstr "Beigetreten"
|
||||||
|
|
||||||
|
msgid "Last login"
|
||||||
|
msgstr "Letzter Login"
|
||||||
|
|
||||||
|
msgid "Actions"
|
||||||
|
msgstr "Aktionen"
|
||||||
|
|
||||||
|
msgid "Local"
|
||||||
|
msgstr "Lokal"
|
||||||
|
|
||||||
|
msgid "Set"
|
||||||
|
msgstr "Setzen"
|
||||||
|
|
||||||
|
msgid "Reset pwd"
|
||||||
|
msgstr "Passwort reset"
|
||||||
|
|
||||||
|
msgid "New password (min 8)"
|
||||||
|
msgstr "Neues Passwort (min 8)"
|
||||||
|
|
||||||
|
msgid "Delete user %(email)s? All their data will be permanently removed."
|
||||||
|
msgstr "Benutzer %(email)s löschen? Alle Daten werden dauerhaft entfernt."
|
||||||
|
|
||||||
|
msgid "(you)"
|
||||||
|
msgstr "(Sie)"
|
||||||
|
|
||||||
|
msgid "Invalid role."
|
||||||
|
msgstr "Ungültige Rolle."
|
||||||
|
|
||||||
|
msgid "Cannot remove the last admin."
|
||||||
|
msgstr "Der letzte Administrator kann nicht entfernt werden."
|
||||||
|
|
||||||
|
msgid "Role updated for %(email)s."
|
||||||
|
msgstr "Rolle für %(email)s aktualisiert."
|
||||||
|
|
||||||
|
msgid "Password reset for %(email)s."
|
||||||
|
msgstr "Passwort für %(email)s zurückgesetzt."
|
||||||
|
|
||||||
|
msgid "You cannot delete your own account."
|
||||||
|
msgstr "Sie können Ihr eigenes Konto nicht löschen."
|
||||||
|
|
||||||
|
msgid "User %(email)s deleted."
|
||||||
|
msgstr "Benutzer %(email)s gelöscht."
|
||||||
|
|
||||||
|
msgid "Admin"
|
||||||
|
msgstr "Admin"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -651,3 +651,69 @@ msgstr "Note sauvegardée."
|
|||||||
|
|
||||||
msgid "Please log in to access this page."
|
msgid "Please log in to access this page."
|
||||||
msgstr "Veuillez vous connecter pour accéder à cette page."
|
msgstr "Veuillez vous connecter pour accéder à cette page."
|
||||||
|
|
||||||
|
msgid "Admin — Users"
|
||||||
|
msgstr "Admin — Utilisateurs"
|
||||||
|
|
||||||
|
msgid "User Management"
|
||||||
|
msgstr "Gestion des utilisateurs"
|
||||||
|
|
||||||
|
msgid "users"
|
||||||
|
msgstr "utilisateurs"
|
||||||
|
|
||||||
|
msgid "Provider"
|
||||||
|
msgstr "Fournisseur"
|
||||||
|
|
||||||
|
msgid "Role"
|
||||||
|
msgstr "Rôle"
|
||||||
|
|
||||||
|
msgid "Language"
|
||||||
|
msgstr "Langue"
|
||||||
|
|
||||||
|
msgid "Joined"
|
||||||
|
msgstr "Inscrit"
|
||||||
|
|
||||||
|
msgid "Last login"
|
||||||
|
msgstr "Dernière connexion"
|
||||||
|
|
||||||
|
msgid "Actions"
|
||||||
|
msgstr "Actions"
|
||||||
|
|
||||||
|
msgid "Local"
|
||||||
|
msgstr "Local"
|
||||||
|
|
||||||
|
msgid "Set"
|
||||||
|
msgstr "Définir"
|
||||||
|
|
||||||
|
msgid "Reset pwd"
|
||||||
|
msgstr "Réinit. mdp"
|
||||||
|
|
||||||
|
msgid "New password (min 8)"
|
||||||
|
msgstr "Nouveau mot de passe (min 8)"
|
||||||
|
|
||||||
|
msgid "Delete user %(email)s? All their data will be permanently removed."
|
||||||
|
msgstr "Supprimer l'utilisateur %(email)s ? Toutes ses données seront définitivement supprimées."
|
||||||
|
|
||||||
|
msgid "(you)"
|
||||||
|
msgstr "(vous)"
|
||||||
|
|
||||||
|
msgid "Invalid role."
|
||||||
|
msgstr "Rôle invalide."
|
||||||
|
|
||||||
|
msgid "Cannot remove the last admin."
|
||||||
|
msgstr "Impossible de supprimer le dernier administrateur."
|
||||||
|
|
||||||
|
msgid "Role updated for %(email)s."
|
||||||
|
msgstr "Rôle mis à jour pour %(email)s."
|
||||||
|
|
||||||
|
msgid "Password reset for %(email)s."
|
||||||
|
msgstr "Mot de passe réinitialisé pour %(email)s."
|
||||||
|
|
||||||
|
msgid "You cannot delete your own account."
|
||||||
|
msgstr "Vous ne pouvez pas supprimer votre propre compte."
|
||||||
|
|
||||||
|
msgid "User %(email)s deleted."
|
||||||
|
msgstr "Utilisateur %(email)s supprimé."
|
||||||
|
|
||||||
|
msgid "Admin"
|
||||||
|
msgstr "Admin"
|
||||||
|
|||||||
Reference in New Issue
Block a user