04/16 Upload codebase
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
from flask import Blueprint, request, jsonify, g
|
||||
from app import db
|
||||
from app.models.user import User
|
||||
from app.models.shared_item import SharedItem
|
||||
from app.services.auth_service import require_jwt
|
||||
|
||||
sharing_bp = Blueprint('sharing', __name__)
|
||||
|
||||
|
||||
# ── Sharing keypair management ────────────────────────────────────────────────
|
||||
|
||||
@sharing_bp.route('/keys', methods=['GET'])
|
||||
@require_jwt
|
||||
def get_my_keys():
|
||||
"""Return current user's encrypted sharing private key (to decrypt client-side)."""
|
||||
user = User.query.get(g.current_user_id)
|
||||
if not user.sharing_public_key:
|
||||
return jsonify({'keys_setup': False}), 200
|
||||
return jsonify({
|
||||
'keys_setup': True,
|
||||
'public_key': user.sharing_public_key,
|
||||
'private_key_enc': user.sharing_private_key_enc,
|
||||
'private_key_iv': user.sharing_private_key_iv,
|
||||
}), 200
|
||||
|
||||
|
||||
@sharing_bp.route('/keys', methods=['POST'])
|
||||
@require_jwt
|
||||
def store_my_keys():
|
||||
"""Store ECDH keypair. Public key plaintext; private key encrypted with vault key."""
|
||||
data = request.get_json(silent=True) or {}
|
||||
public_key = data.get('public_key', '').strip()
|
||||
private_key_enc = data.get('private_key_enc', '').strip()
|
||||
private_key_iv = data.get('private_key_iv', '').strip()
|
||||
|
||||
if not public_key or not private_key_enc or not private_key_iv:
|
||||
return jsonify({'error': 'public_key, private_key_enc, and private_key_iv are required'}), 400
|
||||
|
||||
user = User.query.get(g.current_user_id)
|
||||
user.sharing_public_key = public_key
|
||||
user.sharing_private_key_enc = private_key_enc
|
||||
user.sharing_private_key_iv = private_key_iv
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Sharing keys stored'}), 200
|
||||
|
||||
|
||||
@sharing_bp.route('/public-key', methods=['GET'])
|
||||
@require_jwt
|
||||
def get_public_key():
|
||||
"""Look up another user's ECDH public key by email (needed to create a share)."""
|
||||
email = (request.args.get('email') or '').strip().lower()
|
||||
if not email:
|
||||
return jsonify({'error': 'email query param is required'}), 400
|
||||
|
||||
user = User.query.filter_by(email=email).first()
|
||||
if not user:
|
||||
return jsonify({'error': 'User not found'}), 404
|
||||
if not user.sharing_public_key:
|
||||
return jsonify({'error': 'User has not set up sharing keys yet'}), 404
|
||||
|
||||
return jsonify({
|
||||
'user_id': user.id,
|
||||
'email': user.email,
|
||||
'public_key': user.sharing_public_key,
|
||||
}), 200
|
||||
|
||||
|
||||
# ── Outgoing shares ───────────────────────────────────────────────────────────
|
||||
|
||||
@sharing_bp.route('', methods=['GET'])
|
||||
@require_jwt
|
||||
def list_outgoing():
|
||||
"""List all items the current user has shared with others."""
|
||||
shares = (
|
||||
SharedItem.query
|
||||
.filter_by(owner_id=g.current_user_id)
|
||||
.order_by(SharedItem.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
result = []
|
||||
for s in shares:
|
||||
d = s.to_dict()
|
||||
recipient = User.query.get(s.recipient_id) if s.recipient_id else None
|
||||
d['recipient_name'] = recipient.email if recipient else s.recipient_email
|
||||
result.append(d)
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
@sharing_bp.route('', methods=['POST'])
|
||||
@require_jwt
|
||||
def create_share():
|
||||
"""
|
||||
Share a vault item with another user.
|
||||
|
||||
The caller must already have:
|
||||
1. Fetched the recipient's public key via GET /api/sharing/public-key?email=...
|
||||
2. Loaded their own ECDH private key (decrypted client-side with vault key)
|
||||
3. Derived the ECDH shared secret
|
||||
4. Re-encrypted the item's plaintext with that shared secret → enc_data, iv
|
||||
"""
|
||||
data = request.get_json(silent=True) or {}
|
||||
item_id = data.get('item_id')
|
||||
recipient_email = (data.get('recipient_email') or '').strip().lower()
|
||||
enc_data = data.get('enc_data', '')
|
||||
iv = data.get('iv', '')
|
||||
item_name = (data.get('item_name') or '').strip()
|
||||
item_type = data.get('item_type', 'password')
|
||||
|
||||
if not all([item_id, recipient_email, enc_data, iv, item_name]):
|
||||
return jsonify({'error': 'item_id, recipient_email, enc_data, iv, item_name are required'}), 400
|
||||
|
||||
owner = User.query.get(g.current_user_id)
|
||||
if owner.email == recipient_email:
|
||||
return jsonify({'error': 'Cannot share an item with yourself'}), 400
|
||||
|
||||
# Verify the item belongs to the current user
|
||||
from app.models.vault_item import VaultItem
|
||||
item = VaultItem.query.filter_by(id=item_id, user_id=g.current_user_id).first()
|
||||
if not item:
|
||||
return jsonify({'error': 'Item not found'}), 404
|
||||
|
||||
recipient = User.query.filter_by(email=recipient_email).first()
|
||||
|
||||
share = SharedItem(
|
||||
item_id=item_id,
|
||||
owner_id=g.current_user_id,
|
||||
recipient_email=recipient_email,
|
||||
recipient_id=recipient.id if recipient else None,
|
||||
item_name=item_name,
|
||||
item_type=item_type,
|
||||
enc_data=enc_data,
|
||||
iv=iv,
|
||||
)
|
||||
db.session.add(share)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(share.to_dict()), 201
|
||||
|
||||
|
||||
@sharing_bp.route('/<int:share_id>', methods=['DELETE'])
|
||||
@require_jwt
|
||||
def delete_share(share_id):
|
||||
share = SharedItem.query.filter_by(id=share_id, owner_id=g.current_user_id).first()
|
||||
if not share:
|
||||
return jsonify({'error': 'Share not found'}), 404
|
||||
db.session.delete(share)
|
||||
db.session.commit()
|
||||
return jsonify({'message': 'Share removed'}), 200
|
||||
|
||||
|
||||
# ── Inbox (received shares) ───────────────────────────────────────────────────
|
||||
|
||||
@sharing_bp.route('/inbox', methods=['GET'])
|
||||
@require_jwt
|
||||
def inbox():
|
||||
"""List all items shared with the current user."""
|
||||
user = User.query.get(g.current_user_id)
|
||||
shares = (
|
||||
SharedItem.query
|
||||
.filter(
|
||||
db.or_(
|
||||
SharedItem.recipient_email == user.email,
|
||||
SharedItem.recipient_id == user.id,
|
||||
)
|
||||
)
|
||||
.order_by(SharedItem.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
result = []
|
||||
for s in shares:
|
||||
d = s.to_dict()
|
||||
owner = User.query.get(s.owner_id)
|
||||
d['owner_email'] = owner.email if owner else 'Unknown'
|
||||
d['owner_public_key'] = owner.sharing_public_key if owner else None
|
||||
result.append(d)
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
@sharing_bp.route('/inbox/<int:share_id>/accept', methods=['POST'])
|
||||
@require_jwt
|
||||
def accept_share(share_id):
|
||||
"""Mark a received share as accepted (links recipient_id if not already set)."""
|
||||
user = User.query.get(g.current_user_id)
|
||||
share = SharedItem.query.filter(
|
||||
SharedItem.id == share_id,
|
||||
db.or_(
|
||||
SharedItem.recipient_email == user.email,
|
||||
SharedItem.recipient_id == user.id,
|
||||
),
|
||||
).first()
|
||||
if not share:
|
||||
return jsonify({'error': 'Share not found'}), 404
|
||||
|
||||
share.accepted = True
|
||||
share.recipient_id = user.id
|
||||
db.session.commit()
|
||||
|
||||
d = share.to_dict()
|
||||
owner = User.query.get(share.owner_id)
|
||||
d['owner_email'] = owner.email if owner else 'Unknown'
|
||||
d['owner_public_key'] = owner.sharing_public_key if owner else None
|
||||
return jsonify(d), 200
|
||||
Reference in New Issue
Block a user