Compare commits

...

8 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
Julien Calixte
5d3dcfb4bd Bind Vite dev server to 127.0.0.1 instead of localhost
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 23:18:52 +01:00
Julien Calixte
6167df084f Replace localhost with 127.0.0.1 in redirect_uris (RFC 8252)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 23:18:32 +01:00
Julien Calixte
3576ccf976 Fix signIn error swallowing — re-throw so LoginView shows the error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 23:17:47 +01:00
Julien Calixte
eaa151dbe9 Fix OAuth redirect_uri for local dev
- client_id always points to production so PDS can fetch metadata
- redirect_uri is dynamic (window.location.origin) so dev login
  redirects back to localhost instead of production
- Add localhost:5173/5174 to allowed redirect_uris in metadata

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 23:14:11 +01:00
6 changed files with 30 additions and 12 deletions

View File

@@ -2,7 +2,9 @@
"client_id": "https://coffee.apoena.dev/client-metadata.json",
"client_name": "Coffee Map",
"client_uri": "https://coffee.apoena.dev",
"redirect_uris": ["https://coffee.apoena.dev/oauth/callback"],
"redirect_uris": [
"https://coffee.apoena.dev/oauth/callback"
],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "atproto transition:generic",

View File

@@ -1,9 +1,12 @@
import { BrowserOAuthClient } from '@atproto/oauth-client-browser'
import { Agent } from '@atproto/api'
// The client_id must equal the public URL of client-metadata.json.
// Update VITE_APP_URL in your environment or set it here directly.
const APP_URL = import.meta.env.VITE_APP_URL ?? 'https://coffee.apoena.dev'
// client_id must always point to the publicly accessible metadata file
// so the PDS can fetch it — even in local dev.
const PROD_URL = 'https://coffee.apoena.dev'
// OAuth only works on the deployed domain (web apps can't use loopback).
const ORIGIN = PROD_URL
let _client: BrowserOAuthClient | null = null
@@ -11,10 +14,10 @@ export function getOAuthClient(): BrowserOAuthClient {
if (!_client) {
_client = new BrowserOAuthClient({
clientMetadata: {
client_id: `${APP_URL}/client-metadata.json`,
client_id: `${PROD_URL}/client-metadata.json`,
client_name: 'Coffee Map',
client_uri: APP_URL,
redirect_uris: [`${APP_URL}/oauth/callback`],
client_uri: PROD_URL,
redirect_uris: [`${ORIGIN}/oauth/callback`],
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code'],
scope: 'atproto transition:generic',
@@ -22,7 +25,6 @@ export function getOAuthClient(): BrowserOAuthClient {
token_endpoint_auth_method: 'none',
application_type: 'web',
},
// Use the public ATProto resolver — for full privacy use your own PDS
handleResolver: 'https://bsky.social',
})
}

View File

@@ -46,6 +46,7 @@ export const useAuthStore = defineStore('auth', () => {
} catch (e) {
error.value = e instanceof Error ? e.message : String(e)
loading.value = false
throw e
}
}

View File

@@ -23,7 +23,7 @@
/>
</div>
<p v-if="error" class="text-red-600 text-sm">{{ error }}</p>
<p v-if="error || auth.error" class="text-red-600 text-sm">{{ error || auth.error }}</p>
<button
type="submit"
@@ -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'

View File

@@ -5,6 +5,9 @@ import UnoCSS from 'unocss/vite'
import { fileURLToPath } from 'node:url'
export default defineConfig({
server: {
host: '127.0.0.1',
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),