Yuyao Huang 3c325bdb0f feat(notes): add notes feature with CRUD operations and UI
- Implement notes database schema and API endpoints
- Add notes page with filtering, search, and markdown support
- Persist selected goal and task preferences for better UX
- Include responsive design and mobile-friendly layout
2026-05-08 17:42:42 +08:00

489 lines
15 KiB
Python

from flask import Flask, render_template, request, jsonify, session, redirect, url_for
from datetime import datetime
import database
import auth
import config
app = Flask(__name__)
app.secret_key = config.SECRET_KEY
database.init_db()
@app.route("/")
def index():
if "user_id" in session:
return redirect(url_for("goals_page"))
return redirect(url_for("login_page"))
@app.route("/login")
def login_page():
return render_template("login.html")
@app.route("/register")
def register_page():
return render_template("register.html")
@app.route("/goals")
@auth.login_required
def goals_page():
return render_template("goals.html")
@app.route("/tasks")
@auth.login_required
def tasks_page():
return render_template("tasks.html")
@app.route("/admin")
@auth.login_required
def admin_page():
user = auth.get_current_user()
if not user or user.get("role") != "admin":
return redirect(url_for("goals_page"))
return render_template("admin.html")
@app.route("/notes")
@auth.login_required
def notes_page():
return render_template("notes.html")
@app.route("/api/auth/register", methods=["POST"])
def api_register():
data = request.get_json()
username = data.get("username", "").strip()
password = data.get("password", "")
if not username or not password:
return jsonify({"success": False, "message": "Username and password are required"}), 400
if len(password) < 6:
return jsonify({"success": False, "message": "Password must be at least 6 characters"}), 400
if database.get_user_by_username(username):
return jsonify({"success": False, "message": "Username already exists"}), 400
password_hash = auth.hash_password(password)
user = database.create_user(username, password_hash)
session["user_id"] = user["id"]
session["username"] = username
return jsonify({
"success": True,
"message": "Registration successful",
"user_id": user["id"]
}), 201
@app.route("/api/auth/login", methods=["POST"])
def api_login():
data = request.get_json()
username = data.get("username", "").strip()
password = data.get("password", "")
if not username or not password:
return jsonify({"success": False, "message": "Username and password are required"}), 400
user = database.get_user_by_username(username)
if not user or not auth.check_password(password, user["password_hash"]):
return jsonify({"success": False, "message": "Invalid credentials"}), 401
session["user_id"] = user["id"]
session["username"] = username
return jsonify({
"success": True,
"message": "Login successful",
"user": {
"user_id": user["id"],
"username": user["username"],
"role": user["role"],
"max_goals": user["max_goals"],
"selected_goal_id": user.get("selected_goal_id")
}
})
@app.route("/api/auth/logout", methods=["POST"])
@auth.login_required
def api_logout():
session.clear()
return jsonify({"success": True, "message": "Logged out"})
@app.route("/api/auth/me", methods=["GET"])
@auth.login_required
def api_me():
user = auth.get_current_user()
if not user:
return jsonify({"success": False, "message": "User not found"}), 404
return jsonify({
"user_id": user["id"],
"username": user["username"],
"role": user["role"],
"max_goals": user["max_goals"],
"selected_goal_id": user.get("selected_goal_id")
})
@app.route("/api/user/selected-goal", methods=["PATCH"])
@auth.login_required
def api_set_selected_goal():
data = request.get_json()
goal_id = data.get("goal_id")
database.update_user(session["user_id"], selected_goal_id=goal_id)
return jsonify({"success": True, "selected_goal_id": goal_id})
@app.route("/api/goals", methods=["GET"])
@auth.login_required
def api_get_goals():
user_id = session["user_id"]
goals = database.get_goals_by_user(user_id)
result = []
for goal in goals:
goal_dict = dict(goal)
goal_dict["id"] = goal["id"]
result.append(goal_dict)
return jsonify(result)
@app.route("/api/goals", methods=["POST"])
@auth.login_required
def api_create_goal():
data = request.get_json()
title = data.get("title", "").strip()
if not title:
return jsonify({"success": False, "message": "Title is required"}), 400
user_id = session["user_id"]
user = database.get_user_by_id(user_id)
if database.count_goals_by_user(user_id) >= user["max_goals"]:
return jsonify({
"success": False,
"message": f"Goal limit reached ({user['max_goals']})"
}), 400
goal = database.create_goal(user_id, title)
return jsonify({"success": True, "goal_id": goal["id"]}), 201
@app.route("/api/goals/<int:goal_id>", methods=["PUT"])
@auth.login_required
def api_update_goal(goal_id):
goal = database.get_goal_by_id(goal_id)
if not goal or goal["user_id"] != session["user_id"]:
return jsonify({"success": False, "message": "Goal not found"}), 404
data = request.get_json()
updates = {}
if "title" in data:
title = data["title"].strip()
if not title:
return jsonify({"success": False, "message": "Title cannot be empty"}), 400
updates["title"] = title
if "activated" in data:
updates["activated"] = bool(data["activated"])
database.update_goal(goal_id, **updates)
return jsonify({"success": True})
@app.route("/api/goals/<int:goal_id>", methods=["DELETE"])
@auth.login_required
def api_delete_goal(goal_id):
goal = database.get_goal_by_id(goal_id)
if not goal or goal["user_id"] != session["user_id"]:
return jsonify({"success": False, "message": "Goal not found"}), 404
database.delete_goal(goal_id)
return jsonify({"success": True})
@app.route("/api/goals/<int:goal_id>/toggle", methods=["PATCH"])
@auth.login_required
def api_toggle_goal(goal_id):
goal = database.get_goal_by_id(goal_id)
if not goal or goal["user_id"] != session["user_id"]:
return jsonify({"success": False, "message": "Goal not found"}), 404
new_status = not goal["activated"]
database.update_goal(goal_id, activated=new_status)
return jsonify({"success": True, "activated": new_status})
@app.route("/api/goals/<int:goal_id>/selected-task", methods=["PATCH"])
@auth.login_required
def api_set_selected_task(goal_id):
goal = database.get_goal_by_id(goal_id)
if not goal or goal["user_id"] != session["user_id"]:
return jsonify({"success": False, "message": "Goal not found"}), 404
data = request.get_json()
task_id = data.get("task_id")
database.update_goal(goal_id, selected_task_id=task_id)
return jsonify({"success": True, "selected_task_id": task_id})
@app.route("/api/tasks", methods=["GET"])
@auth.login_required
def api_get_tasks():
goal_id = request.args.get("goal_id", type=int)
if not goal_id:
return jsonify([])
goal = database.get_goal_by_id(goal_id)
if not goal or goal["user_id"] != session["user_id"]:
return jsonify({"success": False, "message": "Goal not found"}), 404
tasks = database.get_tasks_sorted(goal_id)
result = []
for task in tasks:
task_dict = dict(task)
task_dict["id"] = task["id"]
result.append(task_dict)
return jsonify(result)
@app.route("/api/tasks", methods=["POST"])
@auth.login_required
def api_create_task():
data = request.get_json()
goal_id = data.get("goal_id")
title = data.get("title", "").strip()
desc = data.get("desc", "").strip()
if not goal_id or not title:
return jsonify({"success": False, "message": "Goal ID and title are required"}), 400
goal = database.get_goal_by_id(goal_id)
if not goal or goal["user_id"] != session["user_id"]:
return jsonify({"success": False, "message": "Goal not found"}), 404
if not goal["activated"]:
return jsonify({"success": False, "message": "Cannot add task to deactivated goal"}), 400
task = database.create_task(goal_id, title, desc)
return jsonify({"success": True, "task_id": task["id"]}), 201
@app.route("/api/tasks/<int:task_id>", methods=["PUT"])
@auth.login_required
def api_update_task(task_id):
task = database.get_task_by_id(task_id)
if not task:
return jsonify({"success": False, "message": "Task not found"}), 404
goal = database.get_goal_by_id(task["goal_id"])
if goal["user_id"] != session["user_id"]:
return jsonify({"success": False, "message": "Task not found"}), 404
data = request.get_json()
updates = {}
if "title" in data:
title = data["title"].strip()
if not title:
return jsonify({"success": False, "message": "Title cannot be empty"}), 400
updates["title"] = title
if "desc" in data:
updates["desc"] = data["desc"]
database.update_task(task_id, **updates)
return jsonify({"success": True})
@app.route("/api/tasks/<int:task_id>", methods=["DELETE"])
@auth.login_required
def api_delete_task(task_id):
task = database.get_task_by_id(task_id)
if not task:
return jsonify({"success": False, "message": "Task not found"}), 404
goal = database.get_goal_by_id(task["goal_id"])
if goal["user_id"] != session["user_id"]:
return jsonify({"success": False, "message": "Task not found"}), 404
database.delete_task(task_id)
return jsonify({"success": True})
@app.route("/api/tasks/<int:task_id>/status", methods=["PATCH"])
@auth.login_required
def api_update_task_status(task_id):
task = database.get_task_by_id(task_id)
if not task:
return jsonify({"success": False, "message": "Task not found"}), 404
goal = database.get_goal_by_id(task["goal_id"])
if goal["user_id"] != session["user_id"]:
return jsonify({"success": False, "message": "Task not found"}), 404
data = request.get_json()
new_status = data.get("status")
if new_status not in ["todo", "doing", "pending", "done"]:
return jsonify({"success": False, "message": "Invalid status"}), 400
updates = {"status": new_status}
if new_status == "doing":
doing_task = None
all_tasks = database.get_tasks_by_goal(task["goal_id"])
for t in all_tasks:
if t.get("status") == "doing" and t["id"] != task["id"]:
doing_task = t
break
if doing_task:
database.update_task(doing_task["id"], status="todo")
updates["start_time"] = datetime.now().isoformat()
else:
updates["start_time"] = None
if new_status == "done":
updates["finished_time"] = datetime.now().isoformat()
else:
updates["finished_time"] = None
database.update_task(task_id, **updates)
return jsonify({"success": True})
@app.route("/api/tasks/<int:task_id>/order", methods=["PATCH"])
@auth.login_required
def api_update_task_order(task_id):
task = database.get_task_by_id(task_id)
if not task:
return jsonify({"success": False, "message": "Task not found"}), 404
goal = database.get_goal_by_id(task["goal_id"])
if goal["user_id"] != session["user_id"]:
return jsonify({"success": False, "message": "Task not found"}), 404
data = request.get_json()
new_order = data.get("order")
if new_order is None:
return jsonify({"success": False, "message": "Order value required"}), 400
database.update_task(task_id, order=float(new_order))
return jsonify({"success": True})
@app.route("/api/notes", methods=["GET"])
@auth.login_required
def api_get_notes():
goal_id = request.args.get("goal_id", type=int)
search = request.args.get("search", "").strip() or None
notes = database.get_notes(session["user_id"], goal_id, search)
return jsonify(notes)
@app.route("/api/notes", methods=["POST"])
@auth.login_required
def api_create_note():
data = request.get_json()
title = data.get("title", "").strip()
if not title:
return jsonify({"success": False, "message": "Title is required"}), 400
goal_id = data.get("goal_id")
task_id = data.get("task_id")
content = data.get("content", "")
note = database.create_note(session["user_id"], goal_id, task_id, title, content)
return jsonify(note), 201
@app.route("/api/notes/<int:note_id>", methods=["GET"])
@auth.login_required
def api_get_note(note_id):
note = database.get_note_by_id(note_id)
if not note or note["user_id"] != session["user_id"]:
return jsonify({"success": False, "message": "Note not found"}), 404
return jsonify(note)
@app.route("/api/notes/<int:note_id>", methods=["PUT"])
@auth.login_required
def api_update_note(note_id):
note = database.get_note_by_id(note_id)
if not note or note["user_id"] != session["user_id"]:
return jsonify({"success": False, "message": "Note not found"}), 404
data = request.get_json()
title = data.get("title", "").strip()
if not title:
return jsonify({"success": False, "message": "Title is required"}), 400
updated = database.update_note(note_id, title, data.get("content", ""))
return jsonify(updated)
@app.route("/api/notes/<int:note_id>", methods=["DELETE"])
@auth.login_required
def api_delete_note(note_id):
note = database.get_note_by_id(note_id)
if not note or note["user_id"] != session["user_id"]:
return jsonify({"success": False, "message": "Note not found"}), 404
database.delete_note(note_id)
return jsonify({"success": True})
@app.route("/api/admin/users", methods=["GET"])
@auth.admin_required
def api_get_users():
users = database.get_all_users()
result = []
for user in users:
result.append({
"user_id": user["id"],
"username": user["username"],
"role": user["role"],
"max_goals": user["max_goals"]
})
return jsonify(result)
@app.route("/api/admin/users/<int:user_id>", methods=["PUT"])
@auth.admin_required
def api_update_user(user_id):
user = database.get_user_by_id(user_id)
if not user:
return jsonify({"success": False, "message": "User not found"}), 404
data = request.get_json()
updates = {}
if "max_goals" in data:
max_goals = data["max_goals"]
if not isinstance(max_goals, int) or max_goals < 1:
return jsonify({"success": False, "message": "Invalid max_goals value"}), 400
updates["max_goals"] = max_goals
if "role" in data:
role = data["role"]
if role not in ["user", "admin"]:
return jsonify({"success": False, "message": "Invalid role"}), 400
updates["role"] = role
database.update_user(user_id, **updates)
return jsonify({"success": True})
if __name__ == "__main__":
app.run(host=config.HOST, port=config.PORT, debug=config.DEBUG)