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()
|
2026-06-02 04:22:59 +00:00
|
|
|
new_pad = Notepad(name=data['name'], is_public=data.get('is_public', False), user_id=user_id, color=data.get('color', '#3388ff'))
|
2026-05-12 23:58:18 +00:00
|
|
|
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)
|
2026-06-02 03:49:38 +00:00
|
|
|
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
|