Compare commits

..

10 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
Julien Calixte
a40108ea04 Use Tabler coffee icon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 23:11:36 +01:00
Julien Calixte
b2e10b85f2 Add SVG app icon, fix missing PWA icons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 23:11:01 +01:00
7 changed files with 44 additions and 27 deletions

View File

@@ -2,7 +2,9 @@
"client_id": "https://coffee.apoena.dev/client-metadata.json", "client_id": "https://coffee.apoena.dev/client-metadata.json",
"client_name": "Coffee Map", "client_name": "Coffee Map",
"client_uri": "https://coffee.apoena.dev", "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"], "grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"], "response_types": ["code"],
"scope": "atproto transition:generic", "scope": "atproto transition:generic",

9
public/icons/icon.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#fdf6ec" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="24" height="24" rx="4" fill="#6f4e37"/>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M3 14c.83 .642 2.077 1.017 3.5 1c1.423 .017 2.67 -.358 3.5 -1c.83 -.642 2.077 -1.017 3.5 -1c1.423 -.017 2.67 .358 3.5 1"/>
<path d="M8 3a2.4 2.4 0 0 0 -1 2a2.4 2.4 0 0 0 1 2"/>
<path d="M12 3a2.4 2.4 0 0 0 -1 2a2.4 2.4 0 0 0 1 2"/>
<path d="M3 10h14v5a6 6 0 0 1 -6 6h-2a6 6 0 0 1 -6 -6v-5"/>
<path d="M16.746 16.726a3 3 0 1 0 .252 -5.555"/>
</svg>

After

Width:  |  Height:  |  Size: 630 B

View File

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

View File

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

View File

@@ -23,7 +23,7 @@
/> />
</div> </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 <button
type="submit" type="submit"
@@ -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'

View File

@@ -5,6 +5,9 @@ import UnoCSS from 'unocss/vite'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
export default defineConfig({ export default defineConfig({
server: {
host: '127.0.0.1',
},
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),
@@ -15,7 +18,7 @@ export default defineConfig({
vue(), vue(),
VitePWA({ VitePWA({
registerType: 'autoUpdate', registerType: 'autoUpdate',
includeAssets: ['icons/*.png', 'icons/*.svg'], includeAssets: ['icons/*.svg'],
manifest: { manifest: {
name: 'Coffee Map', name: 'Coffee Map',
short_name: 'Coffee', short_name: 'Coffee',
@@ -27,20 +30,10 @@ export default defineConfig({
start_url: '/', start_url: '/',
icons: [ icons: [
{ {
src: 'icons/icon-192.png', src: 'icons/icon.svg',
sizes: '192x192', sizes: 'any',
type: 'image/png', type: 'image/svg+xml',
}, purpose: 'any maskable',
{
src: 'icons/icon-512.png',
sizes: '512x512',
type: 'image/png',
},
{
src: 'icons/icon-512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
}, },
], ],
}, },