File size: 11,175 Bytes
c001f24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# Pre-Migration Report: Single-User to Multi-User Architecture

This document outlines the necessary changes to migrate the DocuPDF application from a single-user to a multi-user architecture. The migration is designed to be completed in phases, ensuring that existing data is preserved and correctly associated with the primary user.

---

## Phase 1: User Authentication Foundation

This phase introduces the core concepts of users and authentication.

### 1.1 New `users` Table

A new table will be created to store user credentials.

```sql
-- file: database.py (addition)
CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT NOT NULL UNIQUE,
    email TEXT NOT NULL UNIQUE,
    password_hash TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```

### 1.2 New File: `user_manager.py`

A new file will handle user session management, password hashing, and provide the user model required by Flask-Login.

```python
# file: user_manager.py (new file)
from flask_login import LoginManager, UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from utils import get_db_connection

class User(UserMixin):
    def __init__(self, id, username, email, password_hash):
        self.id = id
        self.username = username
        self.email = email
        self.password_hash = password_hash

    @staticmethod
    def get(user_id):
        conn = get_db_connection()
        user_row = conn.execute('SELECT * FROM users WHERE id = ?', (user_id,)).fetchone()
        conn.close()
        if user_row:
            return User(user_row['id'], user_row['username'], user_row['email'], user_row['password_hash'])
        return None

    @staticmethod
    def get_by_username(username):
        conn = get_db_connection()
        user_row = conn.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone()
        conn.close()
        if user_row:
            return User(user_row['id'], user_row['username'], user_row['email'], user_row['password_hash'])
        return None

def setup_login_manager(app):
    login_manager = LoginManager()
    login_manager.init_app(app)
    login_manager.login_view = 'user_auth.login' # New blueprint for user auth

    @login_manager.user_loader
    def load_user(user_id):
        return User.get(user_id)

# (Additional functions for creating users, etc. will be added here)
```

### 1.3 Application Setup (`app.py`)

The main `app.py` will be updated to initialize the `LoginManager` and register the new authentication blueprint.

```python
# file: app.py (changes)
# Current
def create_app():
    app = Flask(__name__)
    # ...
    # Register Blueprints
    from routes import main_bp
    # ...
    app.register_blueprint(main_bp)
    return app

# After
from flask_login import LoginManager

def create_app():
    app = Flask(__name__)
    app.config['SECRET_KEY'] = os.urandom(24) # Important for session security
    # ...
    
    # Setup Login Manager
    from user_manager import setup_login_manager
    setup_login_manager(app)

    # Register Blueprints
    from routes import main_bp
    from user_auth_routes import auth_bp # New blueprint for login/register
    # ...
    app.register_blueprint(main_bp)
    app.register_blueprint(auth_bp)
    return app
```

### 1.4 New Templates: `login.html` and `register.html`

New HTML templates will be created for the user login and registration forms. These will be standard forms with fields for username, password, and email.

---

## Phase 2: Database and Data Segregation

This phase links all application data to specific users.

### 2.1 Database Schema Changes

The following tables will be altered to include a `user_id` foreign key.

```sql
-- file: database.py (migrations)

-- Add user_id to sessions
ALTER TABLE sessions ADD COLUMN user_id INTEGER REFERENCES users(id);

-- Add user_id to generated_pdfs
ALTER TABLE generated_pdfs ADD COLUMN user_id INTEGER REFERENCES users(id);

-- Add user_id to folders
ALTER TABLE folders ADD COLUMN user_id INTEGER REFERENCES users(id);
```

### 2.2 Backend Logic Changes (Code-by-Code)

All routes and functions that interact with user-specific data must be updated. This will be done by using the `current_user` object provided by Flask-Login after a user logs in. All routes will also be protected with the `@login_required` decorator.

#### **`routes.py`**

##### `v2_upload()`

**Current:**
```python
@main_bp.route('/v2/upload', methods=['POST'])
def v2_upload():
    session_id = str(uuid.uuid4())
    # ...
    conn = get_db_connection()
    conn.execute('INSERT INTO sessions (id, original_filename, name) VALUES (?, ?, ?)', (session_id, original_filename, original_filename))
    # ...
```

**After:**
```python
from flask_login import login_required, current_user

@main_bp.route('/v2/upload', methods=['POST'])
@login_required
def v2_upload():
    session_id = str(uuid.uuid4())
    # ...
    conn = get_db_connection()
    conn.execute('INSERT INTO sessions (id, original_filename, name, user_id) VALUES (?, ?, ?, ?)', 
                 (session_id, original_filename, original_filename, current_user.id))
    # ...
```

##### `question_entry_v2(session_id)`

**Current:**
```python
@main_bp.route('/question_entry_v2/<session_id>')
def question_entry_v2(session_id):
    conn = get_db_connection()
    session_data = conn.execute(
        'SELECT original_filename, subject, tags, notes FROM sessions WHERE id = ?', (session_id,)
    ).fetchone()
    #...
```

