04/07 updated password reset, ticket templates, and satisfaction survey

This commit is contained in:
2026-04-07 17:32:11 -04:00
parent 36945f5976
commit 60db5c3f9a
18 changed files with 1309 additions and 105 deletions
+131
View File
@@ -453,3 +453,134 @@ class KBFeedback(db.Model):
def __repr__(self):
return f'<KBFeedback article={self.article_id} user={self.user_id} helpful={self.is_helpful}>'
class PasswordResetToken(db.Model):
"""Single-use time-limited token for self-service password reset.
Tokens are stored hashed (SHA-256) so a database breach does not
expose valid reset links. Each token expires after 1 hour and is
deleted on first use.
"""
__tablename__ = 'password_reset_tokens'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id',
ondelete='CASCADE'), nullable=False)
token_hash = db.Column(db.String(64), unique=True, nullable=False, index=True)
expires_at = db.Column(db.DateTime, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
user = db.relationship('User', foreign_keys=[user_id])
@classmethod
def generate(cls, user_id):
"""Create a new token, persist it, and return the raw token string.
Caller must commit the session.
"""
import secrets, hashlib
from datetime import timedelta
raw = secrets.token_urlsafe(32)
hashed= hashlib.sha256(raw.encode()).hexdigest()
token = cls(
user_id = user_id,
token_hash = hashed,
expires_at = datetime.utcnow() + timedelta(hours=1),
)
db.session.add(token)
return raw, token
@classmethod
def verify(cls, raw):
"""Return the token row if raw is valid and unexpired, else None."""
import hashlib
hashed = hashlib.sha256(raw.encode()).hexdigest()
row = cls.query.filter_by(token_hash=hashed).first()
if row and row.expires_at > datetime.utcnow():
return row
return None
def __repr__(self):
return f'<PasswordResetToken user={self.user_id}>'
class TicketTemplate(db.Model):
"""Pre-filled ticket scaffolds selectable by employees on the New Ticket form.
Templates reduce friction for common request types (e.g. VPN Access,
New Laptop Setup) and improve ticket quality by pre-populating category,
priority, and a structured description prompt.
"""
__tablename__ = 'ticket_templates'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), nullable=False)
description = db.Column(db.Text, nullable=False, default='')
# Pre-filled form values
category = db.Column(db.String(50), nullable=False, default='other')
priority = db.Column(db.String(20), nullable=False, default='medium')
title_hint = db.Column(db.String(200), nullable=False, default='')
# UI grouping
icon = db.Column(db.String(40), nullable=False, default='bi-file-text')
is_active = db.Column(db.Boolean, default=True, nullable=False)
sort_order = db.Column(db.Integer, default=0)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow,
onupdate=datetime.utcnow)
creator = db.relationship('User', foreign_keys=[created_by])
def __repr__(self):
return f'<TicketTemplate {self.name}>'
class TicketSatisfaction(db.Model):
"""One satisfaction rating per resolved ticket.
Sent automatically when a ticket moves to Resolved status.
The rating (1-5 stars) and optional comment are submitted via a
token-authenticated endpoint so the employee does not need to be
logged in to respond (they click from the email).
"""
__tablename__ = 'ticket_satisfaction'
id = db.Column(db.Integer, primary_key=True)
ticket_id = db.Column(db.Integer, db.ForeignKey('tickets.id',
ondelete='CASCADE'), unique=True, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id',
ondelete='CASCADE'), nullable=False)
# survey_token authenticates the survey URL in the email — single-use
survey_token= db.Column(db.String(64), unique=True, nullable=False, index=True)
rating = db.Column(db.Integer) # 1-5; NULL until submitted
comment = db.Column(db.Text)
sent_at = db.Column(db.DateTime, default=datetime.utcnow)
submitted_at= db.Column(db.DateTime)
ticket = db.relationship('Ticket',
backref=db.backref('satisfaction', uselist=False,
cascade='all, delete-orphan'))
user = db.relationship('User', foreign_keys=[user_id])
@classmethod
def create_for_ticket(cls, ticket):
"""Create a survey row for a newly-resolved ticket.
Returns None if a survey already exists. Caller must commit.
"""
import secrets
if cls.query.filter_by(ticket_id=ticket.id).first():
return None
row = cls(
ticket_id = ticket.id,
user_id = ticket.created_by_id,
survey_token = secrets.token_urlsafe(32),
)
db.session.add(row)
return row
@property
def submitted(self):
return self.rating is not None
def __repr__(self):
return f'<TicketSatisfaction ticket={self.ticket_id} rating={self.rating}>'