Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.dev.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ VISUALIZER_RESULTS_DIR=./results
# VISUALIZER_MAX_TREE_DEPTH=8
# VISUALIZER_MAX_FILE_SIZE_MB=100

# --- Gravatar avatars -----------------------------------------------------------
# Off by default: the user's initial is shown. Set to "true" to load avatars
# from gravatar.com (the browser sends the MD5 hash of the user's email there).
# SIMPLEAUDIT_GRAVATAR=true

# --- Chat (Open WebUI) ----------------------------------------------------------
# On by default in dev; no line needed. To force it off:
# SIMPLEAUDIT_CHAT=off (env)
Expand Down
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ SIMPLEAUDIT_STUDIO_URL=http://localhost:8000
# management) on or off. ON by default (preserves existing behavior). Set to
# "off" to 404 the /otlp/* and /api/otlp/* routes and hide the OTLP button.
#SIMPLEAUDIT_OTLP=off
# Optional: show Gravatar avatars. OFF by default (the user's initial is shown).
# Set to "true" to load them from gravatar.com; the browser then sends the MD5
# hash of the user's email there.
#SIMPLEAUDIT_GRAVATAR=true

# --- Optional observability -----------------------------------------------------

Expand Down
4 changes: 4 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@ def _csrf_trusted_origins() -> list[str]:
WORKOS_API_KEY = os.environ.get("WORKOS_API_KEY", "")
WORKOS_ENABLED = bool(WORKOS_CLIENT_ID and WORKOS_API_KEY)

# Gravatar avatars: off by default, since loading one sends the MD5 hash of the
# user's email to gravatar.com. When off, templates show the user's initial.
SIMPLEAUDIT_GRAVATAR = env_bool("SIMPLEAUDIT_GRAVATAR", False)

