Coverage for controllers / main.py: 23%

105 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-28 01:16 +0000

1# -*- coding: utf-8 -*- 

2 

3from odoo import http 

4from odoo.http import request 

5import json 

6import logging 

7import hmac 

8import hashlib 

9 

10_logger = logging.getLogger(__name__) 

11 

12 

13class LoxBackupController(http.Controller): 

14 """Controller for LOX Backup webhooks and API endpoints""" 

15 

16 def _verify_signature(self, api_key, signature, body): 

17 """Verify webhook signature""" 

18 if not signature: 

19 return False 

20 expected = hmac.new( 

21 api_key.encode('utf-8'), 

22 body, 

23 hashlib.sha256 

24 ).hexdigest() 

25 return hmac.compare_digest(signature, expected) 

26 

27 def _get_config(self): 

28 """Get active LOX backup configuration""" 

29 return request.env['lox.backup.config'].sudo().search([ 

30 ('active', '=', True), 

31 ('source_registered', '=', True), 

32 ], limit=1) 

33 

34 @http.route('/lox/webhook/backup-status', type='json', auth='public', methods=['POST'], csrf=False) 

35 def webhook_backup_status(self, **kwargs): 

36 """Webhook endpoint for backup status updates from LOX""" 

37 try: 

38 data = request.get_json_data() 

39 backup_uuid = data.get('backup_id') or data.get('uuid') 

40 status = data.get('status') 

41 

42 if not backup_uuid or not status: 

43 return {'error': 'Missing backup_id or status'} 

44 

45 # Find the backup log 

46 log = request.env['lox.backup.log'].sudo().search([ 

47 ('backup_uuid', '=', backup_uuid), 

48 ], limit=1) 

49 

50 if not log: 

51 _logger.warning(f'Webhook: Backup log not found for UUID {backup_uuid}') 

52 return {'error': 'Backup not found'} 

53 

54 # Map status 

55 status_mapping = { 

56 'PENDING': 'pending', 

57 'UPLOADING': 'in_progress', 

58 'VALIDATING': 'validating', 

59 'COMPLETED': 'completed', 

60 'FAILED': 'failed', 

61 'CANCELLED': 'cancelled', 

62 } 

63 

64 updates = { 

65 'status': status_mapping.get(status, 'pending'), 

66 } 

67 

68 if data.get('size'): 

69 updates['size_bytes'] = data['size'] 

70 if data.get('checksum'): 

71 updates['checksum'] = data['checksum'] 

72 if data.get('completed_at'): 

73 updates['completed_at'] = data['completed_at'] 

74 if data.get('error'): 

75 updates['error_message'] = data['error'] 

76 

77 log.write(updates) 

78 _logger.info(f'Webhook: Updated backup {backup_uuid} status to {status}') 

79 

80 return {'success': True} 

81 

82 except Exception as e: 

83 _logger.exception('Webhook error') 

84 return {'error': str(e)} 

85 

86 @http.route('/lox/webhook/restore-ready', type='json', auth='public', methods=['POST'], csrf=False) 

87 def webhook_restore_ready(self, **kwargs): 

88 """Webhook endpoint for restore ready notifications""" 

89 try: 

90 data = request.get_json_data() 

91 backup_uuid = data.get('backup_id') or data.get('uuid') 

92 download_url = data.get('download_url') or data.get('url') 

93 expires_at = data.get('expires_at') 

94 

95 if not backup_uuid: 

96 return {'error': 'Missing backup_id'} 

97 

98 # Find the backup log 

99 log = request.env['lox.backup.log'].sudo().search([ 

100 ('backup_uuid', '=', backup_uuid), 

101 ], limit=1) 

102 

103 if not log: 

104 _logger.warning(f'Webhook: Backup log not found for UUID {backup_uuid}') 

105 return {'error': 'Backup not found'} 

106 

107 log.write({ 

108 'restore_status': 'ready', 

109 'restore_url': download_url, 

110 'restore_expires_at': expires_at, 

111 }) 

112 

113 _logger.info(f'Webhook: Restore ready for backup {backup_uuid}') 

114 return {'success': True} 

115 

116 except Exception as e: 

117 _logger.exception('Webhook error') 

118 return {'error': str(e)} 

119 

120 @http.route('/lox/api/status', type='json', auth='public', methods=['GET', 'POST'], csrf=False) 

121 def api_status(self, **kwargs): 

122 """API endpoint to check module status""" 

123 config = self._get_config() 

124 return { 

125 'status': 'active' if config else 'not_configured', 

126 'source_id': config.source_id if config else None, 

127 'module_version': '18.0.1.0.0', 

128 } 

129 

130 @http.route('/lox/api/trigger-backup', type='json', auth='public', methods=['POST'], csrf=False) 

131 def api_trigger_backup(self, **kwargs): 

132 """API endpoint to trigger a backup remotely""" 

133 try: 

134 data = request.get_json_data() or {} 

135 api_key = request.httprequest.headers.get('X-API-Key') 

136 

137 config = self._get_config() 

138 if not config: 

139 return {'error': 'LOX Backup not configured'} 

140 

141 # Verify API key 

142 if not api_key or api_key != config.api_key: 

143 return {'error': 'Invalid API key'} 

144 

145 component = data.get('component', 'full') 

146 if component not in ('full', 'database', 'filestore'): 

147 component = 'full' 

148 

149 # Create backup via wizard 

150 wizard = request.env['lox.backup.wizard'].sudo().create({ 

151 'config_id': config.id, 

152 'component': component, 

153 }) 

154 

155 # Run backup 

156 wizard.action_run_backup() 

157 

158 return { 

159 'success': True, 

160 'message': 'Backup triggered', 

161 } 

162 

163 except Exception as e: 

164 _logger.exception('API trigger backup error') 

165 return {'error': str(e)} 

166 

167 @http.route('/lox/api/backups', type='json', auth='public', methods=['GET', 'POST'], csrf=False) 

168 def api_list_backups(self, **kwargs): 

169 """API endpoint to list backups""" 

170 try: 

171 api_key = request.httprequest.headers.get('X-API-Key') 

172 

173 config = self._get_config() 

174 if not config: 

175 return {'error': 'LOX Backup not configured'} 

176 

177 # Verify API key 

178 if not api_key or api_key != config.api_key: 

179 return {'error': 'Invalid API key'} 

180 

181 data = request.get_json_data() or {} 

182 limit = data.get('limit', 50) 

183 offset = data.get('offset', 0) 

184 

185 logs = request.env['lox.backup.log'].sudo().search([ 

186 ('config_id', '=', config.id), 

187 ], limit=limit, offset=offset, order='create_date desc') 

188 

189 backups = [] 

190 for log in logs: 

191 backups.append({ 

192 'uuid': log.backup_uuid, 

193 'component': log.component, 

194 'status': log.status, 

195 'size': log.size_bytes, 

196 'created_at': log.create_date.isoformat() if log.create_date else None, 

197 'completed_at': log.completed_at.isoformat() if log.completed_at else None, 

198 }) 

199 

200 return { 

201 'backups': backups, 

202 'total': request.env['lox.backup.log'].sudo().search_count([ 

203 ('config_id', '=', config.id), 

204 ]), 

205 } 

206 

207 except Exception as e: 

208 _logger.exception('API list backups error') 

209 return {'error': str(e)}