107 lines
4.0 KiB
Python
107 lines
4.0 KiB
Python
from datetime import datetime, timedelta
|
|
from . import db
|
|
import secrets
|
|
import string
|
|
from werkzeug.security import generate_password_hash, check_password_hash
|
|
|
|
|
|
class NoteShare(db.Model):
|
|
"""Public sharing links for notes"""
|
|
__tablename__ = 'note_share'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
note_id = db.Column(db.Integer, db.ForeignKey('note.id', ondelete='CASCADE'), nullable=False)
|
|
token = db.Column(db.String(64), unique=True, nullable=False, index=True)
|
|
|
|
# Share settings
|
|
expires_at = db.Column(db.DateTime, nullable=True) # None means no expiry
|
|
password_hash = db.Column(db.String(255), nullable=True) # Optional password protection
|
|
view_count = db.Column(db.Integer, default=0)
|
|
max_views = db.Column(db.Integer, nullable=True) # Limit number of views
|
|
|
|
# Metadata
|
|
created_at = db.Column(db.DateTime, default=datetime.now)
|
|
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
|
last_accessed_at = db.Column(db.DateTime, nullable=True)
|
|
|
|
# Relationships
|
|
note = db.relationship('Note', backref=db.backref('shares', cascade='all, delete-orphan', lazy='dynamic'))
|
|
created_by = db.relationship('User', foreign_keys=[created_by_id])
|
|
|
|
def __init__(self, **kwargs):
|
|
super(NoteShare, self).__init__(**kwargs)
|
|
if not self.token:
|
|
self.token = self.generate_token()
|
|
|
|
@staticmethod
|
|
def generate_token():
|
|
"""Generate a secure random token"""
|
|
alphabet = string.ascii_letters + string.digits
|
|
return ''.join(secrets.choice(alphabet) for _ in range(32))
|
|
|
|
def set_password(self, password):
|
|
"""Set password protection for the share"""
|
|
if password:
|
|
self.password_hash = generate_password_hash(password)
|
|
else:
|
|
self.password_hash = None
|
|
|
|
def check_password(self, password):
|
|
"""Check if the provided password is correct"""
|
|
if not self.password_hash:
|
|
return True
|
|
return check_password_hash(self.password_hash, password)
|
|
|
|
def is_valid(self):
|
|
"""Check if share link is still valid"""
|
|
# Check expiration
|
|
if self.expires_at and datetime.now() > self.expires_at:
|
|
return False
|
|
|
|
# Check view count
|
|
if self.max_views and self.view_count >= self.max_views:
|
|
return False
|
|
|
|
return True
|
|
|
|
def is_expired(self):
|
|
"""Check if the share has expired"""
|
|
if self.expires_at and datetime.now() > self.expires_at:
|
|
return True
|
|
return False
|
|
|
|
def is_view_limit_reached(self):
|
|
"""Check if view limit has been reached"""
|
|
if self.max_views and self.view_count >= self.max_views:
|
|
return True
|
|
return False
|
|
|
|
def record_access(self):
|
|
"""Record that the share was accessed"""
|
|
self.view_count += 1
|
|
self.last_accessed_at = datetime.now()
|
|
|
|
def get_share_url(self, _external=True):
|
|
"""Get the full URL for this share"""
|
|
from flask import url_for
|
|
return url_for('notes_public.view_shared_note',
|
|
token=self.token,
|
|
_external=_external)
|
|
|
|
def to_dict(self):
|
|
"""Convert share to dictionary for API responses"""
|
|
return {
|
|
'id': self.id,
|
|
'token': self.token,
|
|
'url': self.get_share_url(),
|
|
'expires_at': self.expires_at.isoformat() if self.expires_at else None,
|
|
'has_password': bool(self.password_hash),
|
|
'max_views': self.max_views,
|
|
'view_count': self.view_count,
|
|
'created_at': self.created_at.isoformat(),
|
|
'created_by': self.created_by.username,
|
|
'last_accessed_at': self.last_accessed_at.isoformat() if self.last_accessed_at else None,
|
|
'is_valid': self.is_valid(),
|
|
'is_expired': self.is_expired(),
|
|
'is_view_limit_reached': self.is_view_limit_reached()
|
|
} |