File size: 12,823 Bytes
9b44947
7644eac
 
 
 
 
 
 
84fc15b
9b44947
 
 
7644eac
84fc15b
 
 
7644eac
 
 
 
 
9b44947
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a5cfef0
 
9b44947
 
 
 
 
 
 
 
 
 
 
 
a5cfef0
9b44947
 
 
 
a5cfef0
9b44947
 
a5cfef0
9b44947
 
 
a5cfef0
9b44947
 
 
 
a5cfef0
9b44947
 
a5cfef0
9b44947
 
 
7644eac
 
 
171e588
7644eac
 
 
 
 
a5cfef0
 
7644eac
 
 
 
 
 
 
 
 
84fc15b
7644eac
 
 
 
 
 
 
 
 
 
 
 
84fc15b
b80dd92
 
a5cfef0
8113c49
 
84fc15b
 
a5cfef0
 
7644eac
 
 
 
 
 
 
171e588
7644eac
 
 
 
84fc15b
7644eac
 
 
 
 
a5cfef0
 
7644eac
 
 
 
a5cfef0
 
7644eac
 
 
 
 
84fc15b
7644eac
 
 
 
 
 
 
 
 
 
 
 
171e588
7644eac
171e588
7644eac
b80dd92
a5cfef0
8113c49
 
84fc15b
 
 
7644eac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b44947
 
7644eac
 
 
 
 
 
 
 
 
 
9b44947
7644eac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b44947
7644eac
9b44947
7644eac
 
9b44947
 
 
7644eac
 
 
 
 
 
 
 
 
9b44947
7644eac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, current_app
from flask_login import login_user, logout_user, login_required, current_user
# Assuming db and login_manager are initialized in __init__.py
from web_app import db, login_manager
from web_app.models import User
from web_app.auth_forms import LoginForm, RegistrationForm
import json
import random
import logging
import jwt
from datetime import datetime, timedelta
from functools import wraps

# Set up logging
logger = logging.getLogger(__name__)

# Define the blueprint
# If we later move this to an 'auth' subdirectory, the template_folder might change.
bp = Blueprint('auth', __name__, template_folder='templates/auth')


# ============================================================
# JWT TOKEN UTILITIES FOR MOBILE APP
# ============================================================

def generate_jwt_token(user_id, expires_in_days=30):
    """Generate a JWT token for mobile authentication"""
    payload = {
        'user_id': user_id,
        'exp': datetime.utcnow() + timedelta(days=expires_in_days),
        'iat': datetime.utcnow()
    }
    return jwt.encode(payload, current_app.config['SECRET_KEY'], algorithm='HS256')


def verify_jwt_token(token):
    """Verify a JWT token and return the user_id"""
    try:
        payload = jwt.decode(
            token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
        return payload.get('user_id')
    except jwt.ExpiredSignatureError:
        return None
    except jwt.InvalidTokenError:
        return None


def token_required(f):
    """Decorator to require JWT token authentication for API endpoints"""
    @wraps(f)
    def decorated(*args, **kwargs):
        token = None

        # Check for token in Authorization header
        auth_header = request.headers.get('Authorization')
        if auth_header and auth_header.startswith('Bearer '):
            token = auth_header.split(' ')[1]

        if not token:
            return jsonify({'error': 'Authentication token is missing'}), 401

        user_id = verify_jwt_token(token)
        if not user_id:
            return jsonify({'error': 'Invalid or expired token'}), 401

        # Get the user
        user = User.query.get(user_id)
        if not user:
            return jsonify({'error': 'User not found'}), 401

        # Pass the user to the route
        return f(user, *args, **kwargs)

    return decorated


@bp.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('main.dashboard'))

    form = RegistrationForm()

    # Handle form submission
    if form.validate_on_submit():
        logger.info(
            f"Registration form validated for email: {form.email.data}")
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)

        # Add registration metadata
        user.registration_source = 'email_password'
        user.last_seen = datetime.utcnow()

        db.session.add(user)
        db.session.commit()
        logger.info(f"User created: {user.email}")

        # Personalized welcome message
        welcome_messages = [
            f"Welcome to the community, {user.username}!",
            f"Your learning journey begins now, {user.username}!",
            f"Congratulations on joining, {user.username}!",
            f"You're all set to start learning, {user.username}!"
        ]
        flash(random.choice(welcome_messages), 'success')

        # Auto-login after registration for seamless experience
        login_user(user)
        logger.info(f"User logged in after registration: {user.email}")
        dashboard_url = url_for('main.dashboard')
        logger.info(f"Redirecting to dashboard: {dashboard_url}")

        # Use render template with meta refresh as fallback
        return render_template('redirect.html', redirect_url=dashboard_url)
    else:
        if form.is_submitted():
            logger.warning(
                f"Registration form validation failed: {form.errors}")

    return render_template('register.html', title='Join the Community', form=form)


