Add Note Sharing feature.

This commit is contained in:
2025-07-08 12:40:50 +02:00
committed by Jens Luedicke
parent d2ca2905fa
commit 66d65a6ed2
11 changed files with 1505 additions and 10 deletions

107
models/note_share.py Normal file
View File

@@ -0,0 +1,107 @@
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()
}