Fix login redirect — access result.data.token/user
Login/register pages now correctly unwrap API response {data: {token, user}}.
Cleaned up leftover apiFetch code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
odoo_modules/task_team_connector/controllers/__init__.py
Normal file
2
odoo_modules/task_team_connector/controllers/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import webhook
|
||||
from . import oauth
|
||||
142
odoo_modules/task_team_connector/controllers/oauth.py
Normal file
142
odoo_modules/task_team_connector/controllers/oauth.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# Task Team Connector — OAuth2 Authorization Server bridge
|
||||
import json
|
||||
import logging
|
||||
import urllib.parse
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TaskTeamOAuthController(http.Controller):
|
||||
"""
|
||||
Implements a minimal OAuth2 Authorization Code flow so that
|
||||
Task Team can authenticate users via their Odoo credentials.
|
||||
|
||||
Flow:
|
||||
1. Task Team redirects browser to GET /task_team/oauth/authorize
|
||||
2. Odoo shows a consent page (user must be logged in)
|
||||
3. User approves → Odoo redirects to redirect_uri?code=XXX&state=YYY
|
||||
4. Task Team backend calls POST /task_team/oauth/token to exchange code
|
||||
5. Response includes Odoo user info + tt_user_id
|
||||
6. Task Team uses tt_user_id / email to issue its own JWT
|
||||
"""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Authorization endpoint
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@http.route(
|
||||
'/task_team/oauth/authorize',
|
||||
type='http',
|
||||
auth='user',
|
||||
methods=['GET', 'POST'],
|
||||
website=False,
|
||||
)
|
||||
def authorize(
|
||||
self,
|
||||
client_id=None,
|
||||
redirect_uri=None,
|
||||
response_type=None,
|
||||
state=None,
|
||||
**kwargs,
|
||||
):
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
expected_client_id = ICP.get_param('task_team_connector.oauth_client_id', '')
|
||||
|
||||
def _error(msg, status=400):
|
||||
return request.make_response(
|
||||
json.dumps({'error': msg}),
|
||||
headers=[('Content-Type', 'application/json')],
|
||||
status=status,
|
||||
)
|
||||
|
||||
if not client_id or client_id != expected_client_id:
|
||||
return _error('invalid_client')
|
||||
if response_type != 'code':
|
||||
return _error('unsupported_response_type')
|
||||
|
||||
if request.httprequest.method == 'POST':
|
||||
# User approved consent → issue code
|
||||
code = request.env['task.team.oauth.code'].sudo().generate_code(
|
||||
user_id=request.env.user.id,
|
||||
client_id=client_id,
|
||||
redirect_uri=redirect_uri or '',
|
||||
state=state or '',
|
||||
)
|
||||
params = urllib.parse.urlencode(
|
||||
{'code': code, 'state': state or ''}, quote_via=urllib.parse.quote
|
||||
)
|
||||
return request.redirect(f'{redirect_uri}?{params}')
|
||||
|
||||
# GET → render consent form
|
||||
return request.render(
|
||||
'task_team_connector.oauth_consent_template',
|
||||
{
|
||||
'client_id': client_id,
|
||||
'redirect_uri': redirect_uri,
|
||||
'state': state,
|
||||
'user_name': request.env.user.name,
|
||||
},
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Token endpoint
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@http.route(
|
||||
'/task_team/oauth/token',
|
||||
type='json',
|
||||
auth='public',
|
||||
methods=['POST'],
|
||||
csrf=False,
|
||||
)
|
||||
def token(self, **kwargs):
|
||||
data = request.get_json_data()
|
||||
grant_type = data.get('grant_type')
|
||||
client_id = data.get('client_id')
|
||||
client_secret = data.get('client_secret')
|
||||
code = data.get('code')
|
||||
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
expected_client_id = ICP.get_param('task_team_connector.oauth_client_id', '')
|
||||
expected_secret = ICP.get_param('task_team_connector.oauth_client_secret', '')
|
||||
|
||||
if client_id != expected_client_id or client_secret != expected_secret:
|
||||
return {'error': 'invalid_client'}
|
||||
if grant_type != 'authorization_code':
|
||||
return {'error': 'unsupported_grant_type'}
|
||||
if not code:
|
||||
return {'error': 'invalid_request'}
|
||||
|
||||
code_record = (
|
||||
request.env['task.team.oauth.code'].sudo().validate_and_consume(code, client_id)
|
||||
)
|
||||
if not code_record:
|
||||
return {'error': 'invalid_grant'}
|
||||
|
||||
user = code_record.user_id
|
||||
tt_user_id = user._tt_get_or_create_user()
|
||||
|
||||
return {
|
||||
'token_type': 'bearer',
|
||||
'odoo_user_id': user.id,
|
||||
'odoo_email': user.email,
|
||||
'odoo_name': user.name,
|
||||
'tt_user_id': tt_user_id,
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Userinfo endpoint
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@http.route('/task_team/oauth/userinfo', type='json', auth='user', methods=['GET'])
|
||||
def userinfo(self, **kwargs):
|
||||
user = request.env.user
|
||||
return {
|
||||
'sub': str(user.id),
|
||||
'email': user.email,
|
||||
'name': user.name,
|
||||
'tt_user_id': user.tt_user_id,
|
||||
}
|
||||
82
odoo_modules/task_team_connector/controllers/webhook.py
Normal file
82
odoo_modules/task_team_connector/controllers/webhook.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# Task Team Connector — Inbound webhook controller
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TaskTeamWebhookController(http.Controller):
|
||||
|
||||
@http.route(
|
||||
'/task_team/webhook/sync',
|
||||
type='json',
|
||||
auth='public',
|
||||
methods=['POST'],
|
||||
csrf=False,
|
||||
)
|
||||
def sync_webhook(self, **kwargs):
|
||||
"""Receive task-sync events from Task Team API."""
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
secret = ICP.get_param('task_team_connector.webhook_secret', '')
|
||||
|
||||
# HMAC-SHA256 signature verification (when secret is configured)
|
||||
if secret:
|
||||
signature = request.httprequest.headers.get('X-Webhook-Signature', '')
|
||||
body = request.httprequest.get_data()
|
||||
expected = 'sha256=' + hmac.new(
|
||||
secret.encode(), body, hashlib.sha256
|
||||
).hexdigest()
|
||||
if not hmac.compare_digest(expected, signature):
|
||||
_logger.warning('Task Team webhook: invalid HMAC signature')
|
||||
return {'error': 'invalid_signature', 'code': 401}
|
||||
|
||||
data = request.get_json_data()
|
||||
event = data.get('event', 'task_updated')
|
||||
_logger.info('Task Team inbound webhook: event=%s', event)
|
||||
|
||||
if event in ('task_created', 'task_updated'):
|
||||
task_data = data.get('task') or data
|
||||
request.env['project.task'].sudo().tt_upsert_from_webhook(task_data)
|
||||
return {'status': 'ok', 'event': event}
|
||||
|
||||
if event == 'task_deleted':
|
||||
tt_id = data.get('id') or data.get('tt_task_id')
|
||||
if tt_id:
|
||||
task = request.env['project.task'].sudo().search(
|
||||
[('tt_task_id', '=', tt_id)], limit=1
|
||||
)
|
||||
if task:
|
||||
task.with_context(tt_no_sync=True).write({'active': False})
|
||||
return {'status': 'ok', 'event': event}
|
||||
|
||||
if event == 'task_completed':
|
||||
# Mark the Odoo task stage as closed if possible
|
||||
tt_id = data.get('tt_task_id')
|
||||
if tt_id:
|
||||
task = request.env['project.task'].sudo().search(
|
||||
[('tt_task_id', '=', tt_id)], limit=1
|
||||
)
|
||||
if task and task.project_id:
|
||||
closed_stage = request.env['project.task.type'].sudo().search(
|
||||
[
|
||||
('project_ids', 'in', task.project_id.id),
|
||||
('is_closed', '=', True),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
if closed_stage:
|
||||
task.with_context(tt_no_sync=True).write(
|
||||
{'stage_id': closed_stage.id}
|
||||
)
|
||||
return {'status': 'ok', 'event': event}
|
||||
|
||||
_logger.debug('Task Team webhook: unhandled event type %s', event)
|
||||
return {'status': 'ok', 'event': event, 'note': 'unhandled'}
|
||||
|
||||
@http.route('/task_team/webhook/health', type='json', auth='public', methods=['GET'])
|
||||
def health(self, **kwargs):
|
||||
return {'status': 'ok', 'module': 'task_team_connector'}
|
||||
Reference in New Issue
Block a user