# Demo mode: prefill the login form with demo credentials and show a hint banner
# (public demos / HF Spaces).
DEMO_MODE = env_bool("DEMO_MODE", False)
Expand Down
1 change: 1 addition & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ override them when you deliberately need to.
| `BOOTSTRAP_USERNAME`, `BOOTSTRAP_EMAIL`, `BOOTSTRAP_PASSWORD`, `BOOTSTRAP_PROJECT_NAME` | First admin user and workspace. |
| `DEMO_MODE`, `DEMO_USERNAME`, `DEMO_PASSWORD` | Prefilled demo login and the HF iframe cookie/CSRF policy. |
| `WORKOS_CLIENT_ID`, `WORKOS_API_KEY` | Optional passwordless email sign-in. |
| `SIMPLEAUDIT_GRAVATAR` | `true` = show Gravatar avatars (the browser sends the MD5 hash of the user's email to gravatar.com). Default off: the user's initial is shown. |
| `SENTRY_DSN`, `SENTRY_ENVIRONMENT` | Optional error tracking. |
| `LOG_LEVEL`, `PORT` | Logging level; web port for the CLI. |
| any name in a connection's `secret_reference` | Model API key read at execution time. |
Expand Down
27 changes: 19 additions & 8 deletions infra/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@

import hashlib

from django.conf import settings

def gravatar_url(request):
"""Expose the current user's Gravatar URL to templates.

The avatar is derived from the user's email (MD5 hash), so no storage or
upload is needed. Returns ``None`` for anonymous users or users without
an email, letting templates fall back to initials.
def gravatar_for(user) -> str | None:
"""The user's Gravatar URL, or ``None`` so templates fall back to initials.

The URL carries the MD5 hash of the user's email, and the browser sends it
to gravatar.com when it loads the image. Off unless ``SIMPLEAUDIT_GRAVATAR``
is set. Also ``None`` for anonymous users and users without an email.
"""
user = getattr(request, "user", None)
if not settings.SIMPLEAUDIT_GRAVATAR:
return None
if user is None or not user.is_authenticated or not user.email:
return {"gravatar_url": None}
return None
email = user.email.strip().lower()
md5_hash = hashlib.md5(email.encode("utf-8")).hexdigest()
return {"gravatar_url": f"https://www.gravatar.com/avatar/{md5_hash}?d=identicon&s=128"}
return f"https://www.gravatar.com/avatar/{md5_hash}?d=identicon&s=128"


def gravatar_url(request):
"""Expose the current user's Gravatar URL (or ``None``) and the setting."""
return {
"gravatar_url": gravatar_for(getattr(request, "user", None)),
"gravatar_enabled": settings.SIMPLEAUDIT_GRAVATAR,
}


def admin_status(request):
Expand Down
36 changes: 36 additions & 0 deletions infra/tests/test_gravatar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Gravatar avatars are opt-in: off by default, no gravatar.com URL is rendered."""
from django.contrib.auth.models import AnonymousUser
from django.test import Client, TestCase, override_settings

from infra.context_processors import gravatar_for
from infra.tests.factories import UserFactory


class GravatarSettingTest(TestCase):
def setUp(self):
self.user = UserFactory(username="jane", email="Jane@X.com")
self.client = Client(SERVER_NAME="localhost")
self.client.force_login(self.user)

def test_off_by_default(self):
self.assertIsNone(gravatar_for(self.user))
content = self.client.get("/profile/").content.decode()
self.assertNotIn("gravatar.com/avatar/", content)

def test_off_shows_no_gravatar_text_on_the_profile(self):
content = self.client.get("/profile/").content.decode()
self.assertNotIn("Add an email address to get a Gravatar avatar.", content)
self.assertNotIn("Update image on Gravatar", content)

@override_settings(SIMPLEAUDIT_GRAVATAR=True)
def test_on_renders_the_gravatar_url(self):
# MD5 of the trimmed, lower-cased email "jane@x.com".
url = "https://www.gravatar.com/avatar/8e747d19e83ff063fbb509e348a296d4"
self.assertTrue(gravatar_for(self.user).startswith(url))
content = self.client.get("/profile/").content.decode()
self.assertIn(url, content)

@override_settings(SIMPLEAUDIT_GRAVATAR=True)
def test_on_still_none_without_a_signed_in_user_with_email(self):
self.assertIsNone(gravatar_for(AnonymousUser()))
self.assertIsNone(gravatar_for(UserFactory(username="noemail", email="")))
12 changes: 3 additions & 9 deletions infra/visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,16 +517,10 @@ def _inject_sidebar(html: str, request) -> str:

# render_to_string with a plain context does not run context processors, so
# pass the values the partial needs explicitly (user, gravatar).
from infra.context_processors import gravatar_for

user = getattr(request, "user", None)
gravatar = None
if user is not None and user.is_authenticated and user.email:
import hashlib

gravatar = (
"https://www.gravatar.com/avatar/"
+ hashlib.md5(user.email.strip().lower().encode("utf-8")).hexdigest()
+ "?d=identicon&s=128"
)
gravatar = gravatar_for(user)
sidebar = render_to_string(
"partials/visualizer_sidebar.html",
{"request": request, "user": user, "gravatar_url": gravatar},
Expand Down
2 changes: 1 addition & 1 deletion static/scenario_viewer.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<title>SimpleAudit Scenario Viewer</title>
<link rel="icon" type="image/svg+xml" sizes="32x32" href="/static/favicon-32x32.svg">
<link rel="icon" type="image/svg+xml" sizes="16x16" href="/static/favicon-16x16.svg">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
<script>
tailwind.config = {
theme: {
Expand Down
2 changes: 1 addition & 1 deletion static/visualizer.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<script>window.__VISUALIZER_API_BASE = '{{ visualizer_api_base|default:"" }}';</script>
<link rel="icon" type="image/svg+xml" sizes="32x32" href="/static/favicon-32x32.svg">
<link rel="icon" type="image/svg+xml" sizes="16x16" href="/static/favicon-16x16.svg">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
<script>
tailwind.config = {
theme: {
Expand Down
2 changes: 1 addition & 1 deletion templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
{% include "seo/head.html" with seo_title=page_title seo_description=page_description seo_robots=page_robots|default:"noindex,nofollow" seo_canonical=page_canonical seo_jsonld=page_jsonld og_image_url=og_image_url og_image_width=og_image_width og_image_height=og_image_height og_image_alt=og_image_alt %}
<link rel="icon" type="image/svg+xml" sizes="32x32" href="{% static 'favicon-32x32.svg' %}">
<link rel="icon" type="image/svg+xml" sizes="16x16" href="{% static 'favicon-16x16.svg' %}">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
<script>
tailwind.config = {
theme: {
Expand Down
2 changes: 2 additions & 0 deletions templates/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ <h2 class="text-sm font-semibold text-gray-300 mb-4">Profile image</h2>
<div class="w-16 h-16 rounded-full bg-accent/20 flex items-center justify-center text-xl font-bold text-accent">{{ profile_user.username|first|upper }}</div>
{% endif %}
<div class="text-sm">
{% if gravatar_enabled %}
<p class="text-gray-400">
{% if gravatar_url %}
Your avatar comes from <a href="https://gravatar.com" target="_blank" rel="noopener" class="text-accent hover:underline">Gravatar</a>, matched to your email.
Expand All @@ -29,6 +30,7 @@ <h2 class="text-sm font-semibold text-gray-300 mb-4">Profile image</h2>
class="inline-block mt-2 px-3 py-1.5 rounded-lg text-xs bg-surface-overlay border border-gray-700 text-gray-200 hover:border-accent/40 transition-colors">
Update image on Gravatar ↗
</a>
{% endif %}
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion templates/public/base_public.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{% include "seo/head.html" with seo_title=page_title seo_description=page_description seo_robots=page_robots|default:"index,follow" seo_canonical=page_canonical seo_jsonld=page_jsonld og_image_url=og_image_url og_image_width=og_image_width og_image_height=og_image_height og_image_alt=og_image_alt %}
<link rel="icon" type="image/svg+xml" sizes="32x32" href="{% static 'favicon-32x32.svg' %}">
<link rel="icon" type="image/svg+xml" sizes="16x16" href="{% static 'favicon-16x16.svg' %}">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
<script>
tailwind.config = { theme: { extend: { colors: { surface: { DEFAULT: '#0f1117', raised: '#1a1d27', overlay: '#242836' }, accent: { DEFAULT: '#6c8cff', hover: '#8da4ff' } } } } };
</script>
Expand Down
Loading