Compare commits

..

4 Commits

Author SHA1 Message Date
Julien Calixte
345f3c93aa Redirect to production for login when on local dev
OAuth PKCE state is origin-scoped — initiating from 127.0.0.1
and receiving callback on coffee.apoena.dev breaks the flow.
From local dev, redirect to production login with handle pre-filled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 23:23:43 +01:00
Julien Calixte
d67aa41838 Show real auth error on OAuth callback instead of generic message
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 23:23:04 +01:00
Julien Calixte
6051d741b5 Restore 127.0.0.1 host for Vite dev server
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 23:21:47 +01:00
Julien Calixte
f591c2b0a0 Remove loopback redirect URIs — web apps can't use them per ATProto spec
OAuth only works on coffee.apoena.dev. Local dev is for UI only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 23:20:25 +01:00
4 changed files with 16 additions and 9 deletions

View File

@@ -3,9 +3,7 @@
"client_name": "Coffee Map",
"client_uri": "https://coffee.apoena.dev",
"redirect_uris": [
"https://coffee.apoena.dev/oauth/callback",
"http://127.0.0.1:5173/oauth/callback",
"http://127.0.0.1:5174/oauth/callback"
"https://coffee.apoena.dev/oauth/callback"
],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],

View File

@@ -5,9 +5,8 @@ import { Agent } from '@atproto/api'
// so the PDS can fetch it — even in local dev.
const PROD_URL = 'https://coffee.apoena.dev'
// redirect_uri is dynamic so local dev redirects back to the right origin.
// RFC 8252 forbids "localhost" — replace with 127.0.0.1 for loopback.
const ORIGIN = window.location.origin.replace('localhost', '127.0.0.1')
// OAuth only works on the deployed domain (web apps can't use loopback).
const ORIGIN = PROD_URL
let _client: BrowserOAuthClient | null = null

View File

@@ -40,14 +40,24 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()
const handle = ref('')
const route = useRoute()
const handle = ref((route.query.handle as string) ?? '')
const loading = ref(false)
const error = ref('')
const PROD_URL = 'https://coffee.apoena.dev'
const isLocalDev = window.location.origin !== PROD_URL
async function handleSubmit() {
// OAuth state is scoped to the initiating origin — must start from production
if (isLocalDev) {
window.location.href = `${PROD_URL}/login?handle=${encodeURIComponent(handle.value.trim())}`
return
}
loading.value = true
error.value = ''
try {

View File

@@ -2,7 +2,7 @@
<div class="min-h-screen bg-coffee-50 flex items-center justify-center">
<div class="text-center">
<div class="text-4xl mb-4"></div>
<p v-if="error" class="text-red-600">{{ error }}</p>
<p v-if="error || auth.error" class="text-red-600 max-w-sm px-4">{{ error || auth.error }}</p>
<p v-else class="text-coffee-600">Signing you in</p>
</div>
</div>
@@ -26,7 +26,7 @@ onMounted(async () => {
await shops.fetchAll()
router.replace('/')
} else {
error.value = 'Authentication failed. Please try again.'
error.value = auth.error ?? 'Authentication failed — check console for details.'
}
} catch (e) {
error.value = e instanceof Error ? e.message : 'Authentication error'