# plugins/admin/routes.py from flask import ( Blueprint, render_template, redirect, url_for, flash, request, jsonify ) from flask_login import login_required, current_user from flask_wtf.csrf import generate_csrf from sqlalchemy import func, desc from datetime import datetime, timedelta from app import db from plugins.auth.models import User from plugins.plant.growlog.models import GrowLog from plugins.plant.models import Plant from plugins.admin.models import AnalyticsEvent from .forms import UserForm bp = Blueprint('admin', __name__, url_prefix='/admin', template_folder='templates') @bp.route('/dashboard') @login_required def dashboard(): if current_user.role != 'admin': return "Access denied", 403 now = datetime.utcnow() week_ago = now - timedelta(days=7) month_ago= now - timedelta(days=30) # ─── Overview metrics ────────────────────────────────────────────── # # Users total_users = User.query.count() active_users = User.query.filter( User.is_deleted == False, User.is_banned == False, (User.suspended_until == None) | (User.suspended_until <= now) ).count() suspended_users = User.query.filter(User.suspended_until > now).count() banned_users = User.query.filter_by(is_banned=True).count() deleted_users = User.query.filter_by(is_deleted=True).count() new_7d_users = User.query.filter(User.created_at >= week_ago).count() # Grow‐logs total_logs = GrowLog.query.count() logs_7d = GrowLog.query.filter(GrowLog.created_at >= week_ago).count() # Sign‐ups last 30 days signup_dates = [(month_ago + timedelta(days=i)).date() for i in range(31)] signup_counts = [ User.query.filter(func.date(User.created_at)==d).count() for d in signup_dates ] chart_dates = [d.strftime('%Y-%m-%d') for d in signup_dates] # Status distribution role_counts = { 'User': User.query.filter_by(role='user').count(), 'Admin': User.query.filter_by(role='admin').count() } # ─── Analytics aggregates ─────────────────────────────────────────── # ev_q = AnalyticsEvent.query.filter(AnalyticsEvent.timestamp >= week_ago) total_ev = ev_q.count() or 1 error_ev = ev_q.filter(AnalyticsEvent.status_code >= 500).count() error_pct = round(error_ev / total_ev * 100, 1) top_routes = [ {'path': path, 'count': cnt} for path, cnt in db.session.query( AnalyticsEvent.path, func.count(AnalyticsEvent.id) ) .filter(AnalyticsEvent.timestamp >= week_ago) .group_by(AnalyticsEvent.path) .order_by(desc(func.count(AnalyticsEvent.id))) .limit(10) .all() ] # Browser share browsers = {} ua_counts = ( db.session.query( func.substr(AnalyticsEvent.user_agent, 1, 200).label('ua'), func.count(AnalyticsEvent.id).label('cnt') ) .filter(AnalyticsEvent.user_agent.isnot(None)) .filter(AnalyticsEvent.timestamp >= week_ago) .group_by(func.substr(AnalyticsEvent.user_agent, 1, 200)) .all() ) for ua, cnt in ua_counts: b = 'Other' if 'Chrome' in ua: b = 'Chrome' elif 'Firefox' in ua: b = 'Firefox' elif 'Safari' in ua and 'Chrome' not in ua: b = 'Safari' elif 'Edge' in ua: b = 'Edge' browsers[b] = browsers.get(b, 0) + cnt referrers = dict( db.session.query( AnalyticsEvent.referer, func.count(AnalyticsEvent.id) ) .filter(AnalyticsEvent.referer.isnot(None)) .filter(AnalyticsEvent.timestamp >= week_ago) .group_by(AnalyticsEvent.referer) .order_by(desc(func.count(AnalyticsEvent.id))) .limit(5) .all() ) # ─── Stats metrics ────────────────────────────────────────────────── # # Plant totals total_plants = Plant.query.count() active_plants = Plant.query.filter_by(is_active=True).count() # Top 5 popular plant types popular_plants = db.session.query( Plant.plant_type, func.count(Plant.id).label('count') ).filter(Plant.is_active==True) \ .group_by(Plant.plant_type) \ .order_by(desc('count')) \ .limit(5) \ .all() # ─── Render template ──────────────────────────────────────────────── # return render_template( 'admin/dashboard.html', # Overview total_users=total_users, active_users=active_users, suspended_users=suspended_users, banned_users=banned_users, deleted_users=deleted_users, new_7d_users=new_7d_users, total_logs=total_logs, logs_7d=logs_7d, chart_dates=chart_dates, signup_counts=signup_counts, role_counts=role_counts, # Analytics error_pct=error_pct, top_routes=top_routes, browsers=browsers, referrers=referrers, # Stats total_plants=total_plants, active_plants=active_plants, popular_plants=popular_plants, ) @bp.route('/users') @login_required def list_users(): if current_user.role != 'admin': return "Access denied", 403 # --- parse query parameters --- page = request.args.get('page', 1, type=int) show_deleted = request.args.get('show_deleted', '0') == '1' q = request.args.get('q', '', type=str).strip() # --- build base query --- query = User.query if not show_deleted: query = query.filter_by(is_deleted=False) if q: like = f"%{q}%" query = query.filter(User.email.ilike(like)) # --- paginate the results --- pagination = query.order_by(User.created_at.desc()) \ .paginate(page=page, per_page=25, error_out=False) users = pagination.items # --- AJAX response (JSON) --- if request.headers.get('X-Requested-With') == 'XMLHttpRequest': token = generate_csrf() users_data = [] for u in users: if u.is_banned: status = 'banned' until = None elif u.suspended_until and u.suspended_until > datetime.utcnow(): status = 'suspended' until = u.suspended_until.strftime('%Y-%m-%d') else: status = 'active' until = None users_data.append({ 'id': u.id, 'email': u.email, 'role': u.role, 'is_verified': u.is_verified, 'excluded_from_analytics': u.excluded_from_analytics, 'status': status, 'suspended_until': until, 'is_deleted': u.is_deleted, 'created_at': u.created_at.strftime('%Y-%m-%d') }) return jsonify({ 'users': users_data, 'csrf_token': token }) # --- normal HTML response --- return render_template( 'admin/users/list.html', users=users, pagination=pagination, show_deleted=show_deleted, q=q ) @bp.route('/users/create', methods=['GET', 'POST']) @login_required def create_user(): if current_user.role != 'admin': return "Access denied", 403 form = UserForm() if form.validate_on_submit(): u = User( email=form.email.data, role=form.role.data, is_verified=form.is_verified.data, excluded_from_analytics=form.excluded_from_analytics.data ) # ban/suspend logic here if you need it... if form.password.data: u.set_password(form.password.data) db.session.add(u) db.session.commit() flash('User created.', 'success') return redirect(url_for('admin.list_users')) return render_template('admin/users/form.html', form=form, action='Create') @bp.route('/users//edit', methods=['GET', 'POST']) @login_required def edit_user(user_id): if current_user.role != 'admin': return "Access denied", 403 u = User.query.get_or_404(user_id) form = UserForm(obj=u) # Pre-populate on GET if request.method == 'GET': if u.is_banned: form.ban_type.data = 'perm' elif u.suspended_until and u.suspended_until > datetime.utcnow(): form.ban_type.data = 'temp' days = (u.suspended_until - datetime.utcnow()).days form.suspend_days.data = days if days > 0 else 1 else: form.ban_type.data = 'active' if form.validate_on_submit(): u.email = form.email.data u.role = form.role.data u.is_verified = form.is_verified.data u.excluded_from_analytics = form.excluded_from_analytics.data # Ban/Suspend logic if form.ban_type.data == 'perm': u.is_banned = True u.suspended_until = None elif form.ban_type.data == 'temp': u.is_banned = False days = form.suspend_days.data or 7 u.suspended_until = datetime.utcnow() + timedelta(days=days) else: u.is_banned = False u.suspended_until = None if form.password.data: u.set_password(form.password.data) db.session.commit() flash('User updated.', 'success') return redirect(url_for('admin.list_users', page=request.args.get('page'), show_deleted=request.args.get('show_deleted'), q=request.args.get('q'))) return render_template( 'admin/users/form.html', form=form, action='Edit' ) @bp.route('/users//delete', methods=['POST']) @login_required def delete_user(user_id): if current_user.role != 'admin': return "Access denied", 403 u = User.query.get_or_404(user_id) if u.id == current_user.id: flash("Cannot delete yourself.", 'warning') else: u.is_deleted = True db.session.commit() flash('User deleted (soft).', 'success') return redirect(url_for('admin.list_users', page=request.args.get('page'), show_deleted=request.args.get('show_deleted'), q=request.args.get('q'))) @bp.route('/users//undelete', methods=['POST']) @login_required def undelete_user(user_id): if current_user.role != 'admin': return "Access denied", 403 u = User.query.get_or_404(user_id) u.is_deleted = False db.session.commit() flash('User restored.', 'success') return redirect(url_for('admin.list_users', page=request.args.get('page'), show_deleted=request.args.get('show_deleted'), q=request.args.get('q')))