# pyrefly: ignore [missing-import] from odoo import http, fields # pyrefly: ignore [missing-import] from odoo.http import request # pyrefly: ignore [missing-import] from odoo.addons.portal.controllers.portal import CustomerPortal class HostelPortalController(CustomerPortal): """Student portal controller for hostel management. SECURITY DESIGN: - Identity is ALWAYS derived from request.env.user.student_id - URL-supplied IDs (complaint_id, etc.) are NEVER trusted for authorization - All lookups use additional domain [('student_id', '=', student.id)] - Record rules provide a second layer of enforcement at ORM level sudo() AUDIT: - sudo() is NOT used in any route below - Portal user has read access to own records via record rules - Create (complaint, visitor) is allowed via group_portal model access with record-rule enforcement on student_id.portal_user_id = user.id """ def _get_student(self): """Authoritative student lookup — NEVER trust URL parameters.""" return request.env.user.student_id def _get_hostel_member(self): """Returns the student's active allocation (if any).""" student = self._get_student() if not student: return request.env['education.hostel.member'].browse() return request.env['education.hostel.member'].search([ ('student_id', '=', student.id), ('status', '=', 'active'), ], limit=1) def _prepare_home_portal_values(self, counters): values = super()._prepare_home_portal_values(counters) student = self._get_student() if student: member = self._get_hostel_member() values['hostel_member_count'] = 1 if member else 0 return values # ── /my/hostel ──────────────────────────────────────────────────────── @http.route(['/my/hostel'], type='http', auth='user', website=True) def hostel_dashboard(self, **kw): student = self._get_student() if not student: return request.render('education_portal.portal_no_student') member = self._get_hostel_member() # Complaints — only student's own (record rule also enforces) complaints = request.env['education.hostel.complaint'].search([ ('student_id', '=', student.id), ], order='submitted_date desc', limit=5) # Fees — only student's own fees = request.env['education.hostel.fee'].search([ ('student_id', '=', student.id), ('state', 'in', ('due', 'overdue')), ], order='due_date asc', limit=5) # Recent visitors — only student's own visitors = request.env['education.hostel.visitor'].search([ ('student_id', '=', student.id), ], order='visit_date desc', limit=3) return request.render('education_hostel.portal_hostel_dashboard', { 'student': student, 'member': member, 'complaints': complaints, 'fees': fees, 'visitors': visitors, 'page_name': 'hostel_dashboard', }) # ── /my/hostel/allocation ───────────────────────────────────────────── @http.route(['/my/hostel/allocation'], type='http', auth='user', website=True) def hostel_allocation(self, **kw): student = self._get_student() if not student: return request.render('education_portal.portal_no_student') member = self._get_hostel_member() if not member: return request.render('education_hostel.portal_hostel_no_allocation') return request.render('education_hostel.portal_hostel_allocation', { 'student': student, 'member': member, 'page_name': 'hostel_allocation', }) # ── /my/hostel/complaints ───────────────────────────────────────────── @http.route(['/my/hostel/complaints'], type='http', auth='user', website=True) def hostel_complaints(self, **kw): student = self._get_student() if not student: return request.render('education_portal.portal_no_student') complaints = request.env['education.hostel.complaint'].search([ ('student_id', '=', student.id), ], order='submitted_date desc') return request.render('education_hostel.portal_hostel_complaints', { 'student': student, 'complaints': complaints, 'page_name': 'hostel_complaints', }) # ── /my/hostel/complaint/new ────────────────────────────────────────── @http.route(['/my/hostel/complaint/new'], type='http', auth='user', website=True, methods=['GET', 'POST']) def hostel_complaint_new(self, **post): student = self._get_student() if not student: return request.render('education_portal.portal_no_student') member = self._get_hostel_member() if not member: return request.redirect('/my/hostel') if request.httprequest.method == 'POST': title = post.get('title', '').strip() category = post.get('category', 'other') description = post.get('description', '').strip() if not title: return request.render('education_hostel.portal_hostel_complaint_form', { 'student': student, 'member': member, 'error': 'Complaint title is required.', 'post': post, 'page_name': 'hostel_complaint_new', }) # Create complaint — student_id is set server-side, NEVER from POST complaint = request.env['education.hostel.complaint'].create({ 'student_id': student.id, # server-derived identity 'hostel_id': member.hostel_id.id, 'room_id': member.room_id.id, 'category': category, 'title': title, 'description': description, 'state': 'submitted', 'submitted_date': fields.Date.today(), 'company_id': student.company_id.id, }) return request.redirect('/my/hostel/complaints') return request.render('education_hostel.portal_hostel_complaint_form', { 'student': student, 'member': member, 'page_name': 'hostel_complaint_new', }) # ── /my/hostel/fees ─────────────────────────────────────────────────── @http.route(['/my/hostel/fees'], type='http', auth='user', website=True) def hostel_fees(self, **kw): student = self._get_student() if not student: return request.render('education_portal.portal_no_student') fees = request.env['education.hostel.fee'].search([ ('student_id', '=', student.id), ], order='due_date desc') return request.render('education_hostel.portal_hostel_fees', { 'student': student, 'fees': fees, 'page_name': 'hostel_fees', }) # ── /my/hostel/visitors ─────────────────────────────────────────────── @http.route(['/my/hostel/visitors'], type='http', auth='user', website=True, methods=['GET', 'POST']) def hostel_visitors(self, **post): student = self._get_student() if not student: return request.render('education_portal.portal_no_student') member = self._get_hostel_member() if request.httprequest.method == 'POST' and member: visitor_name = post.get('visitor_name', '').strip() if visitor_name: request.env['education.hostel.visitor'].create({ 'student_id': student.id, # server-derived identity 'hostel_id': member.hostel_id.id, 'visitor_name': visitor_name, 'relationship': post.get('relationship', '').strip(), 'phone': post.get('phone', '').strip(), 'purpose': post.get('purpose', '').strip(), 'visit_date': post.get('visit_date') or fields.Date.today(), 'state': 'pending', 'company_id': student.company_id.id, }) return request.redirect('/my/hostel/visitors') visitors = request.env['education.hostel.visitor'].search([ ('student_id', '=', student.id), ], order='visit_date desc') return request.render('education_hostel.portal_hostel_visitors', { 'student': student, 'member': member, 'visitors': visitors, 'page_name': 'hostel_visitors', })