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:
2026-04-07 19:16:34 +08:00
parent d668112edd
commit 844d7234e6
229 changed files with 10439 additions and 102 deletions

2
backend/.gitignore vendored
View File

@@ -1,3 +1,5 @@
node_modules/
.env
*.log
firebase-service-account.json
*-firebase-adminsdk-*.json

2820
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,14 +11,15 @@
"db:seed": "node src/db/seed.js"
},
"dependencies": {
"fastify": "^4.28.1",
"@fastify/cors": "^9.0.1",
"@fastify/sensible": "^5.6.0",
"dotenv": "^16.4.5",
"fastify": "^4.28.1",
"firebase-admin": "^12.2.0",
"ioredis": "^5.4.1",
"pg": "^8.12.0",
"postgres": "^3.4.4",
"zod": "^3.23.8",
"dotenv": "^16.4.5"
"zod": "^3.23.8"
},
"devDependencies": {
"@types/pg": "^8.11.6"

View File

@@ -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)

View File

@@ -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' },

View File

@@ -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
}

View File

@@ -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}`)
}
}

View File

@@ -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 })
})

View File

@@ -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 })
})

View File

@@ -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()

View File

@@ -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