spotnotes.app/blueprints/api.py

392 lines
14 KiB
Python
Raw Normal View History

2026-05-12 23:58:18 +00:00
from flask import Blueprint, jsonify, request, session, current_app, Response
from extensions import db
from itsdangerous import URLSafeSerializer
from models.notepad import Notepad
from models.note import Note
from models.collaborator import Collaborator
from models.user import User
from models.favorite import Favorite
import csv
from io import StringIO
api_bp = Blueprint('api', __name__, url_prefix='/api')
def get_share_token(item_type, item_id):
s = URLSafeSerializer(current_app.config['SECRET_KEY'], salt='share-link')
return s.dumps({'type': item_type, 'id': item_id})
def validate_share_token(token, item_type, item_id):
s = URLSafeSerializer(current_app.config['SECRET_KEY'], salt='share-link')
try:
data = s.loads(token)
return data.get('type') == item_type and data.get('id') == item_id
except:
return False
def check_pad_read_permission(notepad, user_id, token=None):
is_shared_by_link = token and validate_share_token(token, 'notepad', notepad.id)
if notepad.is_public or is_shared_by_link:
return True
if not user_id:
return False
if notepad.user_id == user_id:
return True
# Check for collaborator (any permission level grants read)
if Collaborator.query.filter_by(user_id=user_id, notepad_id=notepad.id).first():
return True
return False
def check_pad_write_permission(notepad, user_id):
if not user_id:
return False
if notepad.user_id == user_id:
return True
# Check for collaborator with 'write' permission
collab = Collaborator.query.filter_by(user_id=user_id, notepad_id=notepad.id, permission='write').first()
return collab is not None
@api_bp.route('/notepads', methods=['GET'])
def get_notepads():
user_id = session.get('user_id')
filter_type = request.args.get('type')
query = Notepad.query
if filter_type == 'favorites':
query = Notepad.query.join(Favorite).filter(Favorite.user_id == user_id) if user_id else Notepad.query.filter_by(id=-1)
elif filter_type == 'mine':
query = query.filter_by(user_id=user_id) if user_id else Notepad.query.filter_by(id=-1)
elif filter_type == 'shared':
query = Notepad.query.join(Collaborator).filter(Collaborator.user_id == user_id) if user_id else Notepad.query.filter_by(id=-1)
elif filter_type == 'public':
query = query.filter_by(is_public=True)
else:
# Default behavior (union) if no type specified
if user_id:
query = query.filter((Notepad.is_public == True) | (Notepad.user_id == user_id))
else:
query = query.filter_by(is_public=True)
pads = query.order_by(Notepad.name).all()
results = []
for pad in pads:
p_dict = pad.to_dict()
if user_id:
if pad.user_id == user_id:
p_dict['share_token'] = get_share_token('notepad', pad.id)
p_dict['can_write'] = check_pad_write_permission(pad, user_id)
else:
p_dict['can_write'] = False
results.append(p_dict)
return jsonify(results)
@api_bp.route('/notepads/<int:pad_id>', methods=['GET'])
def get_notepad(pad_id):
notepad = Notepad.query.get_or_404(pad_id)
user_id = session.get('user_id')
token = request.args.get('token')
if not check_pad_read_permission(notepad, user_id, token):
return jsonify({'error': 'Unauthorized'}), 403
p_dict = notepad.to_dict()
p_dict['can_write'] = check_pad_write_permission(notepad, user_id)
if user_id and notepad.user_id == user_id:
p_dict['share_token'] = get_share_token('notepad', pad_id)
return jsonify(p_dict)
@api_bp.route('/notepads', methods=['POST'])
def create_notepad():
user_id = session.get('user_id')
if not user_id:
return jsonify({'error': 'Unauthorized'}), 401
data = request.get_json()
new_pad = Notepad(name=data['name'], is_public=data.get('is_public', False), user_id=user_id)
db.session.add(new_pad)
db.session.commit()
return jsonify(new_pad.to_dict()), 201
@api_bp.route('/notepads/<int:pad_id>/notes', methods=['GET'])
def get_notes(pad_id):
notepad = Notepad.query.get_or_404(pad_id)
user_id = session.get('user_id')
token = request.args.get('token')
if not check_pad_read_permission(notepad, user_id, token):
return jsonify({'error': 'Unauthorized'}), 403
notes = notepad.notes.filter_by(is_hidden=False).all()
results = []
for note in notes:
n_dict = note.to_dict()
if user_id and notepad.user_id == user_id:
n_dict['share_token'] = get_share_token('note', note.id)
results.append(n_dict)
return jsonify(results)
@api_bp.route('/notes/<int:note_id>', methods=['GET'])
def get_note(note_id):
note = Note.query.get_or_404(note_id)
notepad = Notepad.query.get(note.notepad_id)
user_id = session.get('user_id')
token = request.args.get('token')
is_shared = token and validate_share_token(token, 'note', note_id)
if not notepad.is_public and (not user_id or notepad.user_id != user_id) and not is_shared:
return jsonify({'error': 'Unauthorized'}), 403
n_dict = note.to_dict()
n_dict['can_write'] = check_pad_write_permission(notepad, user_id)
pad_dict = notepad.to_dict()
# Redact name if private and not owner
if not notepad.is_public and (not user_id or notepad.user_id != user_id):
pad_dict['name'] = "Private Notepad"
n_dict['notepad'] = pad_dict
n_dict['owner_id'] = notepad.user_id
n_dict['is_public'] = notepad.is_public
if user_id and notepad.user_id == user_id:
n_dict['share_token'] = get_share_token('note', note_id)
return jsonify(n_dict)
@api_bp.route('/notes/mine', methods=['GET'])
def get_my_all_notes():
user_id = session.get('user_id')
if not user_id:
return jsonify({'error': 'Unauthorized'}), 401
notes = Note.query.join(Notepad).filter(Notepad.user_id == user_id, Note.is_hidden == False).all()
results = []
for note in notes:
n_dict = note.to_dict()
n_dict['notepad'] = note.notepad.to_dict()
n_dict['owner_id'] = user_id
n_dict['is_public'] = note.notepad.is_public
n_dict['share_token'] = get_share_token('note', note.id)
results.append(n_dict)
return jsonify(results)
@api_bp.route('/notes', methods=['POST'])
def create_note():
user_id = session.get('user_id')
if not user_id:
return jsonify({'error': 'Unauthorized'}), 401
data = request.get_json()
notepad_id = data.get('notepad_id')
if not notepad_id:
return jsonify({'error': 'notepad_id is required'}), 400
notepad = Notepad.query.get_or_404(notepad_id)
if not check_pad_write_permission(notepad, user_id):
return jsonify({'error': 'Forbidden'}), 403
new_note = Note(
title=data.get('title', 'Untitled'),
content=data['content'],
lat=data['lat'],
lng=data['lng'],
notepad_id=notepad_id
)
db.session.add(new_note)
db.session.commit()
return jsonify(new_note.to_dict()), 201
@api_bp.route('/notes/<int:note_id>', methods=['PUT'])
def update_note(note_id):
user_id = session.get('user_id')
if not user_id:
return jsonify({'error': 'Unauthorized'}), 401
note = Note.query.get_or_404(note_id)
notepad = Notepad.query.get(note.notepad_id)
if not check_pad_write_permission(notepad, user_id):
return jsonify({'error': 'Forbidden'}), 403
data = request.get_json()
note.title = data.get('title', note.title)
note.content = data.get('content', note.content)
db.session.commit()
return jsonify(note.to_dict())
@api_bp.route('/notepads/<int:pad_id>', methods=['PUT'])
def update_notepad(pad_id):
user_id = session.get('user_id')
if not user_id:
return jsonify({'error': 'Unauthorized'}), 401
notepad = Notepad.query.get_or_404(pad_id)
# Only owner can edit notepad metadata
if notepad.user_id != user_id:
return jsonify({'error': 'Forbidden'}), 403
data = request.get_json()
notepad.name = data.get('name', notepad.name)
notepad.is_public = data.get('is_public', notepad.is_public)
notepad.color = data.get('color', notepad.color)
2026-05-12 23:58:18 +00:00
db.session.commit()
return jsonify(notepad.to_dict())
@api_bp.route('/notes/<int:note_id>/report', methods=['POST'])
def report_note(note_id):
note = Note.query.get_or_404(note_id)
if not note.notepad.is_public:
return jsonify({'error': 'Can only report public notes'}), 400
note.is_hidden = True
db.session.commit()
return jsonify({'message': 'Note reported'}), 200
@api_bp.route('/notepads/<int:pad_id>/favorite', methods=['POST'])
def favorite_notepad(pad_id):
user_id = session.get('user_id')
if not user_id:
return jsonify({'error': 'Unauthorized'}), 401
existing_favorite = Favorite.query.filter_by(user_id=user_id, notepad_id=pad_id).first()
if existing_favorite:
return jsonify({'message': 'Notepad already favorited'}), 200
new_favorite = Favorite(user_id=user_id, notepad_id=pad_id)
db.session.add(new_favorite)
db.session.commit()
return jsonify({'message': 'Notepad favorited'}), 201
@api_bp.route('/notepads/<int:pad_id>/unfavorite', methods=['POST'])
def unfavorite_notepad(pad_id):
user_id = session.get('user_id')
favorite = Favorite.query.filter_by(user_id=user_id, notepad_id=pad_id).first()
if favorite:
db.session.delete(favorite)
db.session.commit()
return jsonify({'message': 'Notepad unfavorited'}), 200
@api_bp.route('/notepads/<int:pad_id>/collaborators', methods=['GET'])
def get_collaborators(pad_id):
user_id = session.get('user_id')
notepad = Notepad.query.get_or_404(pad_id)
# Only owner can see collaborators
if notepad.user_id != user_id:
return jsonify({'error': 'Forbidden'}), 403
collaborators = Collaborator.query.join(User).filter(Collaborator.notepad_id == pad_id).all()
results = []
for collab in collaborators:
results.append({
'user_id': collab.user.id,
'email': collab.user.email,
'permission': collab.permission
})
return jsonify(results)
@api_bp.route('/notepads/<int:pad_id>/collaborators', methods=['POST'])
def add_collaborator(pad_id):
user_id = session.get('user_id')
notepad = Notepad.query.get_or_404(pad_id)
if notepad.user_id != user_id:
return jsonify({'error': 'Forbidden'}), 403
data = request.get_json()
email = data.get('email')
permission = data.get('permission')
if not email or not permission or permission not in ['read', 'write']:
return jsonify({'error': 'Email and permission (read/write) are required'}), 400
collaborator_user = User.query.filter_by(email=email).first()
if not collaborator_user:
return jsonify({'error': 'User not found'}), 404
if collaborator_user.id == user_id:
return jsonify({'error': 'Cannot add yourself as a collaborator'}), 400
existing_collab = Collaborator.query.filter_by(notepad_id=pad_id, user_id=collaborator_user.id).first()
if existing_collab:
existing_collab.permission = permission
else:
new_collab = Collaborator(notepad_id=pad_id, user_id=collaborator_user.id, permission=permission)
db.session.add(new_collab)
db.session.commit()
return jsonify({'message': 'Collaborator added/updated successfully'}), 200
@api_bp.route('/notepads/<int:pad_id>/collaborators/<int:collaborator_user_id>', methods=['DELETE'])
def remove_collaborator(pad_id, collaborator_user_id):
user_id = session.get('user_id')
notepad = Notepad.query.get_or_404(pad_id)
if notepad.user_id != user_id:
return jsonify({'error': 'Forbidden'}), 403
collab_to_remove = Collaborator.query.filter_by(notepad_id=pad_id, user_id=collaborator_user_id).first()
if not collab_to_remove:
return jsonify({'error': 'Collaborator not found'}), 404
db.session.delete(collab_to_remove)
db.session.commit()
return jsonify({'message': 'Collaborator removed'}), 200
@api_bp.route('/notepads/<int:pad_id>/export', methods=['GET'])
def export_notepad(pad_id):
notepad = Notepad.query.get_or_404(pad_id)
user_id = session.get('user_id')
token = request.args.get('token')
if not check_pad_read_permission(notepad, user_id, token):
return jsonify({'error': 'Unauthorized'}), 403
si = StringIO()
cw = csv.writer(si)
cw.writerow(['Note ID', 'Title', 'Content', 'Latitude', 'Longitude', 'Created At'])
for note in notepad.notes.filter_by(is_hidden=False):
cw.writerow([note.id, note.title, note.content, note.lat, note.lng, note.created_at])
output = si.getvalue()
return Response(
output,
mimetype="text/csv",
headers={"Content-Disposition": f"attachment;filename=notepad_{pad_id}_export.csv"}
)
@api_bp.route('/notepads/<int:pad_id>', methods=['DELETE'])
def delete_notepad(pad_id):
user_id = session.get('user_id')
if not user_id:
return jsonify({'error': 'Unauthorized'}), 401
notepad = Notepad.query.get_or_404(pad_id)
if notepad.user_id != user_id:
return jsonify({'error': 'Forbidden'}), 403
for note in notepad.notes:
db.session.delete(note)
Favorite.query.filter_by(notepad_id=notepad.id).delete()
db.session.delete(notepad)
db.session.commit()
return jsonify({'message': 'Notepad deleted'}), 200
@api_bp.route('/notes/<int:note_id>', methods=['DELETE'])
def delete_note(note_id):
user_id = session.get('user_id')
if not user_id:
return jsonify({'error': 'Unauthorized'}), 401
note = Note.query.get_or_404(note_id)
notepad = Notepad.query.get(note.notepad_id)
if not check_pad_write_permission(notepad, user_id):
return jsonify({'error': 'Forbidden'}), 403
db.session.delete(note)
db.session.commit()
return jsonify({'message': 'Note deleted'}), 200