diff --git a/openspec/changes/user-accounts/tasks.md b/openspec/changes/user-accounts/tasks.md index 53a4b27..75d3617 100644 --- a/openspec/changes/user-accounts/tasks.md +++ b/openspec/changes/user-accounts/tasks.md @@ -21,7 +21,7 @@ ## 4. Registration API -- [ ] 4.1 `[sonnet]` Create `POST /api/auth/register` endpoint: validate input (email required, password 8+ chars), check email uniqueness, hash password with bcryptjs, insert user, return 201 +- [x] 4.1 `[sonnet]` Create `POST /api/auth/register` endpoint: validate input (email required, password 8+ chars), check email uniqueness, hash password with bcryptjs, insert user, return 201 - [ ] 4.2 `[sonnet]` Add default data seeding function: on new user creation, insert default annotation_types (break_up, break_down, line) and default span_label_types for the new user ## 5. Auth Middleware & Helpers diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..aa22ed5 --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from 'next/server'; +import bcryptjs from 'bcryptjs'; +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { users } from '@/lib/db/schema'; + +export async function POST(request: NextRequest) { + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + const { email, password, name } = body as Record; + + // Validate email + if (!email || typeof email !== 'string' || email.trim() === '') { + return NextResponse.json({ error: 'Email is required' }, { status: 400 }); + } + + // Validate password + if (!password || typeof password !== 'string' || password.length < 8) { + return NextResponse.json( + { error: 'Password must be at least 8 characters' }, + { status: 400 } + ); + } + + const normalizedEmail = email.trim().toLowerCase(); + + // Check email uniqueness + const [existingUser] = await db + .select({ id: users.id }) + .from(users) + .where(eq(users.email, normalizedEmail)) + .limit(1); + + if (existingUser) { + return NextResponse.json( + { error: 'An account with this email already exists' }, + { status: 409 } + ); + } + + // Hash password + const password_hash = await bcryptjs.hash(password, 12); + + // Insert user + const [newUser] = await db + .insert(users) + .values({ + email: normalizedEmail, + password_hash, + name: typeof name === 'string' && name.trim() !== '' ? name.trim() : null, + provider: 'credentials', + }) + .returning({ id: users.id, email: users.email, name: users.name }); + + return NextResponse.json( + { id: newUser.id, email: newUser.email, name: newUser.name }, + { status: 201 } + ); +}