# -*- coding: utf-8 -*- from odoo import fields, models class FusionLoginAudit(models.Model): _name = 'fusion.login.audit' _description = 'Login Audit Event' _order = 'event_time desc, id desc' _rec_name = 'attempted_login' user_id = fields.Many2one( 'res.users', string='User', ondelete='set null', index=True, help='Null when the attempted login did not match any user.', ) attempted_login = fields.Char( string='Attempted Login', size=255, required=True, index=True, ) result = fields.Selection( [('success', 'Success'), ('failure', 'Failure')], string='Result', required=True, index=True, ) failure_reason = fields.Selection( [ ('bad_password', 'Bad password'), ('unknown_user', 'Unknown user'), ('disabled_user', 'Disabled user'), ('2fa_failed', '2FA failed'), ('other', 'Other'), ], string='Failure Reason', ) event_time = fields.Datetime( string='Event Time', required=True, index=True, default=fields.Datetime.now, ) ip_address = fields.Char(string='IP Address', size=45) ip_hostname = fields.Char(string='Reverse DNS', size=255) country_code = fields.Char(string='Country Code', size=2, index=True) country_name = fields.Char(string='Country', size=64) city = fields.Char(string='City', size=128) geo_state = fields.Char(string='Region', size=64) geo_lookup_state = fields.Selection( [ ('pending', 'Pending'), ('done', 'Done'), ('private_ip', 'Private IP'), ('internal', 'Internal (no request)'), ('failed', 'Lookup failed'), ], string='Geo Lookup State', default='pending', index=True, ) user_agent_raw = fields.Char(string='User Agent', size=512) browser = fields.Char(string='Browser', size=64) os = fields.Char(string='OS', size=64) device_type = fields.Selection( [ ('desktop', 'Desktop'), ('mobile', 'Mobile'), ('tablet', 'Tablet'), ('bot', 'Bot'), ('unknown', 'Unknown'), ], string='Device Type', default='unknown', ) database = fields.Char(string='Database', size=64) # Odoo 19 replaces the legacy `_sql_constraints = [...]` list with # declarative `models.Constraint` attributes. The plan template used the # legacy form, which now only emits a warning and is silently dropped. _result_failure_reason_consistent = models.Constraint( "CHECK ((result = 'success' AND failure_reason IS NULL) " "OR (result = 'failure' AND failure_reason IS NOT NULL))", 'A failure row must have a failure_reason; a success row must not.', ) # Composite indexes supporting the three hot queries: # - per-user history (user_id, event_time DESC) # - failure-burst by login (attempted_login, event_time DESC) # - geo cron worklist (geo_lookup_state, event_time) # Odoo 19 ships `models.Index` as the declarative replacement for the # init()/raw-SQL pattern; the attribute name becomes the index suffix # (e.g. `_user_time_idx` -> `fusion_login_audit_user_time_idx`). _user_time_idx = models.Index('(user_id, event_time DESC)') _login_time_idx = models.Index('(attempted_login, event_time DESC)') _geo_state_idx = models.Index('(geo_lookup_state, event_time)')