04/07 updated password reset, ticket templates, and satisfaction survey
This commit is contained in:
+131
@@ -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}>'
|
||||
|
||||
Reference in New Issue
Block a user