Initial federation app — ActivityPub server for Rose-Ash
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 41s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 41s
Phase 0+1 of AP integration. New 5th Quart microservice: Blueprints: - wellknown: WebFinger, NodeInfo 2.0, host-meta - actors: AP actor profiles (JSON-LD + HTML), outbox, inbox, followers - identity: username selection flow (creates ActorProfile + RSA keypair) - auth: magic link login/logout (ported from blog, self-contained) Services: - Registers SqlFederationService (real impl) for federation domain - Registers real impls for blog, calendar, market, cart - All cross-domain via shared service contracts Templates: - Actor profiles, username selection, platform home - Auth login/check-email (ported from blog) Infrastructure: - Dockerfile + entrypoint.sh (matches other apps) - CI/CD via Gitea Actions - shared/ as git submodule Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
33
templates/_email/magic_link.html
Normal file
33
templates/_email/magic_link.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><meta charset="utf-8"></head>
|
||||
<body style="margin:0;padding:0;background:#f5f5f4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f5f5f4;padding:40px 0;">
|
||||
<tr><td align="center">
|
||||
<table width="480" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:12px;border:1px solid #e7e5e4;padding:40px;">
|
||||
<tr><td>
|
||||
<h1 style="margin:0 0 8px;font-size:20px;font-weight:600;color:#1c1917;">{{ site_name }}</h1>
|
||||
<p style="margin:0 0 24px;font-size:15px;color:#57534e;">Sign in to your account</p>
|
||||
<p style="margin:0 0 24px;font-size:15px;line-height:1.5;color:#44403c;">
|
||||
Click the button below to sign in. This link will expire in 15 minutes.
|
||||
</p>
|
||||
<table cellpadding="0" cellspacing="0" style="margin:0 0 24px;"><tr><td style="border-radius:8px;background:#1c1917;">
|
||||
<a href="{{ link_url }}" target="_blank"
|
||||
style="display:inline-block;padding:12px 32px;font-size:15px;font-weight:500;color:#ffffff;text-decoration:none;border-radius:8px;">
|
||||
Sign in
|
||||
</a>
|
||||
</td></tr></table>
|
||||
<p style="margin:0 0 8px;font-size:13px;color:#78716c;">Or copy and paste this link into your browser:</p>
|
||||
<p style="margin:0 0 24px;font-size:13px;word-break:break-all;">
|
||||
<a href="{{ link_url }}" style="color:#1c1917;">{{ link_url }}</a>
|
||||
</p>
|
||||
<hr style="border:none;border-top:1px solid #e7e5e4;margin:24px 0;">
|
||||
<p style="margin:0;font-size:12px;color:#a8a29e;">
|
||||
If you did not request this email, you can safely ignore it.
|
||||
</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
8
templates/_email/magic_link.txt
Normal file
8
templates/_email/magic_link.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Hello,
|
||||
|
||||
Click this link to sign in:
|
||||
{{ link_url }}
|
||||
|
||||
This link will expire in 15 minutes.
|
||||
|
||||
If you did not request this, you can ignore this email.
|
||||
18
templates/auth/check_email.html
Normal file
18
templates/auth/check_email.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "federation/base.html" %}
|
||||
{% block title %}Check your email — Rose Ash{% endblock %}
|
||||
{% block content %}
|
||||
<div class="py-8 max-w-md mx-auto text-center">
|
||||
<h1 class="text-2xl font-bold mb-4">Check your email</h1>
|
||||
<p class="text-stone-600 mb-2">
|
||||
We sent a sign-in link to <strong>{{ email }}</strong>.
|
||||
</p>
|
||||
<p class="text-stone-500 text-sm">
|
||||
Click the link in the email to sign in. The link expires in 15 minutes.
|
||||
</p>
|
||||
{% if email_error %}
|
||||
<div class="bg-yellow-50 border border-yellow-200 text-yellow-700 p-3 rounded mt-4">
|
||||
{{ email_error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
34
templates/auth/login.html
Normal file
34
templates/auth/login.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends "federation/base.html" %}
|
||||
{% block title %}Login — Rose Ash{% endblock %}
|
||||
{% block content %}
|
||||
<div class="py-8 max-w-md mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-6">Sign in</h1>
|
||||
|
||||
{% if error %}
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ url_for('auth.start_login') }}" class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium mb-1">Email address</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
value="{{ email | default('') }}"
|
||||
required
|
||||
autofocus
|
||||
class="w-full border border-stone-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-stone-500"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition"
|
||||
>
|
||||
Send magic link
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
27
templates/federation/account.html
Normal file
27
templates/federation/account.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends "federation/base.html" %}
|
||||
{% block title %}Account — Rose Ash{% endblock %}
|
||||
{% block content %}
|
||||
<div class="py-8">
|
||||
<h1 class="text-2xl font-bold mb-4">Account</h1>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<p><strong>Email:</strong> {{ g.user.email }}</p>
|
||||
{% if actor %}
|
||||
<p class="mt-2"><strong>Username:</strong> @{{ actor.preferred_username }}</p>
|
||||
<p class="mt-1">
|
||||
<a href="{{ url_for('actors.profile', username=actor.preferred_username) }}"
|
||||
class="text-blue-600 hover:underline">
|
||||
View profile
|
||||
</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="mt-4">
|
||||
<a href="{{ url_for('identity.choose_username_form') }}"
|
||||
class="bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition">
|
||||
Choose a username to start publishing
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
35
templates/federation/base.html
Normal file
35
templates/federation/base.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Rose Ash{% endblock %}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-stone-50 text-stone-900 min-h-screen">
|
||||
<nav class="bg-stone-800 text-white p-4">
|
||||
<div class="max-w-4xl mx-auto flex items-center justify-between">
|
||||
<a href="/" class="font-bold text-lg">Rose Ash</a>
|
||||
<div class="flex items-center gap-4">
|
||||
{% if g.user %}
|
||||
{% if actor %}
|
||||
<a href="{{ url_for('actors.profile', username=actor.preferred_username) }}" class="hover:underline">
|
||||
@{{ actor.preferred_username }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('identity.choose_username_form') }}" class="hover:underline">Choose username</a>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('auth.logout') }}" class="inline">
|
||||
<button type="submit" class="hover:underline">Logout</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.login_form') }}" class="hover:underline">Login</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="max-w-4xl mx-auto p-4">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
53
templates/federation/choose_username.html
Normal file
53
templates/federation/choose_username.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends "federation/base.html" %}
|
||||
{% block title %}Choose Username — Rose Ash{% endblock %}
|
||||
{% block content %}
|
||||
<div class="py-8 max-w-md mx-auto">
|
||||
<h1 class="text-2xl font-bold mb-2">Choose your username</h1>
|
||||
<p class="text-stone-600 mb-6">
|
||||
This will be your identity on the fediverse:
|
||||
<strong>@username@{{ config.get('ap_domain', 'rose-ash.com') }}</strong>
|
||||
</p>
|
||||
|
||||
{% if error %}
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium mb-1">Username</label>
|
||||
<div class="flex items-center">
|
||||
<span class="text-stone-400 mr-1">@</span>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
value="{{ username | default('') }}"
|
||||
pattern="[a-z][a-z0-9_]{2,31}"
|
||||
minlength="3"
|
||||
maxlength="32"
|
||||
required
|
||||
autocomplete="off"
|
||||
class="flex-1 border border-stone-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-stone-500"
|
||||
hx-get="{{ url_for('identity.check_username') }}"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#username-status"
|
||||
hx-include="[name='username']"
|
||||
>
|
||||
</div>
|
||||
<div id="username-status" class="text-sm mt-1"></div>
|
||||
<p class="text-xs text-stone-400 mt-1">
|
||||
3-32 characters. Lowercase letters, numbers, underscores. Must start with a letter.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition"
|
||||
>
|
||||
Claim username
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
23
templates/federation/home.html
Normal file
23
templates/federation/home.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "federation/base.html" %}
|
||||
{% block title %}Rose Ash — Federation{% endblock %}
|
||||
{% block content %}
|
||||
<div class="py-8">
|
||||
<h1 class="text-3xl font-bold mb-4">Rose Ash</h1>
|
||||
<p class="text-stone-600 mb-8">Cooperative platform with ActivityPub federation.</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="text-3xl font-bold">{{ stats.actors }}</div>
|
||||
<div class="text-stone-500">Actors</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="text-3xl font-bold">{{ stats.activities }}</div>
|
||||
<div class="text-stone-500">Activities</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="text-3xl font-bold">{{ stats.followers }}</div>
|
||||
<div class="text-stone-500">Followers</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
templates/federation/profile.html
Normal file
32
templates/federation/profile.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "federation/base.html" %}
|
||||
{% block title %}@{{ actor.preferred_username }} — Rose Ash{% endblock %}
|
||||
{% block content %}
|
||||
<div class="py-8">
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
||||
<h1 class="text-2xl font-bold">{{ actor.display_name or actor.preferred_username }}</h1>
|
||||
<p class="text-stone-500">@{{ actor.preferred_username }}@{{ config.get('ap_domain', 'rose-ash.com') }}</p>
|
||||
{% if actor.summary %}
|
||||
<p class="mt-2">{{ actor.summary }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-bold mb-4">Activities ({{ total }})</h2>
|
||||
{% if activities %}
|
||||
<div class="space-y-4">
|
||||
{% for a in activities %}
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="font-medium">{{ a.activity_type }}</span>
|
||||
<span class="text-sm text-stone-400">{{ a.published.strftime('%Y-%m-%d %H:%M') if a.published }}</span>
|
||||
</div>
|
||||
{% if a.object_type %}
|
||||
<span class="text-sm text-stone-500">{{ a.object_type }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-stone-500">No activities yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user