Skip to content

Commit 74c8dc4

Browse files
feat(root): move email auth flow into api
Add API-owned email registration and login with bcrypt hashing, issue backend JWE tokens directly, and update the web app to use backend auth state for email sessions while keeping social auth bridging in place. Co-Authored-By: First Fluke <our.first.fluke@gmail.com>
1 parent 5c4e157 commit 74c8dc4

36 files changed

Lines changed: 1298 additions & 202 deletions

apps/api/alembic/env.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
from alembic import context
99
from src.lib.config import settings
1010
from src.lib.database import Base
11-
12-
# Import all models here for autogenerate
13-
# from src.users.models import User
11+
from src.users import model as users_model # noqa: F401
1412

1513
config = context.config
1614

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""add password hash column to users
2+
3+
Revision ID: 20260405_000001
4+
Revises:
5+
Create Date: 2026-04-05 01:00:01
6+
"""
7+
8+
from collections.abc import Sequence
9+
10+
import sqlalchemy as sa
11+
12+
from alembic import op
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = "20260405_000001"
16+
down_revision: str | None = None
17+
branch_labels: Sequence[str] | None = None
18+
depends_on: Sequence[str] | None = None
19+
20+
21+
def upgrade() -> None:
22+
op.add_column("users", sa.Column("password_hash", sa.String(length=255), nullable=True))
23+
24+
25+
def downgrade() -> None:
26+
op.drop_column("users", "password_hash")

apps/api/openapi.json

Lines changed: 237 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,111 @@
7070
}
7171
}
7272
},
73+
"/api/auth/register": {
74+
"post": {
75+
"tags": [
76+
"authentication"
77+
],
78+
"summary": "Register",
79+
"description": "Register with email/password and issue backend tokens.",
80+
"operationId": "register_api_auth_register_post",
81+
"requestBody": {
82+
"content": {
83+
"application/json": {
84+
"schema": {
85+
"$ref": "#/components/schemas/RegisterRequest"
86+
}
87+
}
88+
},
89+
"required": true
90+
},
91+
"responses": {
92+
"201": {
93+
"description": "Successful Response",
94+
"content": {
95+
"application/json": {
96+
"schema": {
97+
"$ref": "#/components/schemas/TokenResponse"
98+
}
99+
}
100+
}
101+
},
102+
"422": {
103+
"description": "Validation Error",
104+
"content": {
105+
"application/json": {
106+
"schema": {
107+
"$ref": "#/components/schemas/HTTPValidationError"
108+
}
109+
}
110+
}
111+
}
112+
}
113+
}
114+
},
73115
"/api/auth/login": {
74116
"post": {
75117
"tags": [
76118
"authentication"
77119
],
78120
"summary": "Login",
79-
"description": "OAuth login endpoint.\n\nVerify OAuth token, create/update user, and issue JWE tokens.",
121+
"description": "Login with OAuth or email/password and issue backend tokens.\n\nVerify OAuth token, create/update user, and issue JWE tokens.",
80122
"operationId": "login_api_auth_login_post",
81123
"requestBody": {
82124
"content": {
83125
"application/json": {
84126
"schema": {
85-
"$ref": "#/components/schemas/OAuthLoginRequest"
127+
"anyOf": [
128+
{
129+
"$ref": "#/components/schemas/OAuthLoginRequest"
130+
},
131+
{
132+
"$ref": "#/components/schemas/EmailLoginRequest"
133+
}
134+
],
135+
"title": "Request"
136+
}
137+
}
138+
},
139+
"required": true
140+
},
141+
"responses": {
142+
"200": {
143+
"description": "Successful Response",
144+
"content": {
145+
"application/json": {
146+
"schema": {
147+
"$ref": "#/components/schemas/TokenResponse"
148+
}
149+
}
150+
}
151+
},
152+
"422": {
153+
"description": "Validation Error",
154+
"content": {
155+
"application/json": {
156+
"schema": {
157+
"$ref": "#/components/schemas/HTTPValidationError"
158+
}
159+
}
160+
}
161+
}
162+
}
163+
}
164+
},
165+
"/api/auth/session-exchange": {
166+
"post": {
167+
"tags": [
168+
"authentication"
169+
],
170+
"summary": "Session Exchange",
171+
"description": "Exchange better-auth session token for backend JWE tokens.\n\nUsed by email/password auth users who have no OAuth provider token.\nVerifies session with better-auth server, then issues backend tokens.",
172+
"operationId": "session_exchange_api_auth_session_exchange_post",
173+
"requestBody": {
174+
"content": {
175+
"application/json": {
176+
"schema": {
177+
"$ref": "#/components/schemas/SessionExchangeRequest"
86178
}
87179
}
88180
},
@@ -168,10 +260,51 @@
168260
}
169261
}
170262
}
263+
},
264+
"/api/auth/me": {
265+
"get": {
266+
"tags": [
267+
"authentication"
268+
],
269+
"summary": "Get Me",
270+
"description": "Return the current authenticated user.",
271+
"operationId": "get_me_api_auth_me_get",
272+
"responses": {
273+
"200": {
274+
"description": "Successful Response",
275+
"content": {
276+
"application/json": {
277+
"schema": {
278+
"$ref": "#/components/schemas/UserResponse"
279+
}
280+
}
281+
}
282+
}
283+
}
284+
}
171285
}
172286
},
173287
"components": {
174288
"schemas": {
289+
"EmailLoginRequest": {
290+
"properties": {
291+
"email": {
292+
"type": "string",
293+
"title": "Email"
294+
},
295+
"password": {
296+
"type": "string",
297+
"title": "Password"
298+
}
299+
},
300+
"type": "object",
301+
"required": [
302+
"email",
303+
"password"
304+
],
305+
"title": "EmailLoginRequest",
306+
"description": "Email/password login request."
307+
},
175308
"HTTPValidationError": {
176309
"properties": {
177310
"detail": {
@@ -271,6 +404,36 @@
271404
"title": "RefreshTokenRequest",
272405
"description": "Refresh token request."
273406
},
407+
"RegisterRequest": {
408+
"properties": {
409+
"email": {
410+
"type": "string",
411+
"title": "Email"
412+
},
413+
"password": {
414+
"type": "string",
415+
"title": "Password"
416+
},
417+
"name": {
418+
"anyOf": [
419+
{
420+
"type": "string"
421+
},
422+
{
423+
"type": "null"
424+
}
425+
],
426+
"title": "Name"
427+
}
428+
},
429+
"type": "object",
430+
"required": [
431+
"email",
432+
"password"
433+
],
434+
"title": "RegisterRequest",
435+
"description": "Email/password registration request."
436+
},
274437
"ServiceStatus": {
275438
"properties": {
276439
"status": {
@@ -311,6 +474,20 @@
311474
"title": "ServiceStatus",
312475
"description": "Individual service health status."
313476
},
477+
"SessionExchangeRequest": {
478+
"properties": {
479+
"session_token": {
480+
"type": "string",
481+
"title": "Session Token"
482+
}
483+
},
484+
"type": "object",
485+
"required": [
486+
"session_token"
487+
],
488+
"title": "SessionExchangeRequest",
489+
"description": "Exchange better-auth session token for backend JWE tokens."
490+
},
314491
"TokenResponse": {
315492
"properties": {
316493
"access_token": {
@@ -335,6 +512,64 @@
335512
"title": "TokenResponse",
336513
"description": "Token response."
337514
},
515+
"UserResponse": {
516+
"properties": {
517+
"id": {
518+
"type": "string",
519+
"title": "Id"
520+
},
521+
"email": {
522+
"type": "string",
523+
"title": "Email"
524+
},
525+
"name": {
526+
"anyOf": [
527+
{
528+
"type": "string"
529+
},
530+
{
531+
"type": "null"
532+
}
533+
],
534+
"title": "Name"
535+
},
536+
"image": {
537+
"anyOf": [
538+
{
539+
"type": "string"
540+
},
541+
{
542+
"type": "null"
543+
}
544+
],
545+
"title": "Image"
546+
},
547+
"email_verified": {
548+
"type": "boolean",
549+
"title": "Email Verified",
550+
"default": false
551+
},
552+
"created_at": {
553+
"type": "string",
554+
"format": "date-time",
555+
"title": "Created At"
556+
},
557+
"updated_at": {
558+
"type": "string",
559+
"format": "date-time",
560+
"title": "Updated At"
561+
}
562+
},
563+
"type": "object",
564+
"required": [
565+
"id",
566+
"email",
567+
"created_at",
568+
"updated_at"
569+
],
570+
"title": "UserResponse",
571+
"description": "User response model."
572+
},
338573
"ValidationError": {
339574
"properties": {
340575
"loc": {

apps/api/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dependencies = [
2424
"opentelemetry-exporter-otlp>=1.28.0",
2525
"jwcrypto>=1.5.6",
2626
"psycopg2-binary>=2.9.9",
27+
"bcrypt>=5.0.0",
2728
]
2829

2930
[dependency-groups]
@@ -35,7 +36,7 @@ dev = [
3536
"pytest-asyncio>=0.26.0",
3637
"pytest-cov>=6.1.1",
3738
"factory-boy>=3.3.3",
38-
39+
"aiosqlite>=0.22.1",
3940
]
4041

4142
[tool.uv]

0 commit comments

Comments
 (0)