Authentication¶
The customer portal API (dk_customer_portal_api) uses a bearer token
issued at login, not Odoo’s cookie-based session auth. Every route (except
/api/login, /api/register, and the OAuth/social-login start/callback
routes) is registered as auth="none" in Odoo and checks the token itself.
Getting a token¶
POST /api/login
Content-Type: application/json
{
"db": "your_database_name",
"login": "user@example.com",
"password": "your-password"
}
On success:
{
"success": true,
"token": "a-64-byte-urlsafe-token",
"user": {
"id": 14,
"name": "Jane Doe",
"login": "user@example.com",
"is_admin": false
}
}
On failure, the response is {"success": false, "message": "..."} with
400 (missing db/login/password), 401 (invalid credentials), or
404 (user record missing after auth).
Internally, /api/login calls request.session.authenticate(db, {...})
against Odoo, then generates the token with secrets.token_urlsafe(64) and
stores it on a dk.mobile.token record linked to the authenticated
res.users. The same token model backs both the React customer portal and
the mobile app.
Using the token¶
Send it as a standard Authorization: Bearer header on every subsequent
request:
GET /api/instances
Authorization: Bearer a-64-byte-urlsafe-token
Each controller resolves the caller with the same pattern:
def _get_authenticated_user(self):
auth_header = request.httprequest.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return False
token = auth_header.replace("Bearer ", "").strip()
token_record = request.env["dk.mobile.token"].sudo().search(
[("token", "=", token), ("active", "=", True)], limit=1
)
return token_record.user_id if token_record else False
If the header is missing, malformed, or the token doesn’t match an active
dk.mobile.token record, the endpoint returns:
{ "success": false, "message": "Unauthorized" }
with HTTP status 401.
Tokens don’t currently expire on a fixed schedule — they’re deactivated by
flipping active to false on the dk.mobile.token record (e.g. on
logout or manual revocation). Each token also tracks last_used.
CORS¶
Every controller defines the same _cors_headers() helper and only reflects
Access-Control-Allow-Origin back for an allow-listed set of origins:
http://localhost:3000,http://localhost:3001,http://localhost:8069(local dev)https://dishonkadoh.com,https://www.dishonkadoh.comhttps://app.dishonkadoh.com,https://erp.dishonkadoh.comhttps://farmproduction.dishonkadoh.com,https://demo.dishonkadoh.comhttp://odoo.minikube:31906(local Kubernetes)
Access-Control-Allow-Credentials: true and Vary: Origin are always set.
Every route accepts OPTIONS for the CORS preflight and returns an empty
200 response before touching authentication logic.
Because every route is auth="none", CORS and the bearer check are the only
things standing between an anonymous request and the endpoint. If you add a
new controller, copy _cors_headers()/_json_response()/
_get_authenticated_user() from an existing one (e.g. main.py) rather than
reimplementing them — that’s the pattern used across every controller.
Social / OAuth login¶
google_oauth.pyandsocial_auth.pyimplement start/callback routes for Google, GitHub, GitLab, and Bitbucket (/api/auth/<provider>and/api/auth/<provider>/callback), plus/api/auth/social/exchangeand/api/auth/google/exchangeto trade a provider code for adk.mobile.tokenusing the same mechanism as/api/login. These are separate from theoauth.pyhandoff routes (/api/oauth/handoff,/api/oauth/handoff/consume), which support handing an authenticated session off betweendishonkadoh.comproperties (e.g. intoerp.dishonkadoh.com).