feat(auth): GitHub OAuth login + SMS one-time-code login
Some checks failed
Deploy to Production / deploy (push) Failing after 1m8s
Some checks failed
Deploy to Production / deploy (push) Failing after 1m8s
GitHub: /v1/auth/github + /callback — authorization-code flow, fetches the verified primary email via /user/emails, reuses upsertOAuthLogin. SMS: phone is now a first-class login identity. - schema: users.email nullable, users.phone added, new sms_codes table. - @bmm/auth: issueSmsCode / consumeSmsCode — 6-digit code, hashed at rest, 10-min TTL, per-phone rate limit, 5-attempt cap, get-or-create user by phone. - apps/api: /v1/auth/sms/request + /verify, Twilio REST send (no SDK), per-IP throttle. /v1/auth/providers now reports google/github/sms. - login UI: Google + GitHub buttons, Email|Phone toggle, two-step SMS (number -> 6-digit code with one-time-code autofill). SMS link was rejected in favour of an OTP code — carrier link-scanners consume magic-link tokens before the user taps them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -97,7 +97,10 @@ export const templates = pgTable(
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
// Nullable: a user identifies via email OR phone. Postgres treats NULLs as
|
||||
// distinct, so multiple phone-only users (email NULL) coexist fine.
|
||||
email: varchar('email', { length: 255 }).unique(),
|
||||
phone: varchar('phone', { length: 32 }).unique(),
|
||||
name: varchar('name', { length: 128 }),
|
||||
avatarUrl: text('avatar_url'),
|
||||
emailVerified: boolean('email_verified').default(false).notNull(),
|
||||
@@ -134,6 +137,23 @@ export const magicLinks = pgTable('magic_links', {
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Short-lived 6-digit SMS one-time codes for phone login.
|
||||
export const smsCodes = pgTable(
|
||||
'sms_codes',
|
||||
{
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
phone: varchar('phone', { length: 32 }).notNull(),
|
||||
codeHash: text('code_hash').notNull(),
|
||||
attempts: integer('attempts').default(0).notNull(),
|
||||
expiresAt: timestamp('expires_at').notNull(),
|
||||
consumedAt: timestamp('consumed_at'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
},
|
||||
(t) => ({
|
||||
phoneIdx: index('idx_sms_codes_phone').on(t.phone, t.createdAt),
|
||||
}),
|
||||
);
|
||||
|
||||
export const memberships = pgTable('memberships', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
orgId: uuid('org_id')
|
||||
|
||||
Reference in New Issue
Block a user