**After:**
```python
from flask_login import login_required, current_user

@main_bp.route('/question_entry_v2/<session_id>')
@login_required
def question_entry_v2(session_id):
    conn = get_db_connection()
    # Add user_id check to prevent unauthorized access
    session_data = conn.execute(
        'SELECT original_filename, subject, tags, notes FROM sessions WHERE id = ? AND user_id = ?', 
        (session_id, current_user.id)
    ).fetchone()
    if not session_data:
        return "Unauthorized", 403
    #...
```
*(Note: This pattern of adding `@login_required` and `AND user_id = ?` to queries will be repeated for almost every route in `routes.py`, `dashboard.py`, `json_processor.py`, etc. The examples above illustrate the core change.)*

#### **`dashboard.py`**

##### `dashboard()`

**Current:**
```python
@dashboard_bp.route('/dashboard')
def dashboard():
    conn = get_db_connection()
    sessions_rows = conn.execute("""
        SELECT s.id, ...
        FROM sessions s
        ...
    """).fetchall()
    #...
```

**After:**
```python
from flask_login import login_required, current_user

@dashboard_bp.route('/dashboard')
@login_required
def dashboard():
    conn = get_db_connection()
    sessions_rows = conn.execute("""
        SELECT s.id, ...
        FROM sessions s
        LEFT JOIN images i ON s.id = i.session_id
        WHERE s.user_id = ?
        GROUP BY s.id, ...
        ORDER BY s.created_at DESC
    """, (current_user.id,)).fetchall()
    #...
```

---

## Phase 3: Security and UI

This phase focuses on the user-facing elements and securing file access.

### 3.1 UI Navigation (`_nav_links.html`)

The navigation links will be updated to show context-aware links for login, registration, and logout.

**Current:**
```html
<!-- file: templates/_nav_links.html -->
<div class="navbar-nav ms-auto">
    <a class="nav-link" href="/dashboard">...</a>
    <a class="nav-link" href="{{ url_for('neetprep_bp.index') }}">...</a>
    ...
</div>
```

**After:**
```html
<!-- file: templates/_nav_links.html -->
<div class="navbar-nav ms-auto">
    {% if current_user.is_authenticated %}
        <li class="nav-item">
            <span class="navbar-text">Welcome, {{ current_user.username }}</span>
        </li>
        <a class="nav-link" href="/dashboard">...</a>
        <a class="nav-link" href="{{ url_for('neetprep_bp.index') }}">...</a>
        ...
        <a class="nav-link" href="{{ url_for('user_auth.logout') }}">
            <i class="bi bi-box-arrow-right me-1"></i> Logout
        </a>
    {% else %}
        <a class="nav-link" href="{{ url_for('user_auth.login') }}">
            <i class="bi bi-box-arrow-in-right me-1"></i> Login
        </a>
        <a class="nav-link" href="{{ url_for('user_auth.register') }}">
            <i class="bi bi-person-plus-fill me-1"></i> Register
        </a>
    {% endif %}
</div>
```

### 3.2 Secure File Access (`routes.py`)

Routes that serve files directly must check for ownership before sending the file.

##### `download_file(filename)`

**Current:**
```python
# file: routes.py
@main_bp.route('/download/<filename>')
def download_file(filename):
    return send_file(os.path.join(current_app.config['OUTPUT_FOLDER'], filename), as_attachment=True)
```

**After:**
```python
# file: routes.py
from flask_login import login_required, current_user

@main_bp.route('/download/<filename>')
@login_required
def download_file(filename):
    conn = get_db_connection()
    # Check if the requested file belongs to the current user
    pdf_owner = conn.execute(
        'SELECT user_id FROM generated_pdfs WHERE filename = ?', (filename,)
    ).fetchone()
    conn.close()

    if pdf_owner and pdf_owner['user_id'] == current_user.id:
        return send_file(os.path.join(current_app.config['OUTPUT_FOLDER'], filename), as_attachment=True)
    else:
        return "Unauthorized", 403
```

---

## Data Migration Script (Conceptual)

A one-time script will be created to migrate the existing data.

```python
# file: migrate_to_multiuser.py (conceptual)
import sqlite3
from werkzeug.security import generate_password_hash

def migrate():
    conn = sqlite3.connect('database.db')
    cursor = conn.cursor()

    # 1. Create a default user (credentials should be provided securely)
    default_username = 'admin' # Or your preferred username
    default_password = 'your_secure_password'
    password_hash = generate_password_hash(default_password)
    
    try:
        cursor.execute(
            "INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)",
            (default_username, 'admin@local.host', password_hash)
        )
        user_id = cursor.lastrowid
        print(f"Created default user '{default_username}' with ID {user_id}")
    except sqlite3.IntegrityError:
        print("Default user already exists.")
        user_id = cursor.execute("SELECT id FROM users WHERE username = ?", (default_username,)).fetchone()[0]

    # 2. Add user_id columns (This should be done via ALTER TABLE statements first)
    # ...

    # 3. Assign all existing data to the default user
    tables_to_update = ['sessions', 'generated_pdfs', 'folders']
    for table in tables_to_update:
        try:
            cursor.execute(f"UPDATE {table} SET user_id = ? WHERE user_id IS NULL", (user_id,))
            print(f"Assigned {cursor.rowcount} records in '{table}' to user {user_id}")
        except sqlite3.OperationalError as e:
            print(f"Could not update table {table}. Maybe user_id column doesn't exist? Error: {e}")

    conn.commit()
    conn.close()
    print("Data migration complete.")

if __name__ == '__main__':
    migrate()
```