MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-19

1.1.3 User + OAuthAccount schema + password hashing

Done
Description

Estimate: 15m · Depends on: 1.0.2, 1.1.2

Add the user table and the oauth_account account-linking table to the Prisma schema, wire up the password-hashing helper, and create a clean repository layer so the rest of the codebase never touches password hashing or OAuth account state directly. Better-Auth (configured in 1.1.2) consumes this schema as its persistence layer.

Why two tables and not one: a single user can have both an email/password credential and linked OAuth accounts (via auto-linking on matching email). Storing OAuth account state as rows in a separate oauth_account table — keyed by (provider, providerAccountId) — is Better-Auth's recommended shape and keeps the user table free of provider-specific fields. The User's passwordHash is nullable: Google-only signups never set one.

Why hashing helper is its own concern: Password handling is a security footgun. By centralizing it in one file (/lib/auth/passwords.ts) with both hash() and verify(), the rest of the codebase can never accidentally compare a plaintext password — there's only one way to do it.

What you'll do: Edit /prisma/schema.prisma to add the User and OAuthAccount models (using Better-Auth's expected field names so the Prisma adapter just works), generate and apply a migration, write /lib/auth/passwords.ts using argon2 (preferred) or bcrypt (fallback if argon2 isn't viable), and provide /lib/users/repo.ts with createUser, findUserByEmail, verifyPassword, findOrCreateOAuthUser (used by 1.1.4 for Google sign-in auto-link), and linkOAuthAccount. Wire the Better-Auth instance from 1.1.2 to the Prisma adapter pointing at this schema.

Acceptance criteria

  • User table: id (uuid), email (unique, case-insensitive), passwordHash (nullable — null for Google-only signups), emailVerifiedAt (nullable; set automatically for Google signups since Google has already verified), name, image (nullable; populated from Google profile when available), createdAt, updatedAt.
  • OAuthAccount table: id, userId (FK → User), provider (e.g. "google"), providerAccountId (the Google user ID), accessToken (nullable), refreshToken (nullable), expiresAt (nullable), createdAt, updatedAt. Composite unique on (provider, providerAccountId). Field names match Better-Auth's Prisma-adapter conventions so no custom mapping is required.
  • Migration created and applies cleanly on a fresh database.
  • /lib/auth/passwords.ts exposes hash(plain) → string and verify(plain, hash) → boolean. Uses argon2id with sensible parameters (memoryCost: 19MB, timeCost: 2, parallelism: 1) or bcrypt cost 12 if argon2 isn't viable.
  • /lib/users/repo.ts exposes createUser({ email, password, name? }), findUserByEmail(email), verifyPassword(email, plain), findOrCreateOAuthUser({ provider, providerAccountId, email, name?, image? }) (auto-link semantics: if an existing user matches on email, link the OAuth account to them; otherwise create a new user with passwordHash: null and link), linkOAuthAccount({ userId, provider, providerAccountId, ... }). Email lookups are case-insensitive.
  • Better-Auth's Prisma adapter is wired in /lib/auth/index.ts (from 1.1.2) to point at the new schema.
  • No raw bcrypt/argon2 imports anywhere else in the codebase (lint rule or just convention; spot-check).
  • Tests: hash-and-verify roundtrip; invalid password returns false; duplicate-email password creation fails with a typed error; findOrCreateOAuthUser with matching email links to existing user; findOrCreateOAuthUser with no matching email creates a new user with null passwordHash.

Context refs

  • README.md — Prisma conventions in this repo
  • /prisma/schema.prisma — existing schema (placeholder from 1.0.2)
  • OWASP password-storage cheat-sheet (URL, fetched at prompt-gen)
Status
Done
Type
Sub-task