diff --git a/Forum/app.py b/Forum/app.py
index 7b03ad2b..ebdcd58c 100644
--- a/Forum/app.py
+++ b/Forum/app.py
@@ -1,5 +1,4 @@
from flask import Flask, request, jsonify
-from auth_utils import token_required, roles_required
from flask_cors import CORS
import re
from functools import wraps
@@ -48,14 +47,9 @@ def validate_post_data(data):
forum_posts = []
@app.route('/forum', methods=['GET', 'POST'])
-@token_required
def forum_api():
try:
if request.method == 'POST':
- # Only allow farmers and admins to create posts
- user = getattr(request, 'user', {})
- if user.get('role') not in ['farmer', 'admin']:
- return jsonify({'error': 'Insufficient permissions'}), 403
# Validate content type
if not request.is_json:
return jsonify({'error': 'Content-Type must be application/json'}), 400
@@ -80,7 +74,11 @@ def forum_api():
# Add to posts (in a real app, this would go to a database)
forum_posts.append(sanitized_data)
- return jsonify({"status": "success", "message": "Post created successfully"}), 201
+ return jsonify({
+ "status": "success",
+ "message": "Post created successfully",
+ "post": sanitized_data
+ }), 201
else:
return jsonify(forum_posts)
diff --git a/Forum/forum.js b/Forum/forum.js
index e4f20cf8..c219c7aa 100644
--- a/Forum/forum.js
+++ b/Forum/forum.js
@@ -25,10 +25,13 @@ document.getElementById('forumForm').addEventListener('submit', function (e) {
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
})
- .then(() => {
- loadPosts();
+ .then(data => {
e.target.reset();
- // Show simple success feedback (optional)
+ if (data && data.post) {
+ prependPost(data.post);
+ } else {
+ loadPosts();
+ }
alert('Post created successfully!');
})
.catch(err => {
@@ -92,6 +95,40 @@ function loadPosts() {
});
}
+function prependPost(post) {
+ const container = document.getElementById('forumPosts');
+ if (!container) return;
+
+ const emptyState = container.querySelector('.empty-state-card');
+ if (emptyState) {
+ emptyState.remove();
+ }
+
+ const el = document.createElement('div');
+ el.className = 'forum-post';
+
+ const title = post.title || 'Untitled Discussion';
+ const author = post.author || 'Anonymous';
+ const content = post.content || '';
+
+ el.innerHTML = `
+
+
+
+
+
${escapeHtml(author)}
+
•
+
Just now
+
+ ${escapeHtml(title)}
+
+ ${escapeHtml(content)}
+
+ `;
+
+ container.prepend(el);
+}
+
// Helper to prevent XSS
function escapeHtml(text) {
if (!text) return '';
diff --git a/app.py b/app.py
index cbee1c53..de4176c8 100644
--- a/app.py
+++ b/app.py
@@ -33,6 +33,8 @@
import backend.sockets.alert_socket # Register centralized alert socket events
import backend.sockets.crisis_events # Register crisis monitoring events
from backend.utils.i18n import t
+from backend.auth import auth_bp as legacy_auth_bp
+from backend.docs.swagger import swagger_bp
from routes.irrigation_routes import irrigation_bp
@@ -86,6 +88,8 @@
app.register_blueprint(model_versioning_bp)
app.register_blueprint(irrigation_bp)
app.register_blueprint(rotation_bp)
+app.register_blueprint(legacy_auth_bp)
+app.register_blueprint(swagger_bp)
# Register API v1 (including loan, weather, schemes, etc.)
register_api(app)
diff --git a/backend/api/v1/__init__.py b/backend/api/v1/__init__.py
index 15fe415e..33ba0f41 100644
--- a/backend/api/v1/__init__.py
+++ b/backend/api/v1/__init__.py
@@ -1,5 +1,6 @@
from flask import Blueprint
from .loan import loan_bp
+from .auth import auth_bp
from .config import config_bp
from .tasks import tasks_bp
from .notifications import notifications_bp
@@ -63,6 +64,7 @@
# Register sub-blueprints
api_v1.register_blueprint(loan_bp)
+api_v1.register_blueprint(auth_bp, url_prefix="/auth")
api_v1.register_blueprint(config_bp)
api_v1.register_blueprint(tasks_bp)
api_v1.register_blueprint(notifications_bp)
diff --git a/backend/api/v1/auth.py b/backend/api/v1/auth.py
index 6867589a..2cef7b83 100644
--- a/backend/api/v1/auth.py
+++ b/backend/api/v1/auth.py
@@ -3,10 +3,37 @@
from backend.services.audit_service import AuditService
from backend.models import User
from backend.extensions import db, limiter
+from backend.docs.swagger import swagger_operation
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/register', methods=['POST'])
+@swagger_operation(
+ '/api/v1/auth/register',
+ 'post',
+ 'Register a user and send a verification email',
+ 'Create a new user account, then send a verification email.',
+ request_body={
+ 'required': True,
+ 'content': {
+ 'application/json': {
+ 'schema': {
+ 'type': 'object',
+ 'required': ['username', 'email', 'password'],
+ 'properties': {
+ 'username': {'type': 'string', 'example': 'Farmer Name'},
+ 'email': {'type': 'string', 'format': 'email', 'example': 'farmer@gmail.com'},
+ 'password': {'type': 'string', 'example': 'SecurePass123'},
+ },
+ },
+ },
+ },
+ },
+ responses={
+ '201': {'description': 'User registered successfully'},
+ '400': {'description': 'Validation error'},
+ },
+)
@limiter.limit("5 per hour")
def register():
"""Register a new user and send verification email."""
@@ -50,6 +77,16 @@ def register():
}), 201
@auth_bp.route('/verify-email/', methods=['GET'])
+@swagger_operation(
+ '/api/v1/auth/verify-email/{token}',
+ 'get',
+ 'Verify email address',
+ 'Verify a user email address using the emailed token.',
+ responses={
+ '200': {'description': 'Email verified successfully'},
+ '400': {'description': 'Verification token invalid or expired'},
+ },
+)
def verify_email(token):
"""Verify email endpoint."""
success, message = AuthService.verify_email(token)
@@ -60,6 +97,30 @@ def verify_email(token):
return jsonify({'status': 'error', 'message': message}), 400
@auth_bp.route('/forgot-password', methods=['POST'])
+@swagger_operation(
+ '/api/v1/auth/forgot-password',
+ 'post',
+ 'Request a password reset',
+ 'Send a password reset email without revealing whether the address exists.',
+ request_body={
+ 'required': True,
+ 'content': {
+ 'application/json': {
+ 'schema': {
+ 'type': 'object',
+ 'required': ['email'],
+ 'properties': {
+ 'email': {'type': 'string', 'format': 'email', 'example': 'farmer@gmail.com'},
+ },
+ },
+ },
+ },
+ },
+ responses={
+ '200': {'description': 'Password reset email queued'},
+ '400': {'description': 'Missing email'},
+ },
+)
@limiter.limit("3 per hour")
def forgot_password():
"""Request password reset email."""
@@ -83,6 +144,30 @@ def forgot_password():
}), 200
@auth_bp.route('/reset-password/', methods=['POST'])
+@swagger_operation(
+ '/api/v1/auth/reset-password/{token}',
+ 'post',
+ 'Reset password with token',
+ 'Set a new password using the reset token.',
+ request_body={
+ 'required': True,
+ 'content': {
+ 'application/json': {
+ 'schema': {
+ 'type': 'object',
+ 'required': ['password'],
+ 'properties': {
+ 'password': {'type': 'string', 'example': 'NewSecurePass123'},
+ },
+ },
+ },
+ },
+ },
+ responses={
+ '200': {'description': 'Password reset successfully'},
+ '400': {'description': 'Invalid token or password'},
+ },
+)
@limiter.limit("5 per hour")
def reset_password(token):
"""Reset password using token."""
diff --git a/backend/auth/routes.py b/backend/auth/routes.py
index b09c2a48..68d97760 100644
--- a/backend/auth/routes.py
+++ b/backend/auth/routes.py
@@ -6,6 +6,7 @@
from backend.models import User
from .jwt_utils import jwt_manager
from .decorators import token_required
+from backend.docs.swagger import swagger_operation
from backend.utils.validation import (
sanitize_input,
validate_email,
@@ -22,6 +23,37 @@
@auth_bp.route('/register', methods=['POST'])
+@swagger_operation(
+ '/api/auth/register',
+ 'post',
+ 'Register a user',
+ 'Create a new user account and return the created profile.',
+ request_body={
+ 'required': True,
+ 'content': {
+ 'application/json': {
+ 'schema': {
+ 'type': 'object',
+ 'required': ['username', 'email', 'password', 'full_name', 'role'],
+ 'properties': {
+ 'username': {'type': 'string', 'example': 'farmer123'},
+ 'email': {'type': 'string', 'format': 'email', 'example': 'farmer@example.com'},
+ 'password': {'type': 'string', 'example': 'SecurePass123'},
+ 'full_name': {'type': 'string', 'example': 'John Doe'},
+ 'role': {'type': 'string', 'example': 'farmer'},
+ 'phone': {'type': 'string', 'example': '9876543210'},
+ 'location': {'type': 'string', 'example': 'Maharashtra'},
+ },
+ },
+ },
+ },
+ },
+ responses={
+ '201': {'description': 'User created successfully'},
+ '400': {'description': 'Validation error'},
+ '409': {'description': 'User already exists'},
+ },
+)
def register():
"""
Register a new user.
@@ -125,6 +157,32 @@ def register():
@auth_bp.route('/login', methods=['POST'])
+@swagger_operation(
+ '/api/auth/login',
+ 'post',
+ 'Login a user',
+ 'Authenticate a user and return an access token plus the user profile.',
+ request_body={
+ 'required': True,
+ 'content': {
+ 'application/json': {
+ 'schema': {
+ 'type': 'object',
+ 'required': ['username', 'password'],
+ 'properties': {
+ 'username': {'type': 'string', 'example': 'farmer123'},
+ 'password': {'type': 'string', 'example': 'SecurePass123'},
+ },
+ },
+ },
+ },
+ },
+ responses={
+ '200': {'description': 'Login successful'},
+ '400': {'description': 'Validation error'},
+ '401': {'description': 'Invalid credentials'},
+ },
+)
def login():
"""
Authenticate user and return tokens.
@@ -211,6 +269,16 @@ def login():
@auth_bp.route('/refresh', methods=['POST'])
+@swagger_operation(
+ '/api/auth/refresh',
+ 'post',
+ 'Refresh access token',
+ 'Exchange a valid refresh token cookie for a new access token.',
+ responses={
+ '200': {'description': 'Token refreshed successfully'},
+ '401': {'description': 'Refresh token missing or invalid'},
+ },
+)
def refresh_token():
"""
Refresh access token using refresh token from cookie.
@@ -266,6 +334,17 @@ def refresh_token():
@auth_bp.route('/logout', methods=['POST'])
+@swagger_operation(
+ '/api/auth/logout',
+ 'post',
+ 'Logout the current user',
+ 'Invalidate the current session by clearing the refresh token cookie.',
+ security=[{'bearerAuth': []}],
+ responses={
+ '200': {'description': 'Logout successful'},
+ '401': {'description': 'Authentication required'},
+ },
+)
@token_required
def logout(current_user):
"""
@@ -293,6 +372,18 @@ def logout(current_user):
@auth_bp.route('/me', methods=['GET'])
+@swagger_operation(
+ '/api/auth/me',
+ 'get',
+ 'Get current user',
+ 'Return the authenticated user profile.',
+ security=[{'bearerAuth': []}],
+ responses={
+ '200': {'description': 'User information returned successfully'},
+ '401': {'description': 'Authentication required'},
+ '404': {'description': 'User not found'},
+ },
+)
@token_required
def get_current_user(current_user):
"""
@@ -316,6 +407,17 @@ def get_current_user(current_user):
@auth_bp.route('/validate', methods=['GET'])
+@swagger_operation(
+ '/api/auth/validate',
+ 'get',
+ 'Validate access token',
+ 'Check whether the access token is still valid.',
+ security=[{'bearerAuth': []}],
+ responses={
+ '200': {'description': 'Token is valid'},
+ '401': {'description': 'Authentication required'},
+ },
+)
@token_required
def validate_token(current_user):
"""
diff --git a/backend/docs/swagger.py b/backend/docs/swagger.py
new file mode 100644
index 00000000..dc467ce4
--- /dev/null
+++ b/backend/docs/swagger.py
@@ -0,0 +1,137 @@
+from flask import Blueprint, jsonify, Response
+
+
+swagger_bp = Blueprint("swagger_docs", __name__, url_prefix="/api/docs")
+
+_REGISTRY = []
+
+
+def swagger_operation(path, method, summary, description, *, request_body=None, responses=None, tags=None, security=None):
+ """Attach OpenAPI metadata to a view function."""
+
+ def decorator(func):
+ _REGISTRY.append(
+ {
+ "path": path,
+ "method": method.lower(),
+ "summary": summary,
+ "description": description,
+ "request_body": request_body,
+ "responses": responses or {},
+ "tags": tags or ["Authentication"],
+ "security": security,
+ }
+ )
+ return func
+
+ return decorator
+
+
+def _build_spec():
+ paths = {}
+
+ for entry in _REGISTRY:
+ operation = {
+ "summary": entry["summary"],
+ "description": entry["description"],
+ "tags": entry["tags"],
+ "responses": entry["responses"],
+ }
+
+ if entry["request_body"]:
+ operation["requestBody"] = entry["request_body"]
+
+ if entry["security"] is not None:
+ operation["security"] = entry["security"]
+
+ paths.setdefault(entry["path"], {})[entry["method"]] = operation
+
+ return {
+ "openapi": "3.0.3",
+ "info": {
+ "title": "AgriTech Authentication API",
+ "version": "1.0.0",
+ "description": "OpenAPI documentation for the AgriTech authentication endpoints.",
+ },
+ "servers": [
+ {
+ "url": "http://localhost:5000",
+ "description": "Local development server",
+ }
+ ],
+ "tags": [
+ {
+ "name": "Authentication",
+ "description": "User registration, login, session management, and password recovery.",
+ }
+ ],
+ "components": {
+ "securitySchemes": {
+ "bearerAuth": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT",
+ }
+ }
+ },
+ "paths": paths,
+ }
+
+
+@swagger_bp.route("/openapi.json", methods=["GET"])
+def openapi_json():
+ return jsonify(_build_spec())
+
+
+@swagger_bp.route("/", methods=["GET"])
+def swagger_ui():
+ html = """
+
+
+
+
+
+ AgriTech API Docs
+
+
+
+
+
+
+
+
+
+
+"""
+ return Response(html, mimetype="text/html")