From 235322b4c0a6565ae806728a46403cdf376e0abd Mon Sep 17 00:00:00 2001 From: outlaw-dame <141657791+outlaw-dame@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:09:17 -0400 Subject: [PATCH 01/19] Fix: allow all proxied hosts in Vite dev server config Adds server.allowedHosts: true so the app works when accessed via tunnelled/proxied URLs (Manus sandbox, ngrok, Cloudflare Tunnel, etc.) --- frontend/vite.config.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index b9ebf0aa5..e51c806ea 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -16,5 +16,11 @@ export default defineConfig({ '@': fileURLToPath(new URL('./src', import.meta.url)), '#api': resolve(__dirname, '../api/src') } + }, + server: { + // Allow all proxied / tunnelled hosts (e.g. Manus sandbox, ngrok, Cloudflare Tunnel) + allowedHosts: true, + host: '0.0.0.0', + port: 5173 } }) From 2d88348b9e476c61568dd19ced7ea5ded73cd559 Mon Sep 17 00:00:00 2001 From: outlaw-dame <141657791+outlaw-dame@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:18:52 -0400 Subject: [PATCH 02/19] Fix auth: add ActivityPods stack + pod provider URL to sign-in/up forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docker-compose.local.yml: - Add Fuseki (RDF triple store), Redis, and ActivityPods backend services - ActivityPods backend runs on host network (port 3000) so it can reach the Memory API at localhost:8794 — matches original dev docker-compose - PostgreSQL, Memory API, and Vite frontend remain on bridge network - Mastopod sidecar uses host network to reach Memory API webhook api/src/types/enums.ts: - Replace fixed t.Enum({ 'http://localhost:3000': ... }) with t.String() - Accepts any valid http:// or https:// URL as pod provider endpoint - Enables connecting to external providers (activitypods.org, etc.) frontend/src/components/SignInForm.vue: - Show 'Pod Provider URL' field (default: http://localhost:3000) - Add async/await + try/catch error handling - Show inline error message on failure frontend/src/components/SignupForm.vue: - Show 'Pod Provider URL' field (default: http://localhost:3000) - Clear test-only pre-filled values (username/email/password) - Add endpoint URL validation - Show general error message on failure --- api/src/types/enums.ts | 20 ++++- docker-compose.local.yml | 110 ++++++++++++++++++++++--- frontend/src/components/SignInForm.vue | 58 ++++++++++--- frontend/src/components/SignupForm.vue | 61 +++++++++++--- 4 files changed, 213 insertions(+), 36 deletions(-) diff --git a/api/src/types/enums.ts b/api/src/types/enums.ts index 0e6fc1bb2..371e22a9d 100644 --- a/api/src/types/enums.ts +++ b/api/src/types/enums.ts @@ -1,5 +1,21 @@ import { t } from 'elysia' -export const viablePodProviders = t.Enum({ - 'http://localhost:3000': 'http://localhost:3000' +/** + * Accepts any valid HTTP/HTTPS URL as a pod provider endpoint. + * + * The original fixed enum only allowed http://localhost:3000, which prevented + * users from connecting to external or custom ActivityPods pod providers. + * We validate the URL format here; the ActivityPods service validates the + * endpoint's actual response at runtime. + * + * Security note: The URL is used only to make server-side requests to the + * pod provider's /auth/login and /auth/signup endpoints. It is never reflected + * back to the client without sanitisation. + */ +export const viablePodProviders = t.String({ + minLength: 7, + maxLength: 512, + pattern: '^https?://', + description: 'URL of the ActivityPods pod provider (e.g. http://localhost:3000)', + default: 'http://localhost:3000', }) diff --git a/docker-compose.local.yml b/docker-compose.local.yml index e7d2f6b68..8a3248d83 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -1,23 +1,106 @@ # ============================================================================ # Memory App + AT Protocol Bridge — Full Local Stack # -# Runs everything you need to see the Memory app in your browser with live -# AT Protocol federated content flowing in from the Mastopod sidecar. +# Runs the complete stack locally including the ActivityPods pod provider +# so users can sign up and sign in via their local pod. # # Services: -# pg — PostgreSQL 17 database -# api — Memory API (Elysia/Bun) on http://localhost:8794 -# frontend — Memory UI (Vite/Vue) on http://localhost:5173 -# mastopod — AT Protocol ingress pipeline (Phase 5.5 local harness) +# fuseki — Apache Jena Fuseki (RDF triple store for ActivityPods) +# redis — Redis (ActivityPods job queue + AT cursor store) +# activitypods — ActivityPods backend (pod provider on :3000) +# pg — PostgreSQL 17 (Memory API database) +# api — Memory API (Elysia/Bun) on :8794 +# frontend — Memory UI (Vite/Vue) on :5173 +# mastopod — AT Protocol ingress pipeline (Phase 5.5) # # Usage: # docker compose -f docker-compose.local.yml up --build # # Then open: http://localhost:5173 +# +# Sign up at: http://localhost:5173 (uses the local ActivityPods pod provider +# at http://localhost:3000 — this is pre-filled in the form) # ============================================================================ services: + # -------------------------------------------------------------------------- + # Apache Jena Fuseki — RDF triple store required by ActivityPods + # -------------------------------------------------------------------------- + fuseki: + image: semapps/jena-fuseki-webacl + restart: unless-stopped + ports: + - '3030:3030' + volumes: + - fuseki_data:/fuseki + environment: + ADMIN_PASSWORD: localadmin + healthcheck: + test: ['CMD-SHELL', 'curl -sf http://localhost:3030/$$/ping || exit 1'] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + + # -------------------------------------------------------------------------- + # Redis — job queue for ActivityPods + optional AT cursor store + # -------------------------------------------------------------------------- + redis: + image: redis:7-alpine + restart: unless-stopped + ports: + - '6379:6379' + volumes: + - redis_data:/data + command: ['redis-server', '--appendonly', 'yes'] + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 5s + timeout: 3s + retries: 10 + + # -------------------------------------------------------------------------- + # ActivityPods Backend — the Solid/ActivityPub pod provider + # Users sign up and sign in through this service. + # It exposes: + # POST /auth/signup — create a new pod + # POST /auth/login — get a bearer token + # GET /.well-known/openid-configuration — Solid-OIDC discovery + # -------------------------------------------------------------------------- + activitypods: + image: activitypods/backend:latest + restart: unless-stopped + depends_on: + fuseki: + condition: service_healthy + redis: + condition: service_healthy + ports: + - '3000:3000' + volumes: + - activitypods_logs:/app/backend/logs + - activitypods_jwt:/app/backend/jwt + - activitypods_uploads:/app/backend/uploads + environment: + SEMAPPS_JENA_PASSWORD: localadmin + SEMAPPS_APP_LANG: en + # Disable Mapbox (not needed for local dev — only used for address autocomplete) + SEMAPPS_MAPBOX_ACCESS_TOKEN: '' + # Use the local Fuseki instance + SEMAPPS_JENA_URL: http://fuseki:3030 + # Redis connection + REDIS_HOST: redis + REDIS_PORT: 6379 + # ActivityPods uses host networking so it can reach the app backend at localhost:8794 + network_mode: host + healthcheck: + test: ['CMD-SHELL', 'curl -sf http://localhost:3000/.well-known/openid-configuration || exit 1'] + interval: 15s + timeout: 5s + retries: 15 + start_period: 30s + # -------------------------------------------------------------------------- # PostgreSQL — the database for the Memory API # -------------------------------------------------------------------------- @@ -76,12 +159,10 @@ services: depends_on: - api ports: - - '5173:5173' + - '5173:4000' # -------------------------------------------------------------------------- - # Mastopod AT Protocol Ingress Pipeline (Phase 5.5 local harness) - # Runs the mock firehose + full ingress pipeline and forwards verified events - # to the Memory API webhook endpoint. + # Mastopod AT Protocol Ingress Pipeline (Phase 5.5) # -------------------------------------------------------------------------- mastopod: build: @@ -92,10 +173,15 @@ services: api: condition: service_healthy environment: - MEMORY_WEBHOOK_URL: http://api:8794/at/webhook/ingress + MEMORY_WEBHOOK_URL: http://localhost:8794/at/webhook/ingress FIREHOSE_BRIDGE_SECRET: local-bridge-secret-123 - # Set to 'true' to connect to the real Bluesky relay instead of the mock USE_REAL_FIREHOSE: 'false' + network_mode: host volumes: + fuseki_data: + redis_data: + activitypods_logs: + activitypods_jwt: + activitypods_uploads: pg_data: diff --git a/frontend/src/components/SignInForm.vue b/frontend/src/components/SignInForm.vue index 4a355dc59..7a41942db 100644 --- a/frontend/src/components/SignInForm.vue +++ b/frontend/src/components/SignInForm.vue @@ -1,6 +1,5 @@ diff --git a/frontend/src/components/SignupForm.vue b/frontend/src/components/SignupForm.vue index 704b874ff..b84ab417e 100644 --- a/frontend/src/components/SignupForm.vue +++ b/frontend/src/components/SignupForm.vue @@ -3,23 +3,29 @@ import { validateEmail, validatePassword, validateUsername } from '@/controller/ import { useAuthStore } from '@/stores/authStore' import MemoryButton from '@/components/MemoryButton.vue' import MemoryInput from '@/components/MemoryInput.vue' -import type { ProviderEndpoints } from '@/types/api' import { ref } from 'vue' import { ProviderSignUpErrors } from '@/types' // Store const authStore = useAuthStore() -// Form Data -const username = ref('test') -const email = ref('test@test.com') -const password = ref('testtest') -const confirmPassword = ref('testtest') -const endpoint = ref('http://localhost:3000') +// Form Data — no pre-filled test values in production +const username = ref('') +const email = ref('') +const password = ref('') +const confirmPassword = ref('') +/** + * Pod provider endpoint — defaults to the local ActivityPods instance. + * Users connecting to an external pod provider can change this URL. + * e.g. https://activitypods.org + */ +const endpoint = ref('http://localhost:3000') + // Validation const formIsValid = ref(true) const formLifeCheck = ref(false) const errorMessages = ref>({}) + // Variables const isRequesting = ref(false) @@ -28,10 +34,14 @@ async function submitForm() { validateForm() if (formIsValid.value) { isRequesting.value = true - const authResponse = await authStore.signup(email.value, username.value, password.value, endpoint.value) + const authResponse = await authStore.signup( + email.value, + username.value, + password.value, + endpoint.value as any, + ) // if the response is a string, it means that there was an error if (authResponse) { - console.log(authResponse) switch (authResponse) { case ProviderSignUpErrors['username.already.exists']: case ProviderSignUpErrors['username.invalid']: @@ -45,7 +55,7 @@ async function submitForm() { errorMessages.value.default = authResponse break } - console.error(authResponse) + console.error('[SignupForm] signup error:', authResponse) } isRequesting.value = false } @@ -81,13 +91,20 @@ function validateForm(): void { errorMessages.value.username = usernameVal valid = false } + + // endpoint check + if (!endpoint.value.startsWith('http')) { + errorMessages.value.endpoint = 'Must be a valid http:// or https:// URL' + valid = false + } + formIsValid.value = valid } } diff --git a/frontend/src/components/ControlBar.vue b/frontend/src/components/ControlBar.vue index 8d9eaa083..a76905a35 100644 --- a/frontend/src/components/ControlBar.vue +++ b/frontend/src/components/ControlBar.vue @@ -2,6 +2,7 @@ import { ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import MemoryButton from '@/components/MemoryButton.vue' +import AppIcon from '@/components/AppIcon.vue' // External const route = useRoute() @@ -23,8 +24,8 @@ function getHeader() {