@bp.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('main.dashboard'))

    form = LoginForm()

    if form.validate_on_submit():
        logger.info(f"Login form validated for email: {form.email.data}")
        user = User.query.filter_by(email=form.email.data).first()

        if user is None or not user.check_password(form.password.data):
            # Helpful error message
            if user is None:
                logger.warning(
                    f"Login failed: No user found for email {form.email.data}")
                flash(
                    'No account found with this email. Would you like to register?', 'warning')
                return redirect(url_for('auth.register', email=form.email.data))
            else:
                logger.warning(
                    f"Login failed: Wrong password for {form.email.data}")
                flash('Incorrect password. Please try again.', 'danger')
                return redirect(url_for('auth.login'))

        # Login successful
        login_user(user, remember=form.remember_me.data)
        logger.info(f"User logged in successfully: {user.email}")

        # Update last seen
        user.last_seen = datetime.utcnow()
        db.session.commit()

        # Personalized welcome back message
        greeting = form.get_greeting()
        motivation = form.get_motivation()
        flash(f"{greeting} {motivation}", 'success')

        # Redirect handling
        next_page = request.args.get('next')
        # Basic security check - default to dashboard if no next page or invalid
        if not next_page or not next_page.startswith('/'):
            next_page = url_for('main.dashboard')

        logger.info(f"Redirecting to: {next_page}")

        # Use render template with meta refresh as fallback
        return render_template('redirect.html', redirect_url=next_page)
    else:
        if form.is_submitted():
            logger.warning(f"Login form validation failed: {form.errors}")

    # Pre-fill email if coming from registration suggestion
    if request.args.get('email'):
        form.email.data = request.args.get('email')

    return render_template('login.html', title='Welcome Back', form=form)


@bp.route('/logout')
@login_required
def logout():
    username = current_user.username
    logout_user()

    # Friendly goodbye messages
    goodbye_messages = [
        f"See you soon, {username}!",
        f"Come back soon, {username}!",
        f"Your learning path will be waiting, {username}!",
        f"Taking a break? We'll be here when you return, {username}!"
    ]

    flash(random.choice(goodbye_messages), 'info')
    return redirect('/')

# This is needed by Flask-Login to load a user from the session


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))
# AJAX routes for enhanced user experience


@bp.route('/check-username', methods=['POST'])
def check_username():
    """Check if a username is available and suggest alternatives if not"""
    data = request.get_json(silent=True) or {}
    username = (data.get('username') or '').strip()

    if len(username) < 3:
        return jsonify({
            'available': False,
            'message': 'Username must be at least 3 characters long'
        })

    user = User.query.filter_by(username=username).first()
    if user is not None:
        base = username
        suggestions = [
            f"{base}{random.randint(1, 999)}",
            f"awesome_{base}",
            f"{base}_learner"
        ]

        return jsonify({
            'available': False,
            'message': 'This username is already taken',
            'suggestions': suggestions
        })

    return jsonify({
        'available': True,
        'message': 'Username is available!'
    })


# ============================================================
# JSON API ENDPOINTS FOR MOBILE APP
# ============================================================

@bp.route('/api/register', methods=['POST'])
def register_json():
    """JSON API endpoint for mobile app registration"""
    try:
        data = request.get_json(silent=True) or {}

        username = data.get('username', '').strip(
        ) or data.get('name', '').strip()
        email = data.get('email', '').strip()
        password = data.get('password', '').strip()
        password2 = data.get('password2', '').strip(
        ) or password  # If not provided, use password

        # Validation
        if not all([username, email, password]):
            return jsonify({'error': 'All fields are required'}), 400

        if len(username) < 3:
            return jsonify({'error': 'Username must be at least 3 characters'}), 400

        if '@' not in email or '.' not in email:
            return jsonify({'error': 'Invalid email format'}), 400

        if len(password) < 6:
            return jsonify({'error': 'Password must be at least 6 characters'}), 400

        if password != password2:
            return jsonify({'error': 'Passwords do not match'}), 400

        # Check if user exists
        if User.query.filter_by(username=username).first():
            return jsonify({'error': 'Username already exists'}), 409

        if User.query.filter_by(email=email).first():
            return jsonify({'error': 'Email already registered'}), 409

        # Create user
        user = User(username=username, email=email)
        user.set_password(password)
        user.registration_source = 'mobile'
        user.last_seen = datetime.utcnow()

        db.session.add(user)
        db.session.commit()

        # Generate JWT token for mobile authentication
        token = generate_jwt_token(user.id)

        return jsonify({
            'success': True,
            'message': 'Registration successful',
            'user': {
                'id': user.id,
                'username': user.username,
                'email': user.email,
                'display_name': user.display_name or user.username
            },
            'token': token
        }), 201

    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 500


@bp.route('/api/login', methods=['POST'])
def login_json():
    """JSON API endpoint for mobile app login"""
    try:
        data = request.get_json(silent=True) or {}

        email = data.get('email', '').strip()
        password = data.get('password', '').strip()

        if not email or not password:
            return jsonify({'error': 'Email and password are required'}), 400

        user = User.query.filter_by(email=email).first()

        if user is None or not user.check_password(password):
            return jsonify({'error': 'Invalid email or password'}), 401

        # Update last seen and login count
        user.last_seen = datetime.utcnow()
        user.login_count = (user.login_count or 0) + 1
        db.session.commit()

        # Generate JWT token for mobile authentication
        token = generate_jwt_token(user.id)

        return jsonify({
            'success': True,
            'message': 'Login successful',
            'user': {
                'id': user.id,
                'username': user.username,
                'email': user.email,
                'display_name': user.display_name or user.username
            },
            'token': token
        }), 200

    except Exception as e:
        return jsonify({'error': str(e)}), 500


@bp.route('/api/logout', methods=['POST'])
@login_required
def logout_json():
    """JSON API endpoint for mobile app logout"""
    try:
        logout_user()
        return jsonify({
            'success': True,
            'message': 'Logout successful'
        }), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500


@bp.route('/user', methods=['GET'])
@login_required
def get_user():
    """Get current user information - JSON endpoint for mobile"""
    try:
        user = current_user
        return jsonify({
            'success': True,
            'user': {
                'id': user.id,
                'username': user.username,
                'email': user.email,
                'display_name': user.display_name or user.username,
                'bio': user.bio or '',
                'created_at': user.date_created.isoformat() if user.date_created else None,
                'login_count': getattr(user, 'login_count', 0),
                'registration_source': user.registration_source or 'web'
            }
        }), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500