Phase 1 scaffold: auth for all apps
- Backend: Fastify with two listeners (public + internal), routes, services, DB migration + seed - client_app: Flutter with BLoC, all auth screens (welcome, display name, register, OTP, force-register) - mitra_app: Flutter with BLoC, OTP-only login - control_center: React + Vite, email/password login, mitra/user management, anonymity settings - Docs: phase1 plan, API contract, client app mockup - CLAUDE.md and shared memory for all subprojects Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
58
backend/src/services/cc-user.service.js
Normal file
58
backend/src/services/cc-user.service.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { getDb } from '../db/client.js'
|
||||
|
||||
const sql = getDb()
|
||||
|
||||
export const getCcUserByFirebaseUid = async (firebase_uid) => {
|
||||
const [user] = await sql`
|
||||
SELECT
|
||||
u.id, u.email, u.display_name, u.created_at,
|
||||
r.id as role_id, r.name as role_name, r.permissions
|
||||
FROM control_center_users u
|
||||
JOIN roles r ON r.id = u.role_id
|
||||
WHERE u.firebase_uid = ${firebase_uid}
|
||||
`
|
||||
if (!user) return null
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
display_name: user.display_name,
|
||||
created_at: user.created_at,
|
||||
role: { id: user.role_id, name: user.role_name, permissions: user.permissions },
|
||||
}
|
||||
}
|
||||
|
||||
export const createCcUser = async ({ firebase_uid, email, display_name, role_id }) => {
|
||||
const [user] = await sql`
|
||||
INSERT INTO control_center_users (firebase_uid, email, display_name, role_id)
|
||||
VALUES (${firebase_uid}, ${email}, ${display_name}, ${role_id})
|
||||
RETURNING id, email, display_name, role_id, created_at
|
||||
`
|
||||
const [role] = await sql`SELECT id, name FROM roles WHERE id = ${role_id}`
|
||||
return { ...user, role }
|
||||
}
|
||||
|
||||
export const listCcUsers = async ({ page = 1, limit = 20 }) => {
|
||||
const offset = (page - 1) * limit
|
||||
const items = await sql`
|
||||
SELECT
|
||||
u.id, u.email, u.display_name, u.created_at,
|
||||
r.id as role_id, r.name as role_name
|
||||
FROM control_center_users u
|
||||
JOIN roles r ON r.id = u.role_id
|
||||
ORDER BY u.created_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`
|
||||
const [{ count }] = await sql`SELECT COUNT(*) FROM control_center_users`
|
||||
return {
|
||||
items: items.map((u) => ({
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
display_name: u.display_name,
|
||||
created_at: u.created_at,
|
||||
role: { id: u.role_id, name: u.role_name },
|
||||
})),
|
||||
total: Number(count),
|
||||
page,
|
||||
limit,
|
||||
}
|
||||
}
|
||||
17
backend/src/services/config.service.js
Normal file
17
backend/src/services/config.service.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { getDb } from '../db/client.js'
|
||||
|
||||
const sql = getDb()
|
||||
|
||||
export const getAnonymityConfig = async () => {
|
||||
const [row] = await sql`SELECT value FROM app_config WHERE key = 'anonymity'`
|
||||
return { anonymity_enabled: row?.value?.enabled ?? true }
|
||||
}
|
||||
|
||||
export const setAnonymityConfig = async (enabled) => {
|
||||
await sql`
|
||||
INSERT INTO app_config (key, value, updated_at)
|
||||
VALUES ('anonymity', ${sql.json({ enabled })}, NOW())
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()
|
||||
`
|
||||
return { anonymity_enabled: enabled }
|
||||
}
|
||||
43
backend/src/services/customer.service.js
Normal file
43
backend/src/services/customer.service.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { getDb } from '../db/client.js'
|
||||
|
||||
const sql = getDb()
|
||||
|
||||
export const createAnonymousCustomer = async ({ display_name }) => {
|
||||
const [customer] = await sql`
|
||||
INSERT INTO customers (display_name, is_anonymous)
|
||||
VALUES (${display_name}, true)
|
||||
RETURNING id, display_name, is_anonymous, created_at
|
||||
`
|
||||
return customer
|
||||
}
|
||||
|
||||
export const linkCustomerAccount = async ({ customer_id, firebase_uid }) => {
|
||||
const [existing] = await sql`
|
||||
SELECT id, firebase_uid FROM customers WHERE id = ${customer_id}
|
||||
`
|
||||
if (!existing) throw Object.assign(new Error('Customer not found'), { code: 'NOT_FOUND', statusCode: 404 })
|
||||
if (existing.firebase_uid) throw Object.assign(new Error('Account already linked'), { code: 'ALREADY_REGISTERED', statusCode: 409 })
|
||||
|
||||
// Also fetch phone from firebase_uid if exists in another customer record for uniqueness
|
||||
const [firebaseLinked] = await sql`
|
||||
SELECT id FROM customers WHERE firebase_uid = ${firebase_uid}
|
||||
`
|
||||
if (firebaseLinked) throw Object.assign(new Error('Account already linked'), { code: 'ALREADY_REGISTERED', statusCode: 409 })
|
||||
|
||||
const [updated] = await sql`
|
||||
UPDATE customers
|
||||
SET firebase_uid = ${firebase_uid}, is_anonymous = false
|
||||
WHERE id = ${customer_id}
|
||||
RETURNING id, display_name, is_anonymous, phone, created_at
|
||||
`
|
||||
return updated
|
||||
}
|
||||
|
||||
export const getCustomerByFirebaseUid = async (firebase_uid) => {
|
||||
const [customer] = await sql`
|
||||
SELECT id, display_name, is_anonymous, phone, created_at
|
||||
FROM customers
|
||||
WHERE firebase_uid = ${firebase_uid}
|
||||
`
|
||||
return customer
|
||||
}
|
||||
63
backend/src/services/mitra.service.js
Normal file
63
backend/src/services/mitra.service.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { getDb } from '../db/client.js'
|
||||
|
||||
const sql = getDb()
|
||||
|
||||
export const getMitraByFirebaseUid = async (firebase_uid) => {
|
||||
const [mitra] = await sql`
|
||||
SELECT id, display_name, phone, is_active, created_at
|
||||
FROM mitras
|
||||
WHERE firebase_uid = ${firebase_uid}
|
||||
`
|
||||
return mitra
|
||||
}
|
||||
|
||||
export const getMitraByPhone = async (phone) => {
|
||||
const [mitra] = await sql`
|
||||
SELECT id, display_name, phone, is_active, firebase_uid, created_at
|
||||
FROM mitras
|
||||
WHERE phone = ${phone}
|
||||
`
|
||||
return mitra
|
||||
}
|
||||
|
||||
export const setMitraFirebaseUid = async (id, firebase_uid) => {
|
||||
await sql`
|
||||
UPDATE mitras SET firebase_uid = ${firebase_uid} WHERE id = ${id}
|
||||
`
|
||||
}
|
||||
|
||||
export const createMitra = async ({ phone, display_name }) => {
|
||||
const [mitra] = await sql`
|
||||
INSERT INTO mitras (phone, display_name, is_active)
|
||||
VALUES (${phone}, ${display_name}, false)
|
||||
RETURNING id, phone, display_name, is_active, created_at
|
||||
`
|
||||
return mitra
|
||||
}
|
||||
|
||||
export const updateMitraStatus = async (id, is_active) => {
|
||||
const [mitra] = await sql`
|
||||
UPDATE mitras SET is_active = ${is_active}
|
||||
WHERE id = ${id}
|
||||
RETURNING id, is_active
|
||||
`
|
||||
if (!mitra) throw Object.assign(new Error('Mitra not found'), { code: 'NOT_FOUND', statusCode: 404 })
|
||||
return mitra
|
||||
}
|
||||
|
||||
export const listMitras = async ({ page = 1, limit = 20, is_active }) => {
|
||||
const offset = (page - 1) * limit
|
||||
const conditions = is_active !== undefined
|
||||
? sql`WHERE is_active = ${is_active}`
|
||||
: sql``
|
||||
|
||||
const items = await sql`
|
||||
SELECT id, phone, display_name, is_active, created_at
|
||||
FROM mitras
|
||||
${conditions}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`
|
||||
const [{ count }] = await sql`SELECT COUNT(*) FROM mitras ${conditions}`
|
||||
return { items, total: Number(count), page, limit }
|
||||
}
|
||||
7
backend/src/services/roles.service.js
Normal file
7
backend/src/services/roles.service.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { getDb } from '../db/client.js'
|
||||
|
||||
const sql = getDb()
|
||||
|
||||
export const listRoles = async () => {
|
||||
return sql`SELECT id, name, permissions, created_at FROM roles ORDER BY name`
|
||||
}
|
||||
Reference in New Issue
Block a user