Phase 2 refinements: Firebase config, dev environment fixes, phase 3 requirement draft
- Integrated Firebase SDK in both Flutter apps (google-services, firebase_options) - Fixed auth flow, API client, and pairing/status blocs for dev environment - Added full Flutter project scaffolds (android, ios, web, etc.) - Added phase 3 chat engine requirement document - Added bugreport zip pattern to gitignore Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import Fastify from 'fastify'
|
||||
import cors from '@fastify/cors'
|
||||
import sensible from '@fastify/sensible'
|
||||
import { customerRoutes } from './routes/public/customer.routes.js'
|
||||
import { clientAuthRoutes } from './routes/public/client.auth.routes.js'
|
||||
@@ -12,6 +13,7 @@ import { errorHandler } from './plugins/error-handler.js'
|
||||
export const buildPublicApp = async () => {
|
||||
const app = Fastify({ logger: true })
|
||||
|
||||
await app.register(cors, { origin: true })
|
||||
await app.register(sensible)
|
||||
app.setErrorHandler(errorHandler)
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ export const authenticate = async (request, reply) => {
|
||||
const token = authHeader.slice(7)
|
||||
try {
|
||||
request.firebaseUser = await verifyFirebaseToken(token)
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.error('Auth failed:', err.code || err.message, '| token preview:', token.substring(0, 20) + '...')
|
||||
return reply.code(401).send({
|
||||
success: false,
|
||||
error: { code: 'UNAUTHORIZED', message: 'Invalid or expired token' },
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import admin from 'firebase-admin'
|
||||
import { readFileSync } from 'fs'
|
||||
import { resolve, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
let initialized = false
|
||||
|
||||
export const initFirebase = () => {
|
||||
if (initialized) return
|
||||
|
||||
const serviceAccountPath = process.env.FIREBASE_SERVICE_ACCOUNT_PATH
|
||||
|| resolve(__dirname, '../../firebase-service-account.json')
|
||||
|
||||
const serviceAccount = JSON.parse(readFileSync(serviceAccountPath, 'utf8'))
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert({
|
||||
projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
|
||||
}),
|
||||
credential: admin.credential.cert(serviceAccount),
|
||||
})
|
||||
initialized = true
|
||||
}
|
||||
|
||||
@@ -30,18 +30,24 @@ export const getValkeySub = () => {
|
||||
|
||||
export const publish = async (channel, data) => {
|
||||
const pubClient = getValkeyPub()
|
||||
await pubClient.publish(channel, JSON.stringify(data))
|
||||
const numReceivers = await pubClient.publish(channel, JSON.stringify(data))
|
||||
console.log(`[valkey] publish to ${channel} → ${numReceivers} receiver(s)`)
|
||||
}
|
||||
|
||||
export const subscribe = (channel, callback) => {
|
||||
const subClient = getValkeySub()
|
||||
subClient.subscribe(channel)
|
||||
subClient.on('message', (ch, message) => {
|
||||
console.log(`[valkey] subscribed to ${channel}`)
|
||||
const handler = (ch, message) => {
|
||||
if (ch === channel) {
|
||||
console.log(`[valkey] received on ${channel}`)
|
||||
callback(JSON.parse(message))
|
||||
}
|
||||
})
|
||||
}
|
||||
subClient.on('message', handler)
|
||||
return () => {
|
||||
subClient.unsubscribe(channel)
|
||||
subClient.removeListener('message', handler)
|
||||
console.log(`[valkey] unsubscribed from ${channel}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { authenticate } from '../../plugins/auth.js'
|
||||
import { createAnonymousCustomer, linkCustomerAccount } from '../../services/customer.service.js'
|
||||
|
||||
export const customerRoutes = async (app) => {
|
||||
app.post('/anonymous', async (request, reply) => {
|
||||
app.post('/anonymous', { preHandler: authenticate }, async (request, reply) => {
|
||||
const { display_name } = request.body ?? {}
|
||||
if (!display_name?.trim()) {
|
||||
return reply.code(422).send({
|
||||
@@ -10,7 +10,8 @@ export const customerRoutes = async (app) => {
|
||||
error: { code: 'DISPLAY_NAME_REQUIRED', message: 'Display name is required' },
|
||||
})
|
||||
}
|
||||
const customer = await createAnonymousCustomer({ display_name: display_name.trim() })
|
||||
const firebase_uid = request.firebaseUser.uid
|
||||
const customer = await createAnonymousCustomer({ display_name: display_name.trim(), firebase_uid })
|
||||
return reply.code(201).send({ success: true, data: customer })
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { authenticate } from '../../plugins/auth.js'
|
||||
import { getAnonymityConfig } from '../../services/config.service.js'
|
||||
|
||||
export const sharedConfigRoutes = async (app) => {
|
||||
app.get('/anonymity', { preHandler: authenticate }, async (request, reply) => {
|
||||
app.get('/anonymity', async (request, reply) => {
|
||||
const config = await getAnonymityConfig()
|
||||
return reply.send({ success: true, data: config })
|
||||
})
|
||||
|
||||
@@ -2,12 +2,14 @@ import 'dotenv/config'
|
||||
import { buildPublicApp } from './app.public.js'
|
||||
import { buildInternalApp } from './app.internal.js'
|
||||
import { autoOfflineStaleMitras } from './services/mitra-status.service.js'
|
||||
import { initFirebase } from './plugins/firebase.js'
|
||||
|
||||
const PUBLIC_PORT = process.env.PUBLIC_PORT || 3000
|
||||
const INTERNAL_PORT = process.env.INTERNAL_PORT || 3001
|
||||
const INTERNAL_HOST = process.env.INTERNAL_HOST || '127.0.0.1'
|
||||
|
||||
const start = async () => {
|
||||
initFirebase()
|
||||
const publicApp = await buildPublicApp()
|
||||
const internalApp = await buildInternalApp()
|
||||
|
||||
|
||||
@@ -2,10 +2,17 @@ import { getDb } from '../db/client.js'
|
||||
|
||||
const sql = getDb()
|
||||
|
||||
export const createAnonymousCustomer = async ({ display_name }) => {
|
||||
export const createAnonymousCustomer = async ({ display_name, firebase_uid }) => {
|
||||
// Return existing customer if already linked to this Firebase UID
|
||||
const [existing] = await sql`
|
||||
SELECT id, display_name, is_anonymous, created_at
|
||||
FROM customers WHERE firebase_uid = ${firebase_uid}
|
||||
`
|
||||
if (existing) return existing
|
||||
|
||||
const [customer] = await sql`
|
||||
INSERT INTO customers (display_name, is_anonymous)
|
||||
VALUES (${display_name}, true)
|
||||
INSERT INTO customers (display_name, is_anonymous, firebase_uid)
|
||||
VALUES (${display_name}, true, ${firebase_uid})
|
||||
RETURNING id, display_name, is_anonymous, created_at
|
||||
`
|
||||
return customer
|
||||
|
||||
Reference in New Issue
Block a user