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_name": "Coffee Map",
"client_uri": "https://coffee.apoena.dev", "client_uri": "https://coffee.apoena.dev",
"redirect_uris": [ "redirect_uris": [
"https://coffee.apoena.dev/oauth/callback", "https://coffee.apoena.dev/oauth/callback"
"http://127.0.0.1:5173/oauth/callback",
"http://127.0.0.1:5174/oauth/callback"
], ],
"grant_types": ["authorization_code", "refresh_token"], "grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"], "response_types": ["code"],

View File

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

View File

@@ -40,14 +40,24 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore() const auth = useAuthStore()
const handle = ref('') const route = useRoute()
const handle = ref((route.query.handle as string) ?? '')
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
const PROD_URL = 'https://coffee.apoena.dev'
const isLocalDev = window.location.origin !== PROD_URL
async function handleSubmit() { 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 loading.value = true
error.value = '' error.value = ''
try { try {

View File

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