Skip to content

Commit e8c95b1

Browse files
committed
Fix and complete integration guide
1 parent b7f5dc5 commit e8c95b1

17 files changed

Lines changed: 296 additions & 364 deletions

docs/mkdocs.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,6 @@ theme:
4242
icon:
4343
repo: fontawesome/brands/github
4444

45-
extra_css:
46-
- stylesheets/extra.css
47-
48-
extra_javascript:
49-
- javascripts/mermaid-zoom.js
50-
5145
plugins:
5246
- search
5347
- offline

docs/src/integration-guide.md

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Step by step guide to integrate `hotosm-auth` in your project.
1818
```
1919
Does your app have an existing auth system (legacy)?
2020
21-
├─ NO → Simple Integration (Portal, OAM)
21+
├─ NO → Simple Integration (Portal)
2222
│ You only need to validate Hanko JWT
2323
2424
└─ YES → Integration with Mapping (Drone-TM, fAIr)
@@ -29,7 +29,7 @@ Does your app have an existing auth system (legacy)?
2929

3030
## FastAPI: Simple Integration
3131

32-
For apps **without legacy auth** (e.g.: Portal, OAM).
32+
For apps **without legacy auth** (e.g.: Portal).
3333

3434
### Step 1: Dependency
3535

@@ -196,6 +196,14 @@ MIDDLEWARE = [
196196

197197
```python
198198
# views.py
199+
from hotosm_auth_django import login_required
200+
201+
# Decorator approach
202+
@login_required
203+
def my_view(request):
204+
return JsonResponse({"email": request.hotosm.user.email})
205+
206+
# Class-based view — check manually
199207
class ProtectedView(APIView):
200208
def get(self, request):
201209
if not request.hotosm.user:
@@ -207,9 +215,12 @@ class ProtectedView(APIView):
207215

208216
## Django: Integration with Mapping
209217

218+
### Steps 1-2: Same as Simple
219+
210220
### Step 3: Settings with dual-mode
211221

212222
```python
223+
# settings.py
213224
AUTH_PROVIDER = env("AUTH_PROVIDER", default="legacy")
214225

215226
if AUTH_PROVIDER == "hanko":
@@ -220,14 +231,59 @@ if AUTH_PROVIDER == "hanko":
220231
)
221232
```
222233

223-
### Step 4: Admin routes
234+
### Step 4: DRF authentication backend
235+
236+
The middleware populates `request.hotosm.user`. Create a DRF `BaseAuthentication` class that maps that Hanko user to your app user:
237+
238+
```python
239+
# authentication.py
240+
from rest_framework import authentication
241+
from hotosm_auth_django import get_mapped_user_id
242+
from myapp.models import AppUser
243+
244+
class HankoAuthentication(authentication.BaseAuthentication):
245+
def authenticate(self, request):
246+
hanko_user = getattr(request, 'hotosm', None) and request.hotosm.user
247+
if not hanko_user:
248+
return None, None
249+
250+
app_user_id = get_mapped_user_id(hanko_user, app_name="my-app")
251+
252+
if app_user_id:
253+
user = AppUser.objects.get(id=app_user_id)
254+
return user, None
255+
256+
# No mapping yet → send to onboarding
257+
request.needs_onboarding = True
258+
return None, None
259+
260+
261+
# Select auth class based on provider
262+
if settings.AUTH_PROVIDER == "hanko":
263+
MyAuthentication = HankoAuthentication
264+
else:
265+
MyAuthentication = LegacyAuthentication # your existing class
266+
```
267+
268+
Register it in settings:
269+
270+
```python
271+
# settings.py
272+
REST_FRAMEWORK = {
273+
"DEFAULT_AUTHENTICATION_CLASSES": [
274+
"myapp.authentication.MyAuthentication",
275+
]
276+
}
277+
```
278+
279+
### Step 5: Admin routes
224280

225281
```python
226282
# urls.py
227-
if getattr(settings, 'AUTH_PROVIDER', 'legacy') == 'hanko':
283+
if settings.AUTH_PROVIDER == "hanko":
228284
from hotosm_auth_django.admin_routes import create_admin_urlpatterns
229285
admin_patterns = create_admin_urlpatterns(
230-
app_name="my-app", user_model="myapp.User",
286+
app_name="my-app", user_model="myapp.AppUser",
231287
user_id_column="id", user_name_column="username", user_email_column="email",
232288
)
233289
urlpatterns += [path("api/admin/", include(admin_patterns))]
@@ -237,7 +293,7 @@ if getattr(settings, 'AUTH_PROVIDER', 'legacy') == 'hanko':
237293

238294
## Litestar: Simple Integration
239295

240-
For apps **without legacy auth** (e.g.: Field-TM).
296+
For apps **without legacy auth**.
241297

242298
### Step 1: Dependency
243299

@@ -256,9 +312,10 @@ from litestar import Litestar
256312
from hotosm_auth_litestar import setup_auth
257313

258314
# setup_auth() loads config from env, returns (deps, route_handlers)
315+
# route_handlers includes the OSM OAuth routes — spread alongside your own handlers
259316
deps, route_handlers = setup_auth()
260317

261-
app = Litestar(route_handlers=route_handlers, dependencies=deps)
318+
app = Litestar(route_handlers=[*route_handlers, me, ...], dependencies=deps)
262319
```
263320

