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.com

  • https://app.dishonkadoh.com, https://erp.dishonkadoh.com

  • https://farmproduction.dishonkadoh.com, https://demo.dishonkadoh.com

  • http://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.py and social_auth.py implement start/callback routes for Google, GitHub, GitLab, and Bitbucket (/api/auth/<provider> and /api/auth/<provider>/callback), plus /api/auth/social/exchange and /api/auth/google/exchange to trade a provider code for a dk.mobile.token using the same mechanism as /api/login. These are separate from the oauth.py handoff routes (/api/oauth/handoff, /api/oauth/handoff/consume), which support handing an authenticated session off between dishonkadoh.com properties (e.g. into erp.dishonkadoh.com).