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:
@@ -14,5 +14,5 @@ COPY . .
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 3000
|
||||
|
||||
USER bun
|
||||
CMD ["bun", "run", "src/index.ts"]
|
||||
RUN chmod +x entrypoint.sh
|
||||
CMD ["./entrypoint.sh"]
|
||||
|
||||
10
entrypoint.sh
Normal file
10
entrypoint.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Run seed (creates test user if not exists)
|
||||
echo "Running database seed..."
|
||||
bun run db:seed || echo "Seed skipped or failed (may already exist)"
|
||||
|
||||
# Start the app
|
||||
echo "Starting API..."
|
||||
exec bun run src/index.ts
|
||||
@@ -12,7 +12,8 @@
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio"
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"db:seed": "bun run src/db/seed.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
|
||||
59
src/db/seed.ts
Normal file
59
src/db/seed.ts
Normal 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);
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user