264321
### Step 3: Protect routes
@@ -301,6 +358,7 @@ OSM_CLIENT_SECRET=your-client-secret
301358
# auth_deps.py
302359
from litestar import Request
303360
from hotosm_auth_litestar import get_current_user, get_mapped_user_id
361+
from app.hanko_helpers import lookup_user_by_email, create_app_user
304362

305363
async def login_required(request: Request):
306364
hanko_user = await get_current_user(request)
@@ -316,7 +374,29 @@ async def login_required(request: Request):
316374
return await get_user_by_id(db, user_id)
317375
```
318376

319-
### Step 4: Admin routes (optional)
377+
### Step 4: Helper functions
378+
379+
```python
380+
# hanko_helpers.py
381+
async def lookup_user_by_email(db, email: str) -> Optional[str]:
382+
"""Look up user by email. Returns user_id or None."""
383+
async with db.cursor() as cur:
384+
await cur.execute("SELECT id FROM users WHERE email = %s", [email])
385+
row = await cur.fetchone()
386+
return str(row[0]) if row else None
387+
388+
async def create_app_user(db, hanko_user: HankoUser) -> str:
389+
"""Create new user. Returns user_id."""
390+
async with db.cursor() as cur:
391+
await cur.execute(
392+
"INSERT INTO users (email, name) VALUES (%s, %s) RETURNING id",
393+
[hanko_user.email, hanko_user.username or hanko_user.email.split("@")[0]]
394+
)
395+
row = await cur.fetchone()
396+
return str(row[0])
397+
```
398+
399+
### Step 5: Admin routes (optional)
320400

321401
```python
322402
# main.py
@@ -357,7 +437,7 @@ VITE_HANKO_URL=https://login.hotosm.org
357437
|------|----------------|-----------------|---------------|----------------|-----------------|------------------|
358438
| Dependency |||||||
359439
| Init | init_auth | init_auth | middleware | middleware | setup_auth() | setup_auth() |
360-
| Protect routes | CurrentUser | Override login_required | request.hotosm | request.hotosm | AuthContext | Custom dep |
361-
| Helper functions | - || - | | - ||
362-
| Admin routes | - | Optional | - | Optional | - | Optional |
440+
| Protect routes | CurrentUser | Override login_required | request.hotosm | BaseAuthentication | AuthContext | Custom dep |
441+
| Helper functions | - || - | - | - ||
442+
| Admin routes | - | Step 5 | - | Step 5 | - | Step 5 |
363443
| AUTH_PROVIDER env | - || - || - ||

frontend/public/docs/404.html

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@
4949

5050

5151

52-
<link rel="stylesheet" href="/app/docs/stylesheets/extra.css">
53-
5452
<script>__md_scope=new URL("/app/docs/",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
5553

5654

@@ -911,8 +909,6 @@ <h1>404 - Not found</h1>
911909

912910
<script src="/app/docs/assets/javascripts/bundle.79ae519e.min.js"></script>
913911

914-
<script src="/app/docs/javascripts/mermaid-zoom.js"></script>
915-
916912

917913
</body>
918914
</html>

frontend/public/docs/admin.html

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@
5555

5656

5757

58-
<link rel="stylesheet" href="stylesheets/extra.css">
59-
6058
<script>__md_scope=new URL(".",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
6159

6260

@@ -1318,8 +1316,6 @@ <h2 id="proxy-endpoints-login-backend">Proxy Endpoints (Login Backend)<a class="
13181316

13191317
<script src="assets/javascripts/bundle.79ae519e.min.js"></script>
13201318

1321-
<script src="javascripts/mermaid-zoom.js"></script>
1322-
13231319

13241320
</body>
13251321
</html>

frontend/public/docs/index.html

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,6 @@
5353

5454

5555

56-
<link rel="stylesheet" href="stylesheets/extra.css">
57-
5856
<script>__md_scope=new URL(".",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
5957

6058

@@ -1266,8 +1264,6 @@ <h2 id="implementations">Implementations<a class="headerlink" href="#implementat
12661264

12671265
<script src="assets/javascripts/bundle.79ae519e.min.js"></script>
12681266

1269-
<script src="javascripts/mermaid-zoom.js"></script>
1270-
12711267

12721268
</body>
12731269
</html>

0 commit comments

Comments
 (0)