api support
This commit is contained in:
381
app.py
381
app.py
@@ -5,6 +5,8 @@ from bson.objectid import ObjectId
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import jwt
|
||||
from functools import wraps
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -65,6 +67,23 @@ def get_dashboard_context(user_id):
|
||||
|
||||
return current_entry, active_tasks
|
||||
|
||||
# --- API Helpers ---
|
||||
def token_required(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
token = request.headers.get('Authorization')
|
||||
if not token:
|
||||
return jsonify({'message': 'Token is missing!'}), 401
|
||||
try:
|
||||
if token.startswith('Bearer '):
|
||||
token = token.split(" ")[1]
|
||||
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
|
||||
current_user_id = data['user_id']
|
||||
except:
|
||||
return jsonify({'message': 'Token is invalid!'}), 401
|
||||
return f(current_user_id, *args, **kwargs)
|
||||
return decorated
|
||||
|
||||
# --- Routes ---
|
||||
|
||||
@app.route('/')
|
||||
@@ -795,6 +814,368 @@ def delete_goal(goal_id):
|
||||
db.goals.delete_one({'_id': ObjectId(goal_id), 'user_id': get_user_id()})
|
||||
return redirect(url_for('goals'))
|
||||
|
||||
# --- Auth API ---
|
||||
|
||||
@app.route('/api/login', methods=['POST'])
|
||||
def api_login():
|
||||
auth = request.get_json()
|
||||
if not auth or not auth.get('username') or not auth.get('password'):
|
||||
return jsonify({'message': 'Missing credentials'}), 400
|
||||
|
||||
user = db.users.find_one({'username': auth['username']})
|
||||
if user and bcrypt.check_password_hash(user['password'], auth['password']):
|
||||
token = jwt.encode({
|
||||
'user_id': str(user['_id']),
|
||||
'exp': datetime.utcnow() + timedelta(days=30)
|
||||
}, app.config['SECRET_KEY'], algorithm="HS256")
|
||||
return jsonify({'token': token})
|
||||
|
||||
return jsonify({'message': 'Invalid credentials'}), 401
|
||||
|
||||
@app.route('/api/register', methods=['POST'])
|
||||
def api_register():
|
||||
data = request.get_json()
|
||||
if not data or not data.get('username') or not data.get('password'):
|
||||
return jsonify({'message': 'Missing credentials'}), 400
|
||||
|
||||
if db.users.find_one({'username': data['username']}):
|
||||
return jsonify({'message': 'Username already exists'}), 409
|
||||
|
||||
hashed_password = bcrypt.generate_password_hash(data['password']).decode('utf-8')
|
||||
user_id = db.users.insert_one({'username': data['username'], 'password': hashed_password}).inserted_id
|
||||
|
||||
# Auto login on register
|
||||
token = jwt.encode({
|
||||
'user_id': str(user_id),
|
||||
'exp': datetime.utcnow() + timedelta(days=30)
|
||||
}, app.config['SECRET_KEY'], algorithm="HS256")
|
||||
|
||||
return jsonify({'token': token})
|
||||
|
||||
# --- Dashboard / Status API ---
|
||||
|
||||
@app.route('/api/status', methods=['GET'])
|
||||
@token_required
|
||||
def api_status(user_id):
|
||||
# Get standard dashboard context
|
||||
current_entry, active_tasks = get_dashboard_context(user_id)
|
||||
activities = list(db.activities.find({'user_id': user_id}))
|
||||
|
||||
return jsonify({
|
||||
'is_tracking': current_entry is not None,
|
||||
'entry': {
|
||||
'id': str(current_entry['_id']),
|
||||
'activity_id': str(current_entry['activity_id']),
|
||||
'activity_name': current_entry.get('activity_name'),
|
||||
'activity_color': current_entry.get('activity_color'),
|
||||
'start_time': current_entry['start_time'].isoformat(),
|
||||
'subcategory': current_entry.get('subcategory', ''),
|
||||
'note': current_entry.get('note', '')
|
||||
} if current_entry else None,
|
||||
'active_tasks': [{
|
||||
'id': str(t['_id']),
|
||||
'name': t['name'],
|
||||
'status': t['status'],
|
||||
'completed_at': t['completed_at'].isoformat() if t.get('completed_at') else None
|
||||
} for t in active_tasks],
|
||||
'activities': [{
|
||||
'id': str(a['_id']),
|
||||
'name': a['name'],
|
||||
'color': a.get('color', '#3498db'),
|
||||
'subcategories': a.get('subcategories', [])
|
||||
} for a in activities]
|
||||
})
|
||||
|
||||
# --- Activity Management API ---
|
||||
|
||||
@app.route('/api/activities/create', methods=['POST'])
|
||||
@token_required
|
||||
def api_create_activity(user_id):
|
||||
data = request.get_json()
|
||||
name = data.get('name')
|
||||
if not name: return jsonify({'message': 'Name required'}), 400
|
||||
|
||||
activity_id = db.activities.insert_one({
|
||||
'user_id': user_id,
|
||||
'name': name,
|
||||
'color': data.get('color', '#3498db'),
|
||||
'subcategories': data.get('subcategories', [])
|
||||
}).inserted_id
|
||||
|
||||
# Optional default tasks
|
||||
tasks = data.get('default_tasks', [])
|
||||
for t in tasks:
|
||||
db.tasks.insert_one({
|
||||
'user_id': user_id,
|
||||
'name': t,
|
||||
'activity_id': ObjectId(activity_id),
|
||||
'is_template': True,
|
||||
'status': 'open',
|
||||
'time_entry_id': None,
|
||||
'created_at': datetime.now(),
|
||||
'comments': []
|
||||
})
|
||||
|
||||
return jsonify({'status': 'success', 'id': str(activity_id)})
|
||||
|
||||
@app.route('/api/activities/<activity_id>', methods=['PUT', 'DELETE'])
|
||||
@token_required
|
||||
def api_manage_activity(user_id, activity_id):
|
||||
if request.method == 'DELETE':
|
||||
# Optional: Check for confirmation or cascade delete logic
|
||||
result = db.activities.delete_one({'_id': ObjectId(activity_id), 'user_id': user_id})
|
||||
if result.deleted_count == 0: return jsonify({'message': 'Not found'}), 404
|
||||
return jsonify({'status': 'deleted'})
|
||||
|
||||
if request.method == 'PUT':
|
||||
data = request.get_json()
|
||||
update_fields = {}
|
||||
if 'name' in data: update_fields['name'] = data['name']
|
||||
if 'color' in data: update_fields['color'] = data['color']
|
||||
if 'subcategories' in data: update_fields['subcategories'] = data['subcategories']
|
||||
|
||||
db.activities.update_one(
|
||||
{'_id': ObjectId(activity_id), 'user_id': user_id},
|
||||
{'$set': update_fields}
|
||||
)
|
||||
return jsonify({'status': 'updated'})
|
||||
|
||||
# --- Task API ---
|
||||
|
||||
@app.route('/api/tasks', methods=['GET'])
|
||||
@token_required
|
||||
def api_tasks_list(user_id):
|
||||
# Retrieve filters from query params
|
||||
filter_status = request.args.get('status') # 'open', 'completed'
|
||||
|
||||
query = {'user_id': user_id, 'is_template': False}
|
||||
if filter_status:
|
||||
query['status'] = filter_status
|
||||
|
||||
tasks = list(db.tasks.find(query).sort('due_date', 1))
|
||||
|
||||
return jsonify([{
|
||||
'id': str(t['_id']),
|
||||
'name': t['name'],
|
||||
'status': t['status'],
|
||||
'activity_id': str(t.get('activity_id')) if t.get('activity_id') else None,
|
||||
'subcategory': t.get('subcategory', ''),
|
||||
'due_date': t['due_date'].isoformat() if t.get('due_date') else None,
|
||||
'completed_at': t['completed_at'].isoformat() if t.get('completed_at') else None
|
||||
} for t in tasks])
|
||||
|
||||
@app.route('/api/tasks/create', methods=['POST'])
|
||||
@token_required
|
||||
def api_create_task(user_id):
|
||||
data = request.get_json()
|
||||
if not data.get('name'): return jsonify({'message': 'Name required'}), 400
|
||||
|
||||
activity_id = data.get('activity_id')
|
||||
|
||||
# Check for active context if quick-add flag is present
|
||||
time_entry_id = None
|
||||
if data.get('link_to_active'):
|
||||
current_entry = db.time_entries.find_one({'user_id': user_id, 'end_time': None})
|
||||
if current_entry and (str(current_entry.get('activity_id')) == activity_id or not activity_id):
|
||||
time_entry_id = current_entry['_id']
|
||||
# If no activity specified but linking to active, verify contexts match or inherit?
|
||||
# Keeping it simple: link if requested.
|
||||
|
||||
task_doc = {
|
||||
'user_id': user_id,
|
||||
'name': data['name'],
|
||||
'status': 'open',
|
||||
'is_template': data.get('is_template', False),
|
||||
'source': 'api',
|
||||
'created_at': datetime.now(),
|
||||
'comments': [],
|
||||
'subcategory': data.get('subcategory', ''),
|
||||
'activity_id': ObjectId(activity_id) if activity_id else None,
|
||||
'time_entry_id': time_entry_id
|
||||
}
|
||||
|
||||
if data.get('due_date'):
|
||||
try:
|
||||
task_doc['due_date'] = datetime.fromisoformat(data['due_date'])
|
||||
except: pass
|
||||
|
||||
new_id = db.tasks.insert_one(task_doc).inserted_id
|
||||
return jsonify({'status': 'success', 'id': str(new_id)})
|
||||
|
||||
@app.route('/api/tasks/<task_id>', methods=['GET', 'PUT', 'DELETE'])
|
||||
@token_required
|
||||
def api_task_detail(user_id, task_id):
|
||||
if request.method == 'GET':
|
||||
t = db.tasks.find_one({'_id': ObjectId(task_id), 'user_id': user_id})
|
||||
if not t: return jsonify({'message': 'Not found'}), 404
|
||||
|
||||
# Format comments
|
||||
comments = []
|
||||
for c in t.get('comments', []):
|
||||
comments.append({
|
||||
'text': c['text'],
|
||||
'created_at': c['created_at'].isoformat()
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'id': str(t['_id']),
|
||||
'name': t['name'],
|
||||
'status': t['status'],
|
||||
'comments': comments
|
||||
# Add other fields as needed
|
||||
})
|
||||
|
||||
if request.method == 'DELETE':
|
||||
db.tasks.delete_one({'_id': ObjectId(task_id), 'user_id': user_id})
|
||||
return jsonify({'status': 'deleted'})
|
||||
|
||||
if request.method == 'PUT':
|
||||
data = request.get_json()
|
||||
# Handle simple updates or comments
|
||||
if 'comment' in data:
|
||||
db.tasks.update_one(
|
||||
{'_id': ObjectId(task_id), 'user_id': user_id},
|
||||
{'$push': {'comments': {
|
||||
'text': data['comment'],
|
||||
'created_at': datetime.now(),
|
||||
'user_id': user_id
|
||||
}}}
|
||||
)
|
||||
return jsonify({'status': 'comment_added'})
|
||||
|
||||
update_fields = {}
|
||||
if 'name' in data: update_fields['name'] = data['name']
|
||||
if 'status' in data:
|
||||
update_fields['status'] = data['status']
|
||||
if data['status'] == 'completed':
|
||||
update_fields['completed_at'] = datetime.now()
|
||||
else:
|
||||
update_fields['completed_at'] = None
|
||||
|
||||
db.tasks.update_one(
|
||||
{'_id': ObjectId(task_id), 'user_id': user_id},
|
||||
{'$set': update_fields}
|
||||
)
|
||||
return jsonify({'status': 'updated'})
|
||||
|
||||
# --- Logbook Detail / Edit API ---
|
||||
|
||||
@app.route('/api/logbook/<entry_id>', methods=['GET', 'PUT', 'DELETE'])
|
||||
@token_required
|
||||
def api_log_entry(user_id, entry_id):
|
||||
if request.method == 'DELETE':
|
||||
db.time_entries.delete_one({'_id': ObjectId(entry_id), 'user_id': user_id})
|
||||
return jsonify({'status': 'deleted'})
|
||||
|
||||
if request.method == 'PUT':
|
||||
data = request.get_json()
|
||||
update_fields = {}
|
||||
|
||||
if 'note' in data: update_fields['note'] = data['note']
|
||||
if 'subcategory' in data: update_fields['subcategory'] = data['subcategory']
|
||||
|
||||
try:
|
||||
if 'start_time' in data:
|
||||
# Expect ISO format
|
||||
update_fields['start_time'] = datetime.fromisoformat(data['start_time'].replace('Z', '+00:00'))
|
||||
if 'end_time' in data:
|
||||
# If explicit null passed, sets to running? Usually we set a time.
|
||||
if data['end_time']:
|
||||
update_fields['end_time'] = datetime.fromisoformat(data['end_time'].replace('Z', '+00:00'))
|
||||
except ValueError:
|
||||
return jsonify({'message': 'Invalid date format'}), 400
|
||||
|
||||
db.time_entries.update_one(
|
||||
{'_id': ObjectId(entry_id), 'user_id': user_id},
|
||||
{'$set': update_fields}
|
||||
)
|
||||
return jsonify({'status': 'updated'})
|
||||
|
||||
# GET detail
|
||||
entry = db.time_entries.find_one({'_id': ObjectId(entry_id), 'user_id': user_id})
|
||||
if not entry: return jsonify({'message': 'Not found'}), 404
|
||||
|
||||
# Get associated tasks
|
||||
tasks = list(db.tasks.find({'time_entry_id': ObjectId(entry_id)}))
|
||||
|
||||
return jsonify({
|
||||
'id': str(entry['_id']),
|
||||
'activity_id': str(entry['activity_id']),
|
||||
'start_time': entry['start_time'].isoformat(),
|
||||
'end_time': entry['end_time'].isoformat() if entry.get('end_time') else None,
|
||||
'note': entry.get('note', ''),
|
||||
'subcategory': entry.get('subcategory', ''),
|
||||
'duration_seconds': (entry['end_time'] - entry['start_time']).total_seconds() if entry.get('end_time') else None,
|
||||
'tasks': [{'id': str(t['_id']), 'name': t['name'], 'status': t['status']} for t in tasks]
|
||||
})
|
||||
|
||||
# --- Goals API ---
|
||||
|
||||
@app.route('/api/goals', methods=['GET', 'POST'])
|
||||
@token_required
|
||||
def api_goals(user_id):
|
||||
if request.method == 'GET':
|
||||
# Re-using heavy calculation logic from web route for API response
|
||||
goals_list = list(db.goals.find({'user_id': user_id}))
|
||||
|
||||
result_data = []
|
||||
today = datetime.now()
|
||||
start_of_today = today.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
ranges = {
|
||||
'daily': start_of_today,
|
||||
'weekly': start_of_today - timedelta(days=today.weekday()),
|
||||
'monthly': start_of_today.replace(day=1),
|
||||
'yearly': start_of_today.replace(month=1, day=1)
|
||||
}
|
||||
|
||||
for goal in goals_list:
|
||||
start_date = ranges.get(goal['frequency'], start_of_today)
|
||||
query = {
|
||||
'user_id': user_id,
|
||||
'start_time': {'$gte': start_date},
|
||||
'end_time': {'$ne': None}
|
||||
}
|
||||
if goal.get('activity_id'): query['activity_id'] = goal['activity_id']
|
||||
if goal.get('subcategory'): query['subcategory'] = goal['subcategory']
|
||||
|
||||
entries = list(db.time_entries.find(query))
|
||||
total_seconds = sum([(e['end_time'] - e['start_time']).total_seconds() for e in entries])
|
||||
current_hours = total_seconds / 3600
|
||||
|
||||
result_data.append({
|
||||
'id': str(goal['_id']),
|
||||
'name': goal['name'],
|
||||
'target_hours': goal['target_hours'],
|
||||
'current_hours': round(current_hours, 2),
|
||||
'percent': min(100, int((current_hours / goal['target_hours']) * 100)),
|
||||
'frequency': goal['frequency'],
|
||||
'activity_id': str(goal.get('activity_id')) if goal.get('activity_id') else None
|
||||
})
|
||||
|
||||
return jsonify(result_data)
|
||||
|
||||
if request.method == 'POST':
|
||||
data = request.get_json()
|
||||
if not data.get('activity_id'): return jsonify({'message': 'Activity required'}), 400
|
||||
|
||||
db.goals.insert_one({
|
||||
'user_id': user_id,
|
||||
'name': data.get('name', 'New Goal'),
|
||||
'target_hours': float(data.get('target_hours', 1)),
|
||||
'frequency': data.get('frequency', 'daily'),
|
||||
'activity_id': ObjectId(data['activity_id']),
|
||||
'subcategory': data.get('subcategory', ''),
|
||||
'created_at': datetime.now()
|
||||
})
|
||||
return jsonify({'status': 'created'})
|
||||
|
||||
@app.route('/api/goals/<goal_id>', methods=['DELETE'])
|
||||
@token_required
|
||||
def api_goal_delete(user_id, goal_id):
|
||||
db.goals.delete_one({'_id': ObjectId(goal_id), 'user_id': user_id})
|
||||
return jsonify({'status': 'deleted'})
|
||||
|
||||
debug = os.getenv('DEBUG') == 'False'
|
||||
host = os.getenv('HOST')
|
||||
port = int(os.getenv('PORT', 80))
|
||||
|
||||
Reference in New Issue
Block a user