Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions Forum/app.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down
43 changes: 40 additions & 3 deletions Forum/forum.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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 = `
<div class="post-meta">
<div class="author-avatar">
<i class="fas fa-user"></i>
</div>
<strong>${escapeHtml(author)}</strong>
<span>•</span>
<span>Just now</span>
</div>
<h3 class="post-title">${escapeHtml(title)}</h3>
<div class="post-content">
${escapeHtml(content)}
</div>
`;

container.prepend(el);
}

// Helper to prevent XSS
function escapeHtml(text) {
if (!text) return '';
Expand Down
4 changes: 4 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions backend/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down
85 changes: 85 additions & 0 deletions backend/api/v1/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -50,6 +77,16 @@ def register():
}), 201

@auth_bp.route('/verify-email/<token>', 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)
Expand All @@ -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."""
Expand All @@ -83,6 +144,30 @@ def forgot_password():
}), 200

@auth_bp.route('/reset-password/<token>', 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."""
Expand Down
102 changes: 102 additions & 0 deletions backend/auth/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand All @@ -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):
"""
Expand Down
Loading
Loading