feat: bearer auth, auto-sync events, test seed

- Add bearer plugin to BetterAuth for mobile auth
- Auto-sync birthday/anniversary events on client create/update
- Add /api/events/sync-all endpoint for bulk sync
- Add test user seed (test@test.com / test)
- Expose set-auth-token header in CORS
This commit is contained in:
2026-01-27 22:12:33 +00:00
parent f9643235be
commit 7bd506463e
8 changed files with 206 additions and 4 deletions

59
src/db/seed.ts Normal file
View File

@@ -0,0 +1,59 @@
import { db } from './index';
import { users, accounts } from './schema';
import { eq } from 'drizzle-orm';
// Hash password using the same method as BetterAuth (bcrypt via Bun)
async function hashPassword(password: string): Promise<string> {
return await Bun.password.hash(password, {
algorithm: 'bcrypt',
cost: 10,
});
}
async function seed() {
const testEmail = 'test@test.com';
// Check if test user already exists
const [existing] = await db.select()
.from(users)
.where(eq(users.email, testEmail))
.limit(1);
if (existing) {
console.log('✓ Test user already exists');
return;
}
// Create test user
const userId = crypto.randomUUID();
const hashedPassword = await hashPassword('test');
await db.insert(users).values({
id: userId,
email: testEmail,
name: 'Test User',
emailVerified: true,
createdAt: new Date(),
updatedAt: new Date(),
});
// Create credential account (for email/password login)
await db.insert(accounts).values({
id: crypto.randomUUID(),
userId: userId,
accountId: userId,
providerId: 'credential',
password: hashedPassword,
createdAt: new Date(),
updatedAt: new Date(),
});
console.log('✓ Created test user: test@test.com / test');
}
seed()
.then(() => process.exit(0))
.catch((err) => {
console.error('Seed failed:', err);
process.exit(1);
});

View File

@@ -11,6 +11,7 @@ const app = new Elysia()
.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
exposeHeaders: ['set-auth-token'], // Expose bearer token header for mobile apps
}))
// Health check

View File

@@ -1,4 +1,5 @@
import { betterAuth } from 'better-auth';
import { bearer } from 'better-auth/plugins';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { db } from '../db';
import * as schema from '../db/schema';
@@ -13,6 +14,9 @@ export const auth = betterAuth({
verification: schema.verifications,
},
}),
plugins: [
bearer(), // Enable bearer token auth for mobile apps
],
emailAndPassword: {
enabled: true,
requireEmailVerification: false, // Enable later for production

View File

@@ -1,9 +1,61 @@
import { Elysia, t } from 'elysia';
import { db } from '../db';
import { clients } from '../db/schema';
import { clients, events } from '../db/schema';
import { eq, and, ilike, or, sql } from 'drizzle-orm';
import type { User } from '../lib/auth';
// Helper to sync birthday/anniversary events for a client
async function syncClientEvents(userId: string, client: { id: string; firstName: string; birthday: Date | null; anniversary: Date | null }) {
// Sync birthday event
if (client.birthday) {
const [existing] = await db.select()
.from(events)
.where(and(eq(events.clientId, client.id), eq(events.type, 'birthday')))
.limit(1);
if (!existing) {
await db.insert(events).values({
userId,
clientId: client.id,
type: 'birthday',
title: `${client.firstName}'s Birthday`,
date: client.birthday,
recurring: true,
reminderDays: 7,
});
} else {
// Update date if changed
await db.update(events)
.set({ date: client.birthday, title: `${client.firstName}'s Birthday` })
.where(eq(events.id, existing.id));
}
}
// Sync anniversary event
if (client.anniversary) {
const [existing] = await db.select()
.from(events)
.where(and(eq(events.clientId, client.id), eq(events.type, 'anniversary')))
.limit(1);
if (!existing) {
await db.insert(events).values({
userId,
clientId: client.id,
type: 'anniversary',
title: `${client.firstName}'s Anniversary`,
date: client.anniversary,
recurring: true,
reminderDays: 7,
});
} else {
await db.update(events)
.set({ date: client.anniversary, title: `${client.firstName}'s Anniversary` })
.where(eq(events.id, existing.id));
}
}
}
// Validation schemas
const clientSchema = t.Object({
firstName: t.String({ minLength: 1 }),
@@ -108,6 +160,9 @@ export const clientRoutes = new Elysia({ prefix: '/clients' })
})
.returning();
// Auto-sync birthday/anniversary events
await syncClientEvents(user.id, client);
return client;
}, {
body: clientSchema,
@@ -147,6 +202,11 @@ export const clientRoutes = new Elysia({ prefix: '/clients' })
throw new Error('Client not found');
}
// Auto-sync birthday/anniversary events if dates changed
if (body.birthday !== undefined || body.anniversary !== undefined || body.firstName !== undefined) {
await syncClientEvents(user.id, client);
}
return client;
}, {
params: t.Object({

View File

@@ -206,6 +206,73 @@ export const eventRoutes = new Elysia({ prefix: '/events' })
}),
})
// Sync ALL events from all clients' birthdays/anniversaries
.post('/sync-all', async ({ user }: { user: User }) => {
// Get all clients for this user
const userClients = await db.select()
.from(clients)
.where(eq(clients.userId, user.id));
let created = 0;
let updated = 0;
for (const client of userClients) {
// Sync birthday
if (client.birthday) {
const [existing] = await db.select()
.from(events)
.where(and(eq(events.clientId, client.id), eq(events.type, 'birthday')))
.limit(1);
if (!existing) {
await db.insert(events).values({
userId: user.id,
clientId: client.id,
type: 'birthday',
title: `${client.firstName}'s Birthday`,
date: client.birthday,
recurring: true,
reminderDays: 7,
});
created++;
} else {
await db.update(events)
.set({ date: client.birthday, title: `${client.firstName}'s Birthday` })
.where(eq(events.id, existing.id));
updated++;
}
}
// Sync anniversary
if (client.anniversary) {
const [existing] = await db.select()
.from(events)
.where(and(eq(events.clientId, client.id), eq(events.type, 'anniversary')))
.limit(1);
if (!existing) {
await db.insert(events).values({
userId: user.id,
clientId: client.id,
type: 'anniversary',
title: `${client.firstName}'s Anniversary`,
date: client.anniversary,
recurring: true,
reminderDays: 7,
});
created++;
} else {
await db.update(events)
.set({ date: client.anniversary, title: `${client.firstName}'s Anniversary` })
.where(eq(events.id, existing.id));
updated++;
}
}
}
return { success: true, created, updated, clientsProcessed: userClients.length };
})
// Sync events from client birthdays/anniversaries
.post('/sync/:clientId', async ({ params, user }: { params: { clientId: string }; user: User }) => {
// Get client