├── .cursor └── rules │ ├── 01-architecture.mdc │ ├── 02-backend-standards.mdc │ ├── 03-frontend-patterns.mdc │ ├── 04-deployment.mdc │ └── README.mdc ├── .env-sample ├── .env.tmp ├── .github └── FUNDING.yml ├── .gitignore ├── .readthedocs.yml ├── .windsurfrules ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── docs ├── authentication.mdx ├── cursor-rules.mdx ├── deployment.mdx ├── development.mdx ├── docs.json ├── enferno-demo.gif ├── favicon.svg ├── getting-started.mdx ├── introduction.mdx ├── logo │ ├── dark.svg │ └── light.svg ├── requirements.txt ├── roles-management.jpg └── users-management.jpg ├── enferno ├── __init__.py ├── app.py ├── commands.py ├── extensions.py ├── portal │ └── views.py ├── public │ ├── __init__.py │ ├── models.py │ └── views.py ├── settings.py ├── static │ ├── css │ │ ├── app.css │ │ └── vuetify.min.css │ ├── img │ │ ├── auth-bg.webp │ │ ├── enferno.svg │ │ └── favicon.ico │ ├── js │ │ ├── axios.min.js │ │ ├── config.js │ │ ├── vue.min.js │ │ └── vuetify.min.js │ ├── mdi │ │ ├── .github │ │ │ └── ISSUE_TEMPLATE.md │ │ ├── css │ │ │ ├── materialdesignicons.css │ │ │ ├── materialdesignicons.css.map │ │ │ ├── materialdesignicons.min.css │ │ │ └── materialdesignicons.min.css.map │ │ └── fonts │ │ │ ├── materialdesignicons-webfont.eot │ │ │ ├── materialdesignicons-webfont.ttf │ │ │ ├── materialdesignicons-webfont.woff │ │ │ └── materialdesignicons-webfont.woff2 │ └── robots.txt ├── tasks │ └── __init__.py ├── templates │ ├── 401.html │ ├── 404.html │ ├── 500.html │ ├── auth_layout.html │ ├── cms │ │ ├── activities.html │ │ ├── roles.html │ │ └── users.html │ ├── core │ │ ├── api.jinja2 │ │ ├── dashboard.jinja2 │ │ └── model.jinja2 │ ├── dashboard.html │ ├── index.html │ ├── layout.html │ └── security │ │ ├── _macros.html │ │ ├── _messages.html │ │ ├── change_password.html │ │ ├── confirm_email.html │ │ ├── email │ │ ├── change_notice.html │ │ ├── change_notice.txt │ │ ├── confirmation_instructions.html │ │ ├── confirmation_instructions.txt │ │ ├── login_instructions.html │ │ ├── login_instructions.txt │ │ ├── reset_instructions.html │ │ ├── reset_instructions.txt │ │ ├── reset_notice.html │ │ ├── reset_notice.txt │ │ ├── welcome.html │ │ └── welcome.txt │ │ ├── forgot_password.html │ │ ├── login_user.html │ │ ├── register_user.html │ │ ├── reset_password.html │ │ ├── send_confirmation.html │ │ └── send_login.html ├── user │ ├── __init__.py │ ├── forms.py │ ├── models.py │ └── views.py └── utils │ └── base.py ├── nginx ├── enferno.conf └── nginx.conf ├── requirements.txt ├── run.py └── setup.sh /.cursor/rules/01-architecture.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Enferno project structure and Flask architecture patterns 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # Enferno Architecture & Project Structure 7 | 8 | ## Flask Application Structure 9 | 10 | ### Core Application Files 11 | - **`enferno/app.py`** - Creates the Flask app instance, registers blueprints, configures extensions 12 | - **`enferno/settings.py`** - Handles all configuration (development, production, testing) 13 | - **`enferno/extensions.py`** - Initializes Flask extensions (database, security, etc.) 14 | - **`enferno/commands.py`** - Custom Flask CLI commands for development and deployment 15 | 16 | ### Blueprint Organization 17 | 18 | Features are organized into **Blueprints** by functional area: 19 | 20 | ``` 21 | enferno/ 22 | ├── public/ # Public-facing pages (no auth required) 23 | │ ├── views.py # Routes and view functions 24 | │ ├── models.py # Data models specific to public features 25 | │ └── templates/ # Blueprint-specific Jinja2 templates 26 | ├── user/ # User account management (auth-related) 27 | │ ├── views.py # Login, register, profile routes 28 | │ ├── models.py # User and auth models 29 | │ ├── forms.py # WTForms for user input 30 | │ └── templates/ # Auth-related templates 31 | └── portal/ # Protected admin/user portal 32 | ├── views.py # Dashboard and admin routes 33 | └── templates/ # Portal-specific templates 34 | ``` 35 | 36 | ### Blueprint Pattern 37 | ```python 38 | # In blueprint views.py 39 | from flask import Blueprint, render_template 40 | from flask_security import auth_required 41 | 42 | # Create blueprint 43 | portal = Blueprint('portal', __name__, url_prefix='/portal') 44 | 45 | # Require authentication for all portal routes 46 | @portal.before_request 47 | @auth_required() 48 | def before_request(): 49 | pass 50 | 51 | # Define routes 52 | @portal.route('/dashboard') 53 | def dashboard(): 54 | return render_template('portal/dashboard.html') 55 | ``` 56 | 57 | ## File Organization 58 | 59 | ### Static Assets 60 | - **`enferno/static/`** - Global static assets 61 | - `css/` - Stylesheets (app.css, vuetify.min.css) 62 | - `js/` - JavaScript files (Vue, Vuetify, config) 63 | - `img/` - Images and icons 64 | - `mdi/` - Material Design Icons 65 | 66 | ### Template Structure 67 | - **`enferno/templates/`** - Global Jinja2 templates 68 | - `layout.html` - Base template with Vue/Vuetify setup 69 | - `dashboard.html` - Main dashboard template 70 | - `security/` - Flask-Security templates 71 | - `cms/` - Content management templates 72 | - **Blueprint templates** - Located in each blueprint's `templates/` folder 73 | 74 | ### Frontend Architecture 75 | 76 | #### No Build Step Approach 77 | - Vue.js code written directly in `.js` files 78 | - Vue and Vuetify loaded via CDN in base template 79 | - Components defined using `Vue.defineComponent` with template strings 80 | - Configuration centralized in `enferno/static/js/config.js` 81 | 82 | #### Component Registration Pattern 83 | ```javascript 84 | // Global component registration 85 | const MyComponent = Vue.defineComponent({ 86 | template: `Component content`, 87 | data() { 88 | return { 89 | // component data 90 | } 91 | } 92 | }); 93 | 94 | // Register to main app 95 | app.component('my-component', MyComponent); 96 | ``` 97 | 98 | ## Directory Structure Best Practices 99 | 100 | ### New Blueprint Creation 101 | 1. Create blueprint directory: `enferno/feature_name/` 102 | 2. Add required files: `views.py`, `models.py` 103 | 3. Create templates subdirectory: `templates/feature_name/` 104 | 4. Register blueprint in `app.py` 105 | 106 | ### Template Organization 107 | - Global templates in `enferno/templates/` 108 | - Blueprint-specific templates in `blueprint/templates/blueprint_name/` 109 | - Follow naming convention: `feature_action.html` 110 | 111 | ### Static File Organization 112 | - Feature-specific CSS in `static/css/` 113 | - JavaScript modules in `static/js/` 114 | - Images organized by purpose in `static/img/` 115 | 116 | ## Configuration Management 117 | 118 | ### Settings Structure 119 | ```python 120 | # settings.py 121 | class Config: 122 | # Base configuration 123 | SECRET_KEY = os.environ.get('SECRET_KEY') 124 | 125 | class DevelopmentConfig(Config): 126 | # Development-specific settings 127 | DEBUG = True 128 | 129 | class ProductionConfig(Config): 130 | # Production-specific settings 131 | DEBUG = False 132 | ``` 133 | 134 | ### Extension Initialization 135 | ```python 136 | # extensions.py 137 | from flask_sqlalchemy import SQLAlchemy 138 | from flask_security import Security 139 | 140 | db = SQLAlchemy() 141 | security = Security() 142 | 143 | def init_app(app): 144 | db.init_app(app) 145 | security.init_app(app, user_datastore) 146 | ``` 147 | 148 | This architecture promotes: 149 | - **Separation of concerns** through blueprints 150 | - **Scalable organization** as features grow 151 | - **Clear file hierarchy** for easy navigation 152 | - **Consistent patterns** across the application -------------------------------------------------------------------------------- /.cursor/rules/02-backend-standards.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Python, Flask, and backend development standards for Enferno 3 | globs: ["**/*.py"] 4 | alwaysApply: true 5 | --- 6 | # Backend Development Standards 7 | 8 | ## Python Standards 9 | 10 | ### Code Quality 11 | - **PEP 8 compliant** code with meaningful variable names 12 | - **Type hints** for function parameters and return values 13 | - **Docstrings** for modules, classes, and functions 14 | - **Clean imports** - group by standard library, third-party, local 15 | 16 | ```python 17 | from typing import Dict, List, Optional 18 | from flask import Blueprint, request, jsonify 19 | from enferno.extensions import db 20 | from enferno.user.models import User 21 | 22 | def get_user_data(user_id: int) -> Optional[Dict]: 23 | """Retrieve user data by ID. 24 | 25 | Args: 26 | user_id: The unique identifier for the user 27 | 28 | Returns: 29 | User data dictionary or None if not found 30 | """ 31 | # Implementation here 32 | ``` 33 | 34 | ## Flask Development 35 | 36 | ### Blueprint Organization 37 | Group routes by feature and follow established patterns: 38 | 39 | ```python 40 | # Blueprint structure 41 | from flask import Blueprint 42 | from flask_security import auth_required, roles_required 43 | 44 | # Create blueprint with clear naming 45 | admin = Blueprint('admin', __name__, url_prefix='/admin') 46 | 47 | # Apply authentication to all routes 48 | @admin.before_request 49 | @auth_required() 50 | def require_auth(): 51 | pass 52 | 53 | # Role-based route protection 54 | @admin.route('/users') 55 | @roles_required('admin') 56 | def manage_users(): 57 | return render_template('admin/users.html') 58 | ``` 59 | 60 | ### Blueprint Categories 61 | - **`public/views.py`** - Public pages & resources (no authentication) 62 | - **`portal/views.py`** - Authenticated user routes (`auth_required` in `before_request`) 63 | - **`user/views.py`** - Account/auth routes (login, logout, profile) 64 | 65 | ### Extension Usage 66 | Import initialized extensions from `extensions.py`: 67 | 68 | ```python 69 | from enferno.extensions import db, security, mail 70 | ``` 71 | 72 | ## API Development 73 | 74 | ### RESTful Endpoint Standards 75 | - **Consistent URL patterns**: `/api/resource` for collections, `/api/resource/id` for items 76 | - **HTTP methods**: GET (retrieve), POST (create/update), DELETE (remove) 77 | - **JSON responses** with consistent structure 78 | - **Proper status codes**: 200 (success), 400 (bad request), 404 (not found), 500 (server error) 79 | 80 | ```python 81 | @api.route('/api/users', methods=['GET']) 82 | def get_users(): 83 | """Get paginated list of users.""" 84 | page = request.args.get('page', 1, type=int) 85 | per_page = request.args.get('per_page', 25, type=int) 86 | 87 | stmt = db.select(User).offset((page-1) * per_page).limit(per_page) 88 | users = db.session.scalars(stmt).all() 89 | total = db.session.scalar(db.select(db.func.count(User.id))) 90 | 91 | return jsonify({ 92 | 'items': [user.to_dict() for user in users], 93 | 'total': total, 94 | 'page': page, 95 | 'per_page': per_page 96 | }) 97 | 98 | @api.route('/api/users/', methods=['POST']) 99 | def update_user(user_id: int): 100 | """Update user data.""" 101 | try: 102 | user = db.session.get(User, user_id) 103 | if not user: 104 | return jsonify({'error': 'User not found'}), 404 105 | 106 | data = request.get_json() 107 | user.update_from_dict(data) 108 | db.session.commit() 109 | 110 | return jsonify({'message': 'User updated successfully'}) 111 | 112 | except Exception as e: 113 | db.session.rollback() 114 | return jsonify({'error': str(e)}), 500 115 | ``` 116 | 117 | ### Response Patterns 118 | ```python 119 | # Success response 120 | return jsonify({ 121 | 'message': 'Operation successful', 122 | 'data': result_data 123 | }) 124 | 125 | # Error response 126 | return jsonify({ 127 | 'error': 'Descriptive error message', 128 | 'details': error_details 129 | }), 400 130 | 131 | # List response with pagination 132 | return jsonify({ 133 | 'items': items_list, 134 | 'total': total_count, 135 | 'page': current_page, 136 | 'per_page': items_per_page 137 | }) 138 | ``` 139 | 140 | ## Database Patterns 141 | 142 | ### SQLAlchemy 2.x Statement Style 143 | Use modern SQLAlchemy patterns with explicit statements: 144 | 145 | ```python 146 | from sqlalchemy import select, update, delete 147 | from enferno.extensions import db 148 | 149 | # Select with filtering 150 | stmt = db.select(User).where(User.active == True) 151 | users = db.session.scalars(stmt).all() 152 | 153 | # Select single item 154 | stmt = db.select(User).where(User.id == user_id) 155 | user = db.session.scalar(stmt) 156 | 157 | # Update with conditions 158 | stmt = db.update(User).where(User.id == user_id).values(active=False) 159 | db.session.execute(stmt) 160 | db.session.commit() 161 | ``` 162 | 163 | ### Model Patterns 164 | ```python 165 | class User(db.Model): 166 | """User model with standard patterns.""" 167 | 168 | id = db.Column(db.Integer, primary_key=True) 169 | email = db.Column(db.String(255), unique=True, nullable=False) 170 | created_at = db.Column(db.DateTime, default=datetime.utcnow) 171 | 172 | def to_dict(self) -> Dict: 173 | """Convert model to dictionary for JSON serialization.""" 174 | return { 175 | 'id': self.id, 176 | 'email': self.email, 177 | 'created_at': self.created_at.isoformat() 178 | } 179 | 180 | def update_from_dict(self, data: Dict) -> None: 181 | """Update model attributes from dictionary.""" 182 | for key, value in data.items(): 183 | if hasattr(self, key): 184 | setattr(self, key, value) 185 | ``` 186 | 187 | ### Migration Best Practices 188 | - Use Flask-Migrate for schema changes 189 | - Create descriptive migration messages 190 | - Test migrations on development data first 191 | 192 | ## Security Standards 193 | 194 | ### Authentication & Authorization 195 | - Use Flask-Security for auth management 196 | - Apply `@auth_required()` decorator for protected routes 197 | - Use `@roles_required('role')` for role-based access 198 | - Implement CSRF protection for forms 199 | 200 | ```python 201 | from flask_security import auth_required, roles_required, current_user 202 | 203 | @blueprint.route('/admin-only') 204 | @auth_required() 205 | @roles_required('admin') 206 | def admin_function(): 207 | return render_template('admin.html') 208 | ``` 209 | 210 | ### Input Validation & Sanitization 211 | - Validate all user inputs 212 | - Use WTForms for form validation 213 | - Sanitize data before database operations 214 | - Escape output in templates 215 | 216 | ```python 217 | from wtforms import StringField, validators 218 | 219 | class UserForm(FlaskForm): 220 | email = StringField('Email', [ 221 | validators.Email(), 222 | validators.Length(min=6, max=255) 223 | ]) 224 | ``` 225 | 226 | ## Error Handling 227 | 228 | ### Exception Management 229 | ```python 230 | try: 231 | # Database operation 232 | db.session.commit() 233 | except IntegrityError: 234 | db.session.rollback() 235 | return jsonify({'error': 'Data integrity violation'}), 400 236 | except Exception as e: 237 | db.session.rollback() 238 | current_app.logger.error(f'Unexpected error: {str(e)}') 239 | return jsonify({'error': 'Internal server error'}), 500 240 | ``` 241 | 242 | ### Logging Best Practices 243 | ```python 244 | import logging 245 | 246 | # Use Flask's logger 247 | current_app.logger.info('User login successful') 248 | current_app.logger.error(f'Failed login attempt: {email}') 249 | current_app.logger.debug(f'Processing request: {request.url}') 250 | ``` 251 | 252 | ### User-Friendly Error Messages 253 | - Log technical details for developers 254 | - Return user-friendly messages to clients 255 | - Never expose sensitive information in error responses 256 | - Use consistent error response format 257 | 258 | ## Testing Standards 259 | 260 | ### Unit Test Structure 261 | ```python 262 | import pytest 263 | from enferno.app import create_app 264 | from enferno.extensions import db 265 | 266 | @pytest.fixture 267 | def app(): 268 | app = create_app('testing') 269 | with app.app_context(): 270 | db.create_all() 271 | yield app 272 | db.drop_all() 273 | 274 | def test_user_creation(app): 275 | """Test user model creation.""" 276 | with app.app_context(): 277 | user = User(email='test@example.com') 278 | db.session.add(user) 279 | db.session.commit() 280 | assert user.id is not None 281 | ``` 282 | 283 | These standards ensure: 284 | - **Code consistency** across the application 285 | - **Security best practices** in all components 286 | - **Maintainable architecture** as the project grows 287 | - **Reliable error handling** and user experience -------------------------------------------------------------------------------- /.cursor/rules/03-frontend-patterns.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Vue.js, Vuetify, and template integration patterns for Enferno 3 | globs: ["**/*.html", "**/*.js", "enferno/templates/**/*", "enferno/static/**/*"] 4 | alwaysApply: true 5 | --- 6 | # Frontend Development Patterns 7 | 8 | ## Core Frontend Architecture 9 | 10 | ### No Build Step Philosophy 11 | - **Direct JavaScript** files without compilation or bundling 12 | - **Vue 3 + Vuetify** loaded via CDN in base template 13 | - **Component definition** using `Vue.defineComponent` with template strings 14 | - **Global configuration** in `enferno/static/js/config.js` 15 | - **Per-page Vue instances** rather than SPA architecture 16 | 17 | ### Base Template Structure 18 | Every page extends `layout.html` which provides: 19 | - Vue 3 and Vuetify CDN imports 20 | - Global configuration object 21 | - Material Design Icons 22 | - Base application shell 23 | 24 | ```html 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | {% block content %}{% endblock %} 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | {% block js %}{% endblock %} 45 | 46 | 47 | ``` 48 | 49 | ## Vue-Jinja Integration 50 | 51 | ### CRITICAL: Custom Delimiters 52 | Enferno uses **`${` and `}`** for Vue expressions to avoid conflicts with Jinja's `{{ }}`: 53 | 54 | ```html 55 | 56 | ${ user.name } 57 | 58 | ${ item.title } 59 | 60 | 61 | 62 | {% if current_user.is_authenticated %} 63 | Logout 64 | {% endif %} 65 | 66 | 67 | {{ user.name }} 68 | ``` 69 | 70 | ### Vue App Initialization Pattern 71 | Every page that uses Vue must follow this initialization pattern: 72 | 73 | ```html 74 | {% extends 'layout.html' %} 75 | {% block content %} 76 | 77 | 78 | 79 | {% endblock %} 80 | 81 | {% block js %} 82 | 104 | {% endblock %} 105 | ``` 106 | 107 | ### Passing Server Data to Vue 108 | Use JSON script tags to safely pass data from Jinja to Vue: 109 | 110 | ```html 111 | 112 | 115 | 116 | 131 | ``` 132 | 133 | ## Component Patterns 134 | 135 | ### Data Table Pattern 136 | Standard pattern for displaying tabular data with Vuetify: 137 | 138 | ```javascript 139 | const app = createApp({ 140 | data() { 141 | return { 142 | items: [], 143 | itemsLength: 0, 144 | loading: false, 145 | search: '', 146 | options: { 147 | page: 1, 148 | itemsPerPage: config.itemsPerPage || 25, 149 | sortBy: [], 150 | sortDesc: [] 151 | }, 152 | headers: [ 153 | { title: 'ID', value: 'id', sortable: true }, 154 | { title: 'Name', value: 'name', sortable: true }, 155 | { title: 'Email', value: 'email', sortable: false }, 156 | { title: 'Actions', value: 'actions', sortable: false } 157 | ] 158 | } 159 | }, 160 | methods: { 161 | refresh(options) { 162 | if (options) { 163 | this.options = {...this.options, ...options}; 164 | } 165 | this.loadItems(); 166 | }, 167 | 168 | loadItems() { 169 | this.loading = true; 170 | const params = new URLSearchParams({ 171 | page: this.options.page, 172 | per_page: this.options.itemsPerPage, 173 | search: this.search 174 | }); 175 | 176 | axios.get(`/api/users?${params}`) 177 | .then(res => { 178 | this.items = res.data.items; 179 | this.itemsLength = res.data.total; 180 | }) 181 | .catch(error => { 182 | console.error('Error loading data:', error); 183 | this.showSnack('Failed to load data'); 184 | }) 185 | .finally(() => { 186 | this.loading = false; 187 | }); 188 | } 189 | }, 190 | mounted() { 191 | this.loadItems(); 192 | } 193 | }); 194 | ``` 195 | 196 | ### Edit Dialog Pattern 197 | Standard pattern for create/edit forms: 198 | 199 | ```javascript 200 | data() { 201 | return { 202 | edialog: false, 203 | eitem: { 204 | id: null, 205 | name: '', 206 | email: '', 207 | active: true 208 | }, 209 | defaultItem: { 210 | id: null, 211 | name: '', 212 | email: '', 213 | active: true 214 | } 215 | } 216 | }, 217 | methods: { 218 | editItem(item) { 219 | this.eitem = Object.assign({}, item); 220 | this.edialog = true; 221 | }, 222 | 223 | newItem() { 224 | this.eitem = Object.assign({}, this.defaultItem); 225 | this.edialog = true; 226 | }, 227 | 228 | saveItem() { 229 | const endpoint = this.eitem.id ? 230 | `/api/users/${this.eitem.id}` : 231 | '/api/users'; 232 | 233 | axios.post(endpoint, {item: this.eitem}) 234 | .then(res => { 235 | this.showSnack(res.data.message || 'Saved successfully'); 236 | this.edialog = false; 237 | this.refresh(); 238 | }) 239 | .catch(err => { 240 | this.showSnack(err.response?.data?.error || 'Save failed'); 241 | }); 242 | }, 243 | 244 | closeDialog() { 245 | this.edialog = false; 246 | this.eitem = Object.assign({}, this.defaultItem); 247 | } 248 | } 249 | ``` 250 | 251 | ## UI Component Standards 252 | 253 | ### Button Patterns 254 | ```html 255 | 256 | 257 | Add New 258 | 259 | 260 | 261 | 262 | Cancel 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | Delete 271 | 272 | ``` 273 | 274 | ### Card Layout 275 | ```html 276 | 277 | 278 | Card Title 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | Cancel 289 | Save 290 | 291 | 292 | ``` 293 | 294 | ### Data Table Template 295 | ```html 296 | 305 | 306 | 319 | 320 | 324 | 325 | ``` 326 | 327 | ## API Integration 328 | 329 | ### Standard AJAX Patterns 330 | ```javascript 331 | methods: { 332 | // GET with error handling 333 | async fetchData() { 334 | try { 335 | const response = await axios.get('/api/endpoint'); 336 | this.items = response.data.items; 337 | } catch (error) { 338 | console.error('Fetch error:', error); 339 | this.showSnack('Failed to load data'); 340 | } 341 | }, 342 | 343 | // POST with form data 344 | async saveData() { 345 | try { 346 | const response = await axios.post('/api/endpoint', { 347 | item: this.formData 348 | }); 349 | this.showSnack(response.data.message); 350 | this.refresh(); 351 | } catch (error) { 352 | this.showSnack(error.response?.data?.error || 'Save failed'); 353 | } 354 | }, 355 | 356 | // DELETE with confirmation 357 | async deleteItem(item) { 358 | if (confirm(`Delete ${item.name}?`)) { 359 | try { 360 | await axios.delete(`/api/items/${item.id}`); 361 | this.showSnack('Item deleted'); 362 | this.refresh(); 363 | } catch (error) { 364 | this.showSnack('Delete failed'); 365 | } 366 | } 367 | } 368 | } 369 | ``` 370 | 371 | ## User Feedback Patterns 372 | 373 | ### Snackbar Notifications 374 | ```javascript 375 | data() { 376 | return { 377 | snackBar: false, 378 | snackMessage: '', 379 | snackColor: 'success' 380 | } 381 | }, 382 | methods: { 383 | showSnack(message, color = 'success') { 384 | this.snackMessage = message; 385 | this.snackColor = color; 386 | this.snackBar = true; 387 | } 388 | } 389 | ``` 390 | 391 | ```html 392 | 393 | ${ snackMessage } 394 | 397 | 398 | ``` 399 | 400 | ## Styling Guidelines 401 | 402 | ### Vuetify Utility Classes 403 | ```html 404 | 405 |
406 |
407 | 408 | 409 |
410 |
411 | 412 | 413 | 414 | ``` 415 | 416 | ### Material Design Colors 417 | - Use semantic color names: `primary`, `secondary`, `error`, `warning`, `info`, `success` 418 | - Avoid hard-coded color values 419 | - Leverage Vuetify's color system 420 | 421 | ## Authentication Integration 422 | 423 | ### Template-Level Auth Checks 424 | ```html 425 | 426 | {% if current_user.is_authenticated %} 427 | Dashboard 428 | {% else %} 429 | Login 430 | {% endif %} 431 | 432 | {% if current_user.has_role('admin') %} 433 | Admin Panel 434 | {% endif %} 435 | ``` 436 | 437 | ### Vue-Level Auth State 438 | ```javascript 439 | data() { 440 | return { 441 | user: { 442 | authenticated: {{ current_user.is_authenticated|tojson }}, 443 | roles: {{ current_user.roles|map(attribute='name')|list|tojson }} 444 | } 445 | } 446 | }, 447 | methods: { 448 | hasRole(role) { 449 | return this.user.roles.includes(role); 450 | } 451 | } 452 | ``` 453 | 454 | These patterns ensure: 455 | - **Consistent UI/UX** across all pages 456 | - **Proper Vue-Jinja integration** without conflicts 457 | - **Reusable component patterns** for common functionality 458 | - **Responsive design** following Material Design principles -------------------------------------------------------------------------------- /.cursor/rules/04-deployment.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Docker and deployment best practices for Enferno 3 | globs: ["**/Dockerfile*", "**/docker-compose*.yml", "**/nginx/**/*"] 4 | alwaysApply: false 5 | --- 6 | # Docker & Deployment Standards 7 | 8 | ## Dockerfile Best Practices 9 | 10 | ### Base Image Selection 11 | - **Use official Python images**: `python:3.11-slim` for production 12 | - **Pin specific versions** to ensure reproducible builds 13 | - **Multi-stage builds** to minimize final image size 14 | 15 | ```dockerfile 16 | # Multi-stage build example 17 | FROM python:3.11-slim as builder 18 | WORKDIR /app 19 | COPY requirements.txt . 20 | RUN pip install --user -r requirements.txt 21 | 22 | FROM python:3.11-slim 23 | WORKDIR /app 24 | COPY --from=builder /root/.local /root/.local 25 | COPY . . 26 | ENV PATH=/root/.local/bin:$PATH 27 | EXPOSE 5000 28 | CMD ["python", "run.py"] 29 | ``` 30 | 31 | ### Security Practices 32 | - **Never run as root** - create dedicated user 33 | - **Don't hardcode secrets** in Dockerfiles 34 | - **Use .dockerignore** to exclude sensitive files 35 | - **Regular base image updates** for security patches 36 | 37 | ```dockerfile 38 | # Create non-root user 39 | RUN adduser --disabled-password --gecos '' appuser 40 | USER appuser 41 | 42 | # Use build args for configuration 43 | ARG FLASK_ENV=production 44 | ENV FLASK_ENV=${FLASK_ENV} 45 | ``` 46 | 47 | ## Docker Compose Configuration 48 | 49 | ### Service Organization 50 | ```yaml 51 | version: '3.8' 52 | services: 53 | web: 54 | build: . 55 | ports: 56 | - "5000:5000" 57 | environment: 58 | - FLASK_ENV=development 59 | volumes: 60 | - .:/app 61 | depends_on: 62 | - db 63 | - redis 64 | 65 | db: 66 | image: postgres:15-alpine 67 | environment: 68 | POSTGRES_DB: enferno 69 | POSTGRES_USER: ${DB_USER} 70 | POSTGRES_PASSWORD: ${DB_PASSWORD} 71 | volumes: 72 | - postgres_data:/var/lib/postgresql/data 73 | 74 | nginx: 75 | image: nginx:alpine 76 | ports: 77 | - "80:80" 78 | volumes: 79 | - ./nginx:/etc/nginx/conf.d 80 | depends_on: 81 | - web 82 | 83 | volumes: 84 | postgres_data: 85 | ``` 86 | 87 | ### Environment Management 88 | - **Use .env files** for environment-specific configuration 89 | - **Separate configs** for development, staging, production 90 | - **Document all environment variables** in README 91 | 92 | ## Nginx Configuration 93 | 94 | ### Flask Application Proxy 95 | ```nginx 96 | # nginx/enferno.conf 97 | upstream flask_app { 98 | server web:5000; 99 | } 100 | 101 | server { 102 | listen 80; 103 | server_name localhost; 104 | 105 | # Static files 106 | location /static { 107 | alias /app/enferno/static; 108 | expires 1y; 109 | add_header Cache-Control "public, immutable"; 110 | } 111 | 112 | # Flask application 113 | location / { 114 | proxy_pass http://flask_app; 115 | proxy_set_header Host $host; 116 | proxy_set_header X-Real-IP $remote_addr; 117 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 118 | proxy_set_header X-Forwarded-Proto $scheme; 119 | } 120 | } 121 | ``` 122 | 123 | ## Development Workflow 124 | 125 | ### Local Development Setup 126 | ```bash 127 | # Start development environment 128 | docker-compose up -d 129 | 130 | # View logs 131 | docker-compose logs -f web 132 | 133 | # Execute commands in container 134 | docker-compose exec web flask db upgrade 135 | docker-compose exec web flask create-admin 136 | 137 | # Stop services 138 | docker-compose down 139 | ``` 140 | 141 | ### Production Deployment 142 | ```yaml 143 | # docker-compose.prod.yml 144 | version: '3.8' 145 | services: 146 | web: 147 | build: 148 | context: . 149 | dockerfile: Dockerfile.prod 150 | environment: 151 | - FLASK_ENV=production 152 | restart: unless-stopped 153 | 154 | nginx: 155 | image: nginx:alpine 156 | ports: 157 | - "80:80" 158 | - "443:443" 159 | volumes: 160 | - ./nginx/prod:/etc/nginx/conf.d 161 | - ./certs:/etc/nginx/certs 162 | restart: unless-stopped 163 | ``` 164 | 165 | ## Container Management 166 | 167 | ### Health Checks 168 | ```dockerfile 169 | # Add health check to Dockerfile 170 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ 171 | CMD curl -f http://localhost:5000/health || exit 1 172 | ``` 173 | 174 | ### Resource Limits 175 | ```yaml 176 | # In docker-compose.yml 177 | services: 178 | web: 179 | deploy: 180 | resources: 181 | limits: 182 | cpus: '0.50' 183 | memory: 512M 184 | reservations: 185 | cpus: '0.25' 186 | memory: 256M 187 | ``` 188 | 189 | ## Environment Variables 190 | 191 | ### Required Variables 192 | Document all required environment variables: 193 | 194 | ```bash 195 | # Database 196 | DATABASE_URL=postgresql://user:pass@localhost/enferno 197 | DB_USER=enferno 198 | DB_PASSWORD=secure_password 199 | 200 | # Flask 201 | SECRET_KEY=your-secret-key-here 202 | FLASK_ENV=production 203 | 204 | # Security 205 | SECURITY_PASSWORD_SALT=your-salt-here 206 | MAIL_SERVER=smtp.gmail.com 207 | MAIL_PORT=587 208 | ``` 209 | 210 | ### Security Considerations 211 | - **Never commit secrets** to version control 212 | - **Use Docker secrets** in production swarm mode 213 | - **Rotate secrets regularly** 214 | - **Use strong, unique passwords** for all services 215 | 216 | ## Testing & Quality Assurance 217 | 218 | ### Container Testing 219 | ```bash 220 | # Test build process 221 | docker build -t enferno:test . 222 | 223 | # Test image functionality 224 | docker run --rm enferno:test python -m pytest 225 | 226 | # Security scanning 227 | docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ 228 | aquasec/trivy enferno:latest 229 | ``` 230 | 231 | ### CI/CD Integration 232 | ```yaml 233 | # .github/workflows/docker.yml 234 | name: Docker Build 235 | on: [push, pull_request] 236 | jobs: 237 | build: 238 | runs-on: ubuntu-latest 239 | steps: 240 | - uses: actions/checkout@v2 241 | - name: Build Docker image 242 | run: docker build -t enferno:${{ github.sha }} . 243 | - name: Test application 244 | run: docker run --rm enferno:${{ github.sha }} python -m pytest 245 | ``` 246 | 247 | ## Documentation Requirements 248 | 249 | ### Container Documentation 250 | - **README updates** for any Docker changes 251 | - **Environment variable documentation** with descriptions 252 | - **Port mappings** and service dependencies 253 | - **Volume mount explanations** and data persistence 254 | 255 | ### Deployment Notes 256 | ```markdown 257 | ## Docker Deployment 258 | 259 | ### Requirements 260 | - Docker 20.10+ 261 | - Docker Compose 2.0+ 262 | 263 | ### Environment Setup 264 | 1. Copy `.env.example` to `.env` 265 | 2. Update environment variables 266 | 3. Run `docker-compose up -d` 267 | 268 | ### Ports 269 | - 5000: Flask application 270 | - 80: Nginx web server 271 | - 5432: PostgreSQL database 272 | ``` 273 | 274 | These standards ensure: 275 | - **Secure container operations** with non-root users 276 | - **Reproducible deployments** across environments 277 | - **Efficient resource utilization** with optimized images 278 | - **Proper documentation** for team collaboration 279 | -------------------------------------------------------------------------------- /.cursor/rules/README.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Overview and index of Enferno development rules 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # Enferno Framework Rules for Cursor 7 | 8 | This directory contains comprehensive coding standards and architectural patterns for the **Enferno** framework. 9 | 10 | ## Rule Files Organization 11 | 12 | ### Core Architecture 13 | - `01-architecture.mdc` — Project structure, Flask app organization, and blueprint patterns 14 | - `02-backend-standards.mdc` — Python, Flask, and API development standards 15 | - `03-frontend-patterns.mdc` — Vue.js, Vuetify, and template integration patterns 16 | - `04-deployment.mdc` — Docker and deployment best practices 17 | 18 | ## Framework Overview 19 | 20 | **Enferno** is a full-stack web framework built with: 21 | 22 | - **Backend**: Python Flask with Blueprint organization 23 | - **Frontend**: Vue 3 + Vuetify (no build step, direct JS) 24 | - **Templates**: Jinja2 server-side rendering with Vue integration 25 | - **UI**: Material Design via Vuetify components 26 | - **API**: RESTful architecture with JSON responses 27 | - **Database**: SQLAlchemy 2.x with statement-based patterns 28 | 29 | ## Key Architectural Principles 30 | 31 | 1. **Blueprint-based organization** by feature (admin, public, user) 32 | 2. **No SPA** - each page mounts its own Vue instance 33 | 3. **Custom Vue delimiters** (`${}`) to avoid Jinja conflicts 34 | 4. **Direct JavaScript** without build tools 35 | 5. **RESTful API design** with consistent responses 36 | 37 | These rules ensure consistency, scalability, and maintainability across the codebase. 38 | 39 | -------------------------------------------------------------------------------- /.env-sample: -------------------------------------------------------------------------------- 1 | #Enferno 2 | SECRET_KEY=3nF3Rn@ 3 | FLASK_APP=run.py 4 | FLASK_DEBUG=0 5 | MAIL_USERNAME=sendgrid 6 | MAIL_PASSWORD=sendgrid_secure_password 7 | SQLALCHEMY_DATABASE_URI=postgresql://postgres:pass@localhost/dbname 8 | SESSION_REDIS=redis://localhost:6379/1 9 | CELERY_BROKER_URL=redis://localhost:6379/2 10 | CELERY_RESULT_BACKEND=redis://localhost:6379/3 11 | SECURITY_TOTP_SECRETS=secret1,secret2 12 | 13 | # Additional Security Keys 14 | SECURITY_PASSWORD_SALT=3nF3Rn0 15 | 16 | # Docker Configuration (uncomment for Docker) 17 | #REDIS_PASSWORD=verystrongpass 18 | #DB_PASSWORD=verystrongpass 19 | #SQLALCHEMY_DATABASE_URI=postgresql://enferno:${DB_PASSWORD}@postgres/enferno 20 | #SESSION_REDIS=redis://:${REDIS_PASSWORD}@redis:6379/1 21 | #CELERY_BROKER_URL=redis://:${REDIS_PASSWORD}@redis:6379/2 22 | #CELERY_RESULT_BACKEND=redis://:${REDIS_PASSWORD}@redis:6379/3 23 | 24 | # Google OAuth Settings 25 | GOOGLE_AUTH_ENABLED=True 26 | GOOGLE_OAUTH_CLIENT_ID=your_google_client_id 27 | GOOGLE_OAUTH_CLIENT_SECRET=your_google_client_secret 28 | OAUTHLIB_INSECURE_TRANSPORT=1 29 | 30 | # GitHub OAuth Settings 31 | GITHUB_AUTH_ENABLED=False 32 | GITHUB_OAUTH_CLIENT_ID=your_github_client_id 33 | GITHUB_OAUTH_CLIENT_SECRET=your_github_client_secret 34 | 35 | # Mail Settings 36 | MAIL_SERVER=smtp.sendgrid.net 37 | SECURITY_EMAIL_SENDER=info@domain.com 38 | 39 | # Cookie Settings 40 | SESSION_COOKIE_SECURE=True 41 | SESSION_COOKIE_HTTPONLY=True 42 | SESSION_COOKIE_SAMESITE=Lax 43 | 44 | # Docker Compose Settings 45 | COMPOSE_PROJECT_NAME=enferno 46 | PYTHONUNBUFFERED=true 47 | 48 | # Docker-specific settings 49 | # UID for Docker containers (defaults to 1000 if not set) 50 | DOCKER_UID=1000 51 | # PostgreSQL database password 52 | DB_PASSWORD=verystrongpass 53 | # Redis password 54 | REDIS_PASSWORD=verystrongpass 55 | -------------------------------------------------------------------------------- /.env.tmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level09/enferno/81c827bd32fc06d3d83476a48e9c52f89ecb7a68/.env.tmp -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [level09] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .webassets-cache 3 | .idea 4 | celerybeat-schedule 5 | env 6 | .env 7 | .env.backup* 8 | local_settings.py 9 | node_modules/ 10 | .DS_Store 11 | .sass-cache 12 | .vscode/ 13 | .cache/ 14 | .parcel-cache 15 | enferno.sqlite3 16 | instance/ 17 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | mkdocs: 9 | configuration: mkdocs.yml 10 | 11 | -------------------------------------------------------------------------------- /.windsurfrules: -------------------------------------------------------------------------------- 1 | Our approach is pythonic, minimal and readable code. Follow these guidelines: 2 | 3 | 1. Use Vuetify components for UI (refer to vuetify.min.js) 4 | 2. Leverage Material Design Icons (refer to materialdesignicons.css) 5 | 3. Follow Vue.js patterns for state management and reactivity 6 | 4. Use the snackbar pattern for user feedback (refer to roles.html lines 120-123) 7 | 5. For data tables, use the headers pattern (refer to roles.html lines 98-103) 8 | 6. Keep components modular and reusable 9 | 7. Use the mounted() lifecycle hook for initial data loading (refer to roles.html lines 113-115) 10 | 8. For forms, use the edialog pattern with eitem object (refer to roles.html lines 104-109) 11 | 9. Maintain consistent error handling and user feedback 12 | 10. Use Vue's reactivity system for state management 13 | 11. Keep API calls in methods and use async/await 14 | 12. Use the refresh pattern for data updates (refer to roles.html line 125) 15 | 13. When using Jinja templates, set Vue custom delimiters to "${ }" to avoid conflict with Jinja's {{ }} (refer to roles.html line 116) 16 | 14. Use the drawer pattern for navigation (refer to roles.html line 96) 17 | 15. Keep styles minimal and use Vuetify's built-in classes 18 | 16. Write Vue code directly in client-side JavaScript without build tools -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v11.2.0 (2025-04-24) 4 | 5 | ### Added 6 | - Production-ready Docker configuration with multi-stage builds 7 | - PostgreSQL service in Docker Compose setup 8 | - Improved environment variable handling for Docker 9 | - Support for user-specific Docker UID configuration 10 | - Enhanced setup.sh script with Docker configuration option 11 | 12 | ### Changed 13 | - Optimized Dockerfile with multi-stage build for smaller, more secure images 14 | - Fixed Redis connectivity by using correct environment variables 15 | - Improved nginx configuration with proper retry settings 16 | - Enhanced tmpfs configuration for better performance 17 | - Added proper health checks for all Docker services 18 | 19 | ## v11.1.0 (2025-03-30) 20 | 21 | ### Added 22 | - Migrated from pip/venv to uv for package management 23 | - Faster installation and dependency resolution 24 | - Better Python environment isolation 25 | 26 | ### Changed 27 | - Updated setup.sh script to use uv instead of venv 28 | - Modified Dockerfile to use uv for package installation 29 | - Updated documentation to reference uv 30 | 31 | ## v11.0 (2023-03-27) 32 | 33 | ### Added 34 | - New activity model to track user actions like creating and editing users/roles 35 | - Cursor Rules for improved code generation and assistance 36 | - Comprehensive documentation for Cursor Rules approach 37 | 38 | ### Changed 39 | - Improved user and roles tables design in both frontend and backend 40 | - Transitioned from OpenAI integration to Cursor Rules for code generation 41 | - Enhanced admin user creation with better console output 42 | 43 | ### Removed 44 | - Removed flask-openai dependency and related code generation commands 45 | - Removed OpenAI API key requirements 46 | 47 | ### Fixed 48 | - Various UI and UX improvements 49 | - Code cleanup and bug fixes -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM python:3.12-slim AS builder 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Install build dependencies 8 | RUN apt-get update && apt-get install -y --no-install-recommends \ 9 | build-essential \ 10 | python3-dev \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | # Install uv 14 | # Using pip as uv is not yet available in Debian repositories. 15 | # Alternatively, use: curl -sSf https://astral.sh/uv/install.sh | sh 16 | RUN pip install --no-cache-dir uv 17 | 18 | # Create virtual environment 19 | RUN uv venv /opt/venv 20 | ENV PATH="/opt/venv/bin:$PATH" 21 | 22 | # Install Python dependencies 23 | COPY requirements.txt . 24 | RUN uv pip install --no-cache-dir -r requirements.txt 25 | 26 | # Runtime stage 27 | FROM python:3.12-slim 28 | 29 | # Copy virtual environment from build stage 30 | COPY --from=builder /opt/venv /opt/venv 31 | ENV PATH="/opt/venv/bin:$PATH" 32 | 33 | # Set working directory 34 | WORKDIR /app 35 | 36 | # Create non-root user 37 | RUN useradd -m -u 1000 enferno && \ 38 | chown -R enferno:enferno /app 39 | 40 | # Install only runtime dependencies 41 | RUN apt-get update && apt-get install -y --no-install-recommends \ 42 | curl \ 43 | libexpat1 \ 44 | && rm -rf /var/lib/apt/lists/* 45 | 46 | # Copy project files 47 | COPY --chown=enferno:enferno . . 48 | 49 | # Switch to non-root user 50 | USER enferno 51 | 52 | # Health check 53 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ 54 | CMD curl -f http://localhost:5000/ || exit 1 55 | 56 | # Run the application 57 | CMD ["uwsgi", "--http", "0.0.0.0:5000", "--master", "--wsgi", "run:app", "--uid", "1000"] 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nidal Alhariri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Enferno 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | Enferno is a modern Flask framework optimized for AI-assisted development workflows. By combining carefully crafted development patterns, smart Cursor Rules, and modern libraries, it enables developers to build sophisticated web applications with unprecedented speed. Whether you're using AI-powered IDEs like Cursor or traditional tools, Enferno's intelligent patterns and contextual guides help you create production-ready SAAS applications faster than ever. 6 | 7 | ![Enferno Demo](docs/enferno-demo.gif) 8 | 9 | Key Features 10 | =========== 11 | - **Modern Stack**: Python 3.11+, Flask, Vue 3, Vuetify 3 12 | - **Authentication**: Flask-Security with role-based access control 13 | - **OAuth Integration**: Google and GitHub login via Flask-Dance 14 | - **Database**: SQLAlchemy ORM with PostgreSQL/SQLite support 15 | - **Task Queue**: Celery with Redis for background tasks 16 | - **Frontend**: Client-side Vue.js with Vuetify components 17 | - **Security**: CSRF protection, secure session handling 18 | - **Docker Ready**: Production-grade Docker configuration 19 | - **Cursor Rules**: Smart IDE-based code generation and assistance 20 | - **Package Management**: Fast installation with uv 21 | 22 | Frontend Features 23 | --------------- 24 | - Vue.js without build tools - direct browser integration 25 | - Vuetify Material Design components 26 | - Axios for API calls 27 | - Snackbar notifications pattern 28 | - Dialog forms pattern 29 | - Data table server pattern 30 | - Authentication state integration 31 | - Material Design Icons 32 | 33 | OAuth Integration 34 | --------------- 35 | Supports social login with: 36 | - Google (profile and email scope) 37 | - GitHub (user:email scope) 38 | 39 | Configure in `.env`: 40 | ```bash 41 | # Google OAuth 42 | GOOGLE_AUTH_ENABLED=true 43 | GOOGLE_OAUTH_CLIENT_ID=your_client_id 44 | GOOGLE_OAUTH_CLIENT_SECRET=your_client_secret 45 | 46 | # GitHub OAuth 47 | GITHUB_AUTH_ENABLED=true 48 | GITHUB_OAUTH_CLIENT_ID=your_client_id 49 | GITHUB_OAUTH_CLIENT_SECRET=your_client_secret 50 | ``` 51 | 52 | Prerequisites 53 | ------------ 54 | - Python 3.11+ 55 | - Redis (for caching and sessions) 56 | - PostgreSQL (optional, SQLite works for development) 57 | - Git 58 | - uv (fast Python package installer and resolver) 59 | 60 | Quick Start 61 | ---------- 62 | 63 | ### Local Setup 64 | 65 | 1. Install uv: 66 | ```bash 67 | # Install using pip 68 | pip install uv 69 | 70 | # Or using the installer script 71 | curl -sSf https://astral.sh/uv/install.sh | bash 72 | ``` 73 | 74 | 2. Clone and setup: 75 | ```bash 76 | git clone git@github.com:level09/enferno.git 77 | cd enferno 78 | ./setup.sh # Creates Python environment, installs requirements, and generates secure .env 79 | ``` 80 | 81 | 3. Activate Environment: 82 | ```bash 83 | source .venv/bin/activate 84 | ``` 85 | 86 | 4. Initialize application: 87 | ```bash 88 | flask create-db # Setup database 89 | flask install # Create admin user 90 | ``` 91 | 92 | 5. Run development server: 93 | ```bash 94 | flask run 95 | ``` 96 | 97 | ### Docker Setup 98 | 99 | One-command setup with Docker: 100 | ```bash 101 | docker compose up --build 102 | ``` 103 | 104 | The Docker setup includes: 105 | - Redis for caching and session management 106 | - PostgreSQL database 107 | - Nginx for serving static files 108 | - Celery for background tasks 109 | 110 | Configuration 111 | ------------ 112 | 113 | Key environment variables (.env): 114 | 115 | ```bash 116 | # Core 117 | FLASK_APP=run.py 118 | FLASK_DEBUG=1 # 0 in production 119 | SECRET_KEY=your_secret_key 120 | 121 | # Database (choose one) 122 | SQLALCHEMY_DATABASE_URI=sqlite:///enferno.sqlite3 123 | # Or for PostgreSQL: 124 | # SQLALCHEMY_DATABASE_URI=postgresql://username:password@localhost/dbname 125 | 126 | # Redis & Celery 127 | REDIS_URL=redis://localhost:6379/0 128 | CELERY_BROKER_URL=redis://localhost:6379/1 129 | CELERY_RESULT_BACKEND=redis://localhost:6379/2 130 | 131 | # Email Settings (optional) 132 | MAIL_SERVER=smtp.example.com 133 | MAIL_PORT=465 134 | MAIL_USE_SSL=True 135 | MAIL_USERNAME=your_email 136 | MAIL_PASSWORD=your_password 137 | SECURITY_EMAIL_SENDER=noreply@example.com 138 | 139 | # OAuth (optional) 140 | GOOGLE_AUTH_ENABLED=true 141 | GOOGLE_OAUTH_CLIENT_ID=your_client_id 142 | GOOGLE_OAUTH_CLIENT_SECRET=your_client_secret 143 | 144 | GITHUB_AUTH_ENABLED=true 145 | GITHUB_OAUTH_CLIENT_ID=your_client_id 146 | GITHUB_OAUTH_CLIENT_SECRET=your_client_secret 147 | 148 | # Security Settings 149 | SECURITY_PASSWORD_SALT=your_secure_salt 150 | SECURITY_TOTP_SECRETS=your_totp_secrets 151 | ``` 152 | 153 | Security Features 154 | --------------- 155 | - Two-factor authentication (2FA) 156 | - WebAuthn support 157 | - OAuth integration 158 | - Password policies 159 | - Session protection 160 | - CSRF protection 161 | - Secure cookie settings 162 | - Rate limiting 163 | - XSS protection 164 | 165 | For detailed documentation, visit [docs.enferno.io](https://docs.enferno.io) 166 | 167 | Contributing 168 | ----------- 169 | Contributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md). 170 | 171 | License 172 | ------- 173 | MIT licensed. 174 | 175 | 176 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | redis: 4 | container_name: redis 5 | image: 'redis:7-alpine' 6 | user: "${DOCKER_UID:-1000}" 7 | command: redis-server --requirepass ${REDIS_PASSWORD:-verystrongpass} 8 | volumes: 9 | - 'redis-data:/data' 10 | ports: 11 | - '6379:6379' 12 | healthcheck: 13 | test: ["CMD", "redis-cli", "ping"] 14 | interval: 5s 15 | timeout: 3s 16 | retries: 3 17 | 18 | postgres: 19 | container_name: postgres 20 | image: 'postgres:15-alpine' 21 | volumes: 22 | - 'postgres-data:/var/lib/postgresql/data' 23 | ports: 24 | - '5432:5432' 25 | environment: 26 | - POSTGRES_USER=enferno 27 | - POSTGRES_PASSWORD=${DB_PASSWORD:-verystrongpass} 28 | - POSTGRES_DB=enferno 29 | healthcheck: 30 | test: ["CMD", "pg_isready", "-U", "enferno"] 31 | interval: 5s 32 | timeout: 3s 33 | retries: 3 34 | 35 | website: 36 | container_name: website 37 | build: . 38 | user: "${DOCKER_UID:-1000}" 39 | volumes: 40 | - '.:/app' 41 | depends_on: 42 | - redis 43 | - postgres 44 | ports: 45 | - '8000:5000' 46 | environment: 47 | - FLASK_APP=run.py 48 | - FLASK_DEBUG=0 49 | - SQLALCHEMY_DATABASE_URI=postgresql://enferno:${DB_PASSWORD:-verystrongpass}@postgres/enferno 50 | - CELERY_BROKER_URL=redis://:${REDIS_PASSWORD:-verystrongpass}@redis:6379/2 51 | - CELERY_RESULT_BACKEND=redis://:${REDIS_PASSWORD:-verystrongpass}@redis:6379/3 52 | - REDIS_SESSION=redis://:${REDIS_PASSWORD:-verystrongpass}@redis:6379/1 53 | healthcheck: 54 | test: ["CMD", "curl", "-f", "http://localhost:5000/"] 55 | interval: 30s 56 | timeout: 10s 57 | retries: 3 58 | 59 | nginx: 60 | container_name: nginx 61 | restart: always 62 | image: 'nginx:alpine' 63 | user: "${DOCKER_UID:-1000}" 64 | ports: 65 | - '80:80' 66 | volumes: 67 | - './enferno/static/:/app/static/:ro' 68 | - './nginx/nginx.conf:/etc/nginx/nginx.conf:ro' 69 | - './nginx/enferno.conf:/etc/nginx/conf.d/default.conf:ro' 70 | tmpfs: 71 | - /tmp:exec 72 | - /var/run 73 | - /var/cache/nginx 74 | depends_on: 75 | - website 76 | healthcheck: 77 | test: ["CMD", "nginx", "-t"] 78 | interval: 30s 79 | timeout: 10s 80 | 81 | celery: 82 | container_name: celery 83 | build: . 84 | user: "${DOCKER_UID:-1000}" 85 | command: celery -A enferno.tasks worker -l info 86 | volumes: 87 | - '.:/app' 88 | depends_on: 89 | - redis 90 | environment: 91 | - FLASK_APP=run.py 92 | - CELERY_BROKER_URL=redis://:${REDIS_PASSWORD:-verystrongpass}@redis:6379/2 93 | - CELERY_RESULT_BACKEND=redis://:${REDIS_PASSWORD:-verystrongpass}@redis:6379/3 94 | - REDIS_SESSION=redis://:${REDIS_PASSWORD:-verystrongpass}@redis:6379/1 95 | 96 | volumes: 97 | redis-data: 98 | driver: local 99 | postgres-data: 100 | driver: local 101 | -------------------------------------------------------------------------------- /docs/authentication.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Authentication" 3 | description: "User authentication and security features in Enferno" 4 | sidebarTitle: "Authentication" 5 | --- 6 | 7 | ## Overview 8 | 9 | Enferno provides a robust authentication system built on Flask-Security-Too with modern security features and OAuth integration. 10 | 11 | ## Features 12 | 13 | - User registration and login 14 | - Role-based access control (RBAC) 15 | - Two-factor authentication (2FA) 16 | - WebAuthn support 17 | - OAuth integration (Google, GitHub) 18 | - Password policies and recovery 19 | - Session protection 20 | - CSRF protection 21 | - Rate limiting 22 | - XSS protection 23 | 24 | ## OAuth Integration 25 | 26 | Enable social login with Google and GitHub: 27 | 28 | ```bash 29 | # Google OAuth 30 | GOOGLE_AUTH_ENABLED=true 31 | GOOGLE_OAUTH_CLIENT_ID=your_client_id 32 | GOOGLE_OAUTH_CLIENT_SECRET=your_client_secret 33 | 34 | # GitHub OAuth 35 | GITHUB_AUTH_ENABLED=true 36 | GITHUB_OAUTH_CLIENT_ID=your_client_id 37 | GITHUB_OAUTH_CLIENT_SECRET=your_client_secret 38 | ``` 39 | 40 | Scopes: 41 | - Google: profile and email 42 | - GitHub: user:email 43 | 44 | ## User Management 45 | 46 | ### Registration 47 | 48 | ```python 49 | from enferno.user.models import User 50 | from enferno.user.forms import RegisterForm 51 | 52 | @app.route('/register', methods=['POST']) 53 | def register(): 54 | form = RegisterForm() 55 | if form.validate_on_submit(): 56 | user = User( 57 | email=form.email.data, 58 | password=form.password.data, 59 | active=True 60 | ) 61 | user.save() 62 | return jsonify({'message': 'Registration successful'}) 63 | return jsonify(form.errors), 400 64 | ``` 65 | 66 | ### Role-Based Access 67 | 68 | ```python 69 | from flask_security import roles_required, roles_accepted 70 | 71 | @app.route('/admin') 72 | @roles_required('admin') 73 | def admin_dashboard(): 74 | return 'Admin only content' 75 | 76 | @app.route('/premium') 77 | @roles_accepted('premium', 'admin') 78 | def premium_content(): 79 | return 'Premium or admin content' 80 | ``` 81 | 82 | ## API Authentication 83 | 84 | ### Token-Based Auth 85 | 86 | ```python 87 | from flask_security import auth_token_required 88 | 89 | @app.route('/api/protected') 90 | @auth_token_required 91 | def protected_endpoint(): 92 | return jsonify({'message': 'Authenticated access'}) 93 | ``` 94 | 95 | ### Generate Auth Token 96 | 97 | ```python 98 | from flask_security.utils import get_token_status 99 | 100 | def generate_auth_token(user): 101 | token = user.get_auth_token() 102 | return jsonify({ 103 | 'token': token, 104 | 'expires': get_token_status(token)['exp'] 105 | }) 106 | ``` 107 | 108 | ## Security Configuration 109 | 110 | Key security settings in `.env`: 111 | 112 | ```bash 113 | # Security Settings 114 | SECURITY_PASSWORD_SALT=your_secure_salt 115 | SECURITY_TOTP_SECRETS=your_totp_secrets 116 | SECURITY_REGISTERABLE=true 117 | SECURITY_CONFIRMABLE=true 118 | SECURITY_RECOVERABLE=true 119 | SECURITY_TRACKABLE=true 120 | SECURITY_PASSWORD_LENGTH_MIN=8 121 | SECURITY_TOKEN_MAX_AGE=86400 122 | ``` 123 | 124 | ## Two-Factor Authentication 125 | 126 | Enable 2FA for enhanced security: 127 | 128 | ```python 129 | from flask_security import two_factor_required 130 | 131 | @app.route('/sensitive') 132 | @two_factor_required 133 | def sensitive_data(): 134 | return 'Two-factor authenticated content' 135 | ``` 136 | 137 | ## WebAuthn Support 138 | 139 | Enable WebAuthn for passwordless authentication: 140 | 141 | ```python 142 | from flask_security import webauthn_required 143 | 144 | @app.route('/webauthn-protected') 145 | @webauthn_required 146 | def webauthn_protected(): 147 | return 'WebAuthn authenticated content' 148 | ``` 149 | 150 | ## Session Protection 151 | 152 | Enferno includes several session security measures: 153 | 154 | ```python 155 | # Session Configuration 156 | SESSION_PROTECTION = 'strong' 157 | PERMANENT_SESSION_LIFETIME = timedelta(days=1) 158 | SESSION_COOKIE_SECURE = True 159 | SESSION_COOKIE_HTTPONLY = True 160 | SESSION_COOKIE_SAMESITE = 'Lax' 161 | ``` 162 | 163 | ## Rate Limiting 164 | 165 | Protect against brute force attacks: 166 | 167 | ```python 168 | from flask_limiter import Limiter 169 | from flask_limiter.util import get_remote_address 170 | 171 | limiter = Limiter( 172 | app, 173 | key_func=get_remote_address, 174 | default_limits=["200 per day", "50 per hour"] 175 | ) 176 | 177 | @app.route('/login', methods=['POST']) 178 | @limiter.limit("5 per minute") 179 | def login(): 180 | # Login logic here 181 | pass 182 | ``` 183 | 184 | ## Best Practices 185 | 186 | 1. **Password Storage** 187 | - Passwords are automatically hashed using secure algorithms 188 | - Salt is unique per user 189 | - Configurable password policies 190 | 191 | 2. **CSRF Protection** 192 | - Automatic CSRF token generation 193 | - Required for all POST/PUT/DELETE requests 194 | - Configurable token lifetime 195 | 196 | 3. **XSS Prevention** 197 | - Content Security Policy headers 198 | - Automatic HTML escaping in templates 199 | - Secure cookie flags 200 | 201 | 4. **Security Headers** 202 | - HSTS enabled 203 | - X-Frame-Options set 204 | - X-Content-Type-Options: nosniff 205 | - Referrer-Policy configured 206 | 207 | ## Troubleshooting 208 | 209 | Common issues and solutions: 210 | 211 | 1. **Token Expiration** 212 | - Check `SECURITY_TOKEN_MAX_AGE` setting 213 | - Verify system time synchronization 214 | - Clear expired tokens regularly 215 | 216 | 2. **OAuth Issues** 217 | - Verify callback URLs in provider settings 218 | - Check scope permissions 219 | - Ensure secrets are correctly configured 220 | 221 | 3. **2FA Problems** 222 | - Verify TOTP secrets configuration 223 | - Check time synchronization 224 | - Provide backup codes for recovery -------------------------------------------------------------------------------- /docs/cursor-rules.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Cursor Rules" 3 | description: "AI-powered development assistance in Enferno" 4 | sidebarTitle: "Cursor Rules" 5 | --- 6 | 7 | ## Overview 8 | 9 | Enferno leverages Cursor Rules for AI-powered development assistance, providing context-aware code generation and guidance through modern IDEs like Cursor. This approach replaces traditional template-based generation with more flexible, intelligent assistance. 10 | 11 | ## What Are Cursor Rules? 12 | 13 | Cursor Rules are documentation-as-code specifications that provide structured guidance to AI assistants in modern IDEs. They contain information about: 14 | 15 | - Code patterns and conventions 16 | - Framework-specific best practices 17 | - Integration techniques 18 | - Component usage examples 19 | - UI/UX standards 20 | 21 | ## Available Rules 22 | 23 | Enferno includes rules for key development aspects: 24 | 25 | ### Vue-Jinja Patterns 26 | Guidelines for integrating Vue.js with Flask Jinja templates: 27 | ```python 28 | # Template example 29 | @app.route('/products') 30 | def products(): 31 | return render_template('products.html', 32 | products=products, 33 | vue_enabled=True) 34 | ``` 35 | 36 | ```html 37 | 38 | {% extends "layout.html" %} 39 | {% block content %} 40 |
41 | 45 |
46 | {% endblock %} 47 | ``` 48 | 49 | ### UI Components 50 | Standards for Vuetify components and usage: 51 | ```html 52 | 53 | 54 | 57 | 58 | Dialog Title 59 | Content here 60 | 61 | 62 | ``` 63 | 64 | ### Python Standards 65 | Flask patterns and backend conventions: 66 | ```python 67 | # Blueprint organization 68 | from flask import Blueprint 69 | from flask_security import auth_required 70 | 71 | portal = Blueprint('portal', __name__) 72 | 73 | @portal.before_request 74 | @auth_required() 75 | def before_request(): 76 | pass 77 | 78 | @portal.route('/dashboard') 79 | def dashboard(): 80 | return render_template('portal/dashboard.html') 81 | ``` 82 | 83 | ### API Design 84 | RESTful API design patterns: 85 | ```python 86 | @api.route('/resources', methods=['GET']) 87 | def get_resources(): 88 | stmt = db.session.select(Resource) 89 | resources = db.session.scalars(stmt).all() 90 | return jsonify([resource.to_dict() for resource in resources]) 91 | ``` 92 | 93 | ## Using Cursor Rules 94 | 95 | When working with Cursor IDE: 96 | 97 | 1. The AI assistant automatically understands these rules 98 | 2. Reference specific patterns in your questions 99 | 3. Ask for help with implementation details 100 | 4. Get context-aware suggestions 101 | 102 | Example prompts: 103 | - "Create a data table for users following our UI patterns" 104 | - "Show how to integrate Vue with Jinja for a product page" 105 | - "Generate a RESTful endpoint following our API standards" 106 | 107 | ## Benefits 108 | 109 | The Cursor Rules approach offers several advantages: 110 | 111 | 1. **Contextual Awareness** 112 | - AI understands your entire codebase 113 | - Suggestions consider existing patterns 114 | - Better integration with your code 115 | 116 | 2. **Flexible Generation** 117 | - Not limited to predefined templates 118 | - Adaptable to your needs 119 | - Custom implementations supported 120 | 121 | 3. **Development Environment Integration** 122 | - Works within your IDE 123 | - No external dependencies 124 | - Immediate feedback 125 | 126 | 4. **Continuous Learning** 127 | - Rules evolve with your codebase 128 | - Team knowledge integration 129 | - Pattern refinement over time 130 | 131 | ## Creating Custom Rules 132 | 133 | Extend the rules for your project: 134 | 135 | 1. Create markdown files in `cursor/rules` 136 | 2. Follow the established format 137 | 3. Include concrete examples 138 | 4. Reference existing patterns 139 | 5. Organize by domain or feature 140 | 141 | For more information about Cursor Rules or to contribute, check our [contribution guidelines](https://github.com/level09/enferno). -------------------------------------------------------------------------------- /docs/deployment.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Deployment" 3 | description: "Deploy your Enferno application to production" 4 | sidebarTitle: "Deployment" 5 | --- 6 | 7 | ## Overview 8 | 9 | Enferno provides multiple deployment options to suit different needs. You can either use the automated Enferno CLI tool for quick Ubuntu server deployments, or follow the traditional deployment process for other environments. 10 | 11 | ## Automated Deployment (Recommended) 12 | 13 | The [Enferno CLI](https://github.com/level09/enferno-cli) tool automates the entire deployment process on Ubuntu servers with a single command. 14 | 15 | ### Features 16 | 17 | - Automated server provisioning with Python 3.13+ support 18 | - Nginx configuration with SSL certificates 19 | - PostgreSQL/SQLite database setup 20 | - Systemd services configuration 21 | - User management and security setup 22 | 23 | ### Quick Start 24 | 25 | 1. Install the CLI tool: 26 | ```bash 27 | pip install enferno_cli 28 | ``` 29 | 30 | 2. Run the interactive setup: 31 | ```bash 32 | enferno setup 33 | ``` 34 | 35 | The tool will guide you through the configuration process, prompting for necessary information like server hostname, credentials, and deployment options. 36 | 37 | ## Traditional Deployment 38 | 39 | If you prefer manual deployment or need to deploy to a different environment, follow these steps: 40 | 41 | ### Prerequisites 42 | 43 | 1. Python 3.11+ 44 | 2. PostgreSQL or SQLite 45 | 3. Redis 46 | 4. Nginx 47 | 5. Systemd (for service management) 48 | 6. uv (fast Python package installer) 49 | 50 | ### Server Setup 51 | 52 | 1. Update system packages: 53 | ```bash 54 | sudo apt update 55 | sudo apt upgrade -y 56 | ``` 57 | 58 | 2. Install dependencies: 59 | ```bash 60 | sudo apt install -y python3-pip python3-venv nginx redis-server 61 | ``` 62 | 63 | 3. Install uv: 64 | ```bash 65 | pip install uv 66 | ``` 67 | 68 | 4. Install PostgreSQL (optional): 69 | ```bash 70 | sudo apt install -y postgresql postgresql-contrib 71 | ``` 72 | 73 | ### Application Setup 74 | 75 | 1. Clone and setup: 76 | ```bash 77 | git clone https://github.com/level09/enferno.git 78 | cd enferno 79 | ./setup.sh # Creates Python environment, installs requirements, and generates secure .env 80 | ``` 81 | 82 | 2. Initialize database: 83 | ```bash 84 | source .venv/bin/activate 85 | flask create-db # Setup database 86 | flask install # Create admin user 87 | ``` 88 | 89 | ### Database Configuration 90 | 91 | Enferno supports both SQLite (default) and PostgreSQL databases. Choose one of the following options: 92 | 93 | #### SQLite (Default) 94 | No additional configuration needed. The database will be created automatically when you run: 95 | ```bash 96 | flask create-db # Initialize database 97 | flask install # Create admin user 98 | ``` 99 | 100 | #### PostgreSQL 101 | 1. Create database and user: 102 | ```bash 103 | sudo -u postgres createuser -s myuser 104 | sudo -u postgres createdb mydb 105 | ``` 106 | 107 | 2. Update `.env` configuration: 108 | ```bash 109 | SQLALCHEMY_DATABASE_URI=postgresql://myuser:password@localhost/mydb 110 | ``` 111 | 112 | 3. Initialize database: 113 | ```bash 114 | flask create-db # Initialize database 115 | flask install # Create admin user 116 | ``` 117 | 118 | ### Nginx Configuration 119 | 120 | Create a new Nginx configuration: 121 | 122 | ```nginx 123 | server { 124 | listen 80; 125 | server_name yourdomain.com; 126 | 127 | location / { 128 | proxy_pass http://127.0.0.1:5000; 129 | proxy_set_header Host $host; 130 | proxy_set_header X-Real-IP $remote_addr; 131 | } 132 | 133 | location /static { 134 | alias /path/to/enferno/static; 135 | expires 30d; 136 | } 137 | } 138 | ``` 139 | 140 | ### SSL Configuration 141 | 142 | 1. Install Certbot: 143 | ```bash 144 | sudo apt install -y certbot python3-certbot-nginx 145 | ``` 146 | 147 | 2. Obtain SSL certificate: 148 | ```bash 149 | sudo certbot --nginx -d yourdomain.com 150 | ``` 151 | 152 | ### Systemd Service 153 | 154 | Create a service file `/etc/systemd/system/enferno.service`: 155 | 156 | ```ini 157 | [Unit] 158 | Description=Enferno Web Application 159 | After=network.target 160 | 161 | [Service] 162 | User=your_user 163 | WorkingDirectory=/path/to/enferno 164 | Environment="PATH=/path/to/enferno/venv/bin" 165 | ExecStart=/path/to/enferno/venv/bin/gunicorn -w 4 -b 127.0.0.1:5000 wsgi:app 166 | 167 | [Install] 168 | WantedBy=multi-user.target 169 | ``` 170 | 171 | Enable and start the service: 172 | ```bash 173 | sudo systemctl enable enferno 174 | sudo systemctl start enferno 175 | ``` 176 | 177 | ### Celery Worker Service 178 | 179 | Create `/etc/systemd/system/enferno-celery.service`: 180 | 181 | ```ini 182 | [Unit] 183 | Description=Enferno Celery Worker 184 | After=network.target 185 | 186 | [Service] 187 | User=your_user 188 | WorkingDirectory=/path/to/enferno 189 | Environment="PATH=/path/to/enferno/venv/bin" 190 | ExecStart=/path/to/enferno/venv/bin/celery -A enferno.tasks worker --loglevel=info 191 | 192 | [Install] 193 | WantedBy=multi-user.target 194 | ``` 195 | 196 | Enable and start the worker: 197 | ```bash 198 | sudo systemctl enable enferno-celery 199 | sudo systemctl start enferno-celery 200 | ``` 201 | 202 | ## Production Checklist 203 | 204 | - [ ] Set `FLASK_DEBUG=0` in `.env` 205 | - [ ] Use a secure `SECRET_KEY` 206 | - [ ] Configure proper logging 207 | - [ ] Set up monitoring (e.g., Sentry) 208 | - [ ] Configure backup strategy 209 | - [ ] Set up firewall rules 210 | - [ ] Enable rate limiting 211 | - [ ] Configure CORS settings 212 | - [ ] Set secure cookie flags 213 | - [ ] Enable HTTPS redirection 214 | 215 | ## Troubleshooting 216 | 217 | ### Common Issues 218 | 219 | 1. **Static files not found (404)** 220 | - Check Nginx static file path configuration 221 | - Verify file permissions 222 | - Run `flask static` to collect static files 223 | 224 | 2. **Database connection errors** 225 | - Verify database credentials in `.env` 226 | - Check database service status 227 | - Ensure proper permissions 228 | 229 | 3. **Application not starting** 230 | - Check systemd service logs: `sudo journalctl -u enferno` 231 | - Verify environment variables 232 | - Check application logs 233 | 234 | 4. **Redis connection issues** 235 | - Verify Redis is running: `sudo systemctl status redis` 236 | - Check Redis connection settings 237 | - Ensure proper permissions 238 | 239 | For more deployment options and troubleshooting, visit the [Enferno CLI documentation](https://github.com/level09/enferno-cli). -------------------------------------------------------------------------------- /docs/development.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Development" 3 | description: "Development guidelines and best practices for Enferno" 4 | sidebarTitle: "Development" 5 | --- 6 | 7 | ## Project Structure 8 | 9 | ``` 10 | enferno/ 11 | ├── enferno/ # Main application package 12 | │ ├── __init__.py 13 | │ ├── app.py # Application factory and configuration 14 | │ ├── settings.py # Application settings 15 | │ ├── extensions.py # Flask extensions initialization 16 | │ ├── commands.py # CLI commands 17 | │ ├── portal/ # Blueprint: Usually protected routes 18 | │ ├── public/ # Blueprint: Public routes 19 | │ ├── user/ # Blueprint: User management 20 | │ ├── tasks/ # Celery tasks 21 | │ ├── utils/ # Utility functions 22 | │ ├── static/ # Static assets 23 | │ └── templates/ # Jinja2 templates 24 | ├── docs/ # Documentation 25 | ├── nginx/ # Nginx configuration 26 | ├── instance/ # Instance-specific files 27 | ├── .env # Environment variables 28 | ├── .env-sample # Environment template 29 | ├── requirements.txt # Python dependencies 30 | ├── setup.sh # Setup script 31 | ├── run.py # Application entry point 32 | ├── Dockerfile # Docker configuration 33 | └── docker-compose.yml # Docker Compose configuration 34 | ``` 35 | 36 | ## Blueprints 37 | 38 | Enferno uses a three-blueprint architecture for better organization and security: 39 | 40 | ### 1. Portal Blueprint (`portal/`) 41 | Usually contains protected routes that require authentication. Enferno uses a pattern of protecting all routes in this blueprint automatically using `before_request`: 42 | 43 | ```python 44 | from flask import Blueprint 45 | from flask_security import auth_required 46 | 47 | portal = Blueprint('portal', __name__) 48 | 49 | # Protect all routes in this blueprint automatically 50 | @portal.before_request 51 | @auth_required() 52 | def before_request(): 53 | pass 54 | 55 | @portal.route('/dashboard') 56 | def dashboard(): 57 | return render_template('portal/dashboard.html') 58 | 59 | @portal.route('/settings') 60 | def settings(): 61 | return render_template('portal/settings.html') 62 | ``` 63 | 64 | This pattern ensures that all routes within the portal blueprint require authentication without needing to decorate each route individually. 65 | 66 | ### 2. User Blueprint (`user/`) 67 | Handles user management, authentication, and profile-related routes: 68 | ```python 69 | from flask import Blueprint 70 | from flask_security import auth_required 71 | 72 | user = Blueprint('user', __name__) 73 | 74 | @user.route('/profile') 75 | @auth_required() 76 | def profile(): 77 | return render_template('user/profile.html') 78 | ``` 79 | 80 | ### 3. Public Blueprint (`public/`) 81 | Contains routes that are publicly accessible without authentication: 82 | ```python 83 | from flask import Blueprint 84 | 85 | public = Blueprint('public', __name__) 86 | 87 | @public.route('/') 88 | def index(): 89 | return render_template('public/index.html') 90 | ``` 91 | 92 | ## Database Operations 93 | 94 | Enferno uses SQLAlchemy 2.x with the Flask-SQLAlchemy extension. The `db` instance is available from `enferno.extensions`. 95 | 96 | ### Model Definition 97 | 98 | ```python 99 | from enferno.extensions import db 100 | from datetime import datetime 101 | 102 | class Post(db.Model): 103 | id = db.Column(db.Integer, primary_key=True) 104 | title = db.Column(db.String(80), nullable=False) 105 | content = db.Column(db.Text) 106 | created_at = db.Column(db.DateTime, default=datetime.utcnow) 107 | user_id = db.Column(db.Integer, db.ForeignKey('user.id')) 108 | user = db.relationship('User', back_populates='posts') 109 | ``` 110 | 111 | ### Database Operations 112 | 113 | ```python 114 | from enferno.extensions import db 115 | from enferno.models import Post 116 | from sqlalchemy import select 117 | 118 | # Create 119 | post = Post(title='New Post', content='Content here') 120 | db.session.add(post) 121 | db.session.commit() 122 | 123 | # Simple queries 124 | stmt = db.session.select(Post) # Select all posts 125 | posts = db.session.scalars(stmt).all() 126 | 127 | post = db.session.get(Post, 1) # Get by ID 128 | 129 | # Filtered query 130 | stmt = db.session.select(Post).where(Post.title.like('%python%')) 131 | python_posts = db.session.scalars(stmt).all() 132 | 133 | # Ordered query with join 134 | stmt = ( 135 | db.session.select(Post) 136 | .join(Post.user) 137 | .order_by(Post.created_at.desc()) 138 | ) 139 | recent_posts = db.session.scalars(stmt).all() 140 | 141 | # Update 142 | post = db.session.get(Post, 1) 143 | post.title = 'Updated Title' 144 | db.session.commit() 145 | 146 | # Delete 147 | db.session.delete(post) 148 | db.session.commit() 149 | ``` 150 | 151 | ## Task Queue 152 | 153 | Enferno uses Celery for background tasks. Tasks are defined in `enferno/tasks/__init__.py`: 154 | 155 | ```python 156 | from enferno.tasks import celery 157 | 158 | @celery.task 159 | def send_email(user_id, subject, message): 160 | from enferno.extensions import db 161 | from enferno.user.models import User 162 | 163 | user = db.session.get(User, user_id) 164 | # Send email to user... 165 | return True 166 | ``` 167 | 168 | Call tasks from anywhere in your application: 169 | ```python 170 | from enferno.tasks import send_email 171 | 172 | # Call task asynchronously 173 | send_email.delay(user.id, 'Welcome', 'Welcome to Enferno!') 174 | ``` 175 | 176 | Run Celery worker: 177 | ```bash 178 | celery -A enferno.tasks worker --loglevel=info 179 | ``` 180 | 181 | ## API Development 182 | 183 | API endpoints are defined in `app/api/`. Enferno follows RESTful principles. 184 | 185 | ```python 186 | from flask import Blueprint, jsonify 187 | from app.extensions import db 188 | from app.models import Post 189 | 190 | api = Blueprint('api', __name__) 191 | 192 | @api.route('/posts') 193 | def get_posts(): 194 | query = db.select(Post).order_by(Post.created_at.desc()) 195 | posts = db.session.execute(query).scalars() 196 | return jsonify([{ 197 | 'id': post.id, 198 | 'title': post.title, 199 | 'content': post.content 200 | } for post in posts]) 201 | ``` 202 | 203 | ## Development Server 204 | 205 | Run the development server with: 206 | ```bash 207 | flask run 208 | ``` 209 | 210 | For Celery worker: 211 | ```bash 212 | celery -A enferno.tasks worker --loglevel=info 213 | ``` 214 | 215 | ## Security Best Practices 216 | 217 | 1. **Input Validation** 218 | ```python 219 | from flask_wtf import FlaskForm 220 | from wtforms import StringField 221 | from wtforms.validators import DataRequired, Length 222 | 223 | class PostForm(FlaskForm): 224 | title = StringField('Title', validators=[ 225 | DataRequired(), 226 | Length(max=80) 227 | ]) 228 | ``` 229 | 230 | 2. **CSRF Protection** 231 | ```python 232 | from flask_wtf.csrf import CSRFProtect 233 | csrf = CSRFProtect(app) 234 | ``` 235 | 236 | 3. **Authentication** 237 | ```python 238 | from flask_security import auth_required 239 | 240 | @app.route('/protected') 241 | @auth_required() 242 | def protected_route(): 243 | return 'Protected content' 244 | ``` 245 | 246 | ## Debugging 247 | 248 | Enferno includes several debugging tools: 249 | 250 | 1. **Flask Debug Toolbar** 251 | ```python 252 | from flask_debugtoolbar import DebugToolbarExtension 253 | toolbar = DebugToolbarExtension(app) 254 | ``` 255 | 256 | 2. **Logging** 257 | ```python 258 | import logging 259 | logging.basicConfig(level=logging.DEBUG) 260 | logger = logging.getLogger(__name__) 261 | ``` 262 | 263 | 3. **Database Debugging** 264 | ```python 265 | # Enable SQLAlchemy query logging 266 | logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) 267 | ``` -------------------------------------------------------------------------------- /docs/docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://mintlify.com/schema.json", 3 | "theme": "mint", 4 | "name": "Enferno Documentation", 5 | "description": "A modern Flask framework optimized for AI-assisted development workflows, enabling rapid development of sophisticated web applications.", 6 | "colors": { 7 | "primary": "#3B82F6", 8 | "light": "#60A5FA", 9 | "dark": "#2563EB" 10 | }, 11 | "favicon": "/favicon.svg", 12 | "navigation": { 13 | "tabs": [ 14 | { 15 | "tab": "Documentation", 16 | "groups": [ 17 | { 18 | "group": "Getting Started", 19 | "pages": [ 20 | "introduction", 21 | "getting-started" 22 | ] 23 | }, 24 | { 25 | "group": "Core Features", 26 | "pages": [ 27 | "authentication", 28 | "deployment" 29 | ] 30 | }, 31 | { 32 | "group": "Development", 33 | "pages": [ 34 | "development", 35 | "cursor-rules" 36 | ] 37 | } 38 | ] 39 | } 40 | ] 41 | }, 42 | "logo": { 43 | "light": "/logo/light.svg", 44 | "dark": "/logo/dark.svg" 45 | }, 46 | "navbar": { 47 | "primary": { 48 | "type": "github", 49 | "label": "GitHub", 50 | "href": "https://github.com/level09/enferno" 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /docs/enferno-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level09/enferno/81c827bd32fc06d3d83476a48e9c52f89ecb7a68/docs/enferno-demo.gif -------------------------------------------------------------------------------- /docs/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Quick Start" 3 | description: "Get up and running with Enferno" 4 | sidebarTitle: "Quick Start" 5 | --- 6 | 7 | ## Prerequisites 8 | 9 | - Python 3.11+ 10 | - Redis (for caching and sessions) 11 | - PostgreSQL (optional, SQLite works for development) 12 | - Git 13 | - uv (fast Python package installer and resolver) 14 | 15 | ## Local Setup 16 | 17 | 1. Install uv: 18 | ```bash 19 | pip install uv 20 | # Or using the installer script 21 | curl -sSf https://astral.sh/uv/install.sh | bash 22 | ``` 23 | 24 | 2. Clone and setup: 25 | ```bash 26 | git clone git@github.com:level09/enferno.git 27 | cd enferno 28 | ./setup.sh # Creates Python environment, installs requirements, and generates secure .env 29 | ``` 30 | 31 | 3. Initialize application: 32 | ```bash 33 | source .venv/bin/activate # Activate environment 34 | flask create-db # Setup database 35 | flask install # Create admin user 36 | flask run # Start development server 37 | ``` 38 | 39 | Visit `http://localhost:5000` to see your application. 40 | 41 | ## Docker Setup 42 | 43 | One-command setup with Docker: 44 | ```bash 45 | docker compose up --build 46 | ``` 47 | 48 | Includes: 49 | - Redis for caching and sessions 50 | - PostgreSQL database 51 | - Nginx for static files 52 | - Celery for background tasks 53 | 54 | ## Environment Configuration 55 | 56 | Key variables in `.env`: 57 | 58 | ```bash 59 | # Core 60 | FLASK_APP=run.py 61 | FLASK_DEBUG=1 # 0 in production 62 | SECRET_KEY=your_secret_key 63 | 64 | # Database 65 | SQLALCHEMY_DATABASE_URI=sqlite:///enferno.sqlite3 66 | # Or: postgresql://username:password@localhost/dbname 67 | 68 | # Redis & Celery 69 | REDIS_URL=redis://localhost:6379/0 70 | CELERY_BROKER_URL=redis://localhost:6379/1 71 | CELERY_RESULT_BACKEND=redis://localhost:6379/2 72 | ``` 73 | 74 | ## Next Steps 75 | 76 | - [Authentication](/authentication) - User management and OAuth setup 77 | - [Deployment](/deployment) - Production deployment guide 78 | - [Development](/development) - Development guidelines and best practices -------------------------------------------------------------------------------- /docs/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Enferno Framework" 3 | description: "A modern Flask framework optimized for AI-assisted development" 4 | sidebarTitle: "Introduction" 5 | --- 6 | 7 | ## Overview 8 | 9 | Enferno is a modern Flask framework optimized for AI-assisted development workflows. By combining carefully crafted development patterns, smart Cursor Rules, and modern libraries, it enables developers to build sophisticated web applications with unprecedented speed. Whether you're using AI-powered IDEs like Cursor or traditional tools, Enferno's intelligent patterns and contextual guides help you create production-ready SAAS applications faster than ever. 10 | 11 | ## Key Features 12 | 13 | - **Modern Stack**: Python 3.11+, Flask, Vue 3, Vuetify 3 14 | - **Authentication**: Flask-Security with role-based access control 15 | - **OAuth Integration**: Google and GitHub login via Flask-Dance 16 | - **Database**: SQLAlchemy ORM with PostgreSQL/SQLite support 17 | - **Task Queue**: Celery with Redis for background tasks 18 | - **Frontend**: Client-side Vue.js with Vuetify components 19 | - **Docker Ready**: Production-grade Docker configuration 20 | - **Cursor Rules**: Smart IDE-based code generation and assistance 21 | - **Package Management**: Fast installation with uv 22 | 23 | ## Documentation Sections 24 | 25 | - [Getting Started](/getting-started) - Installation and setup guide 26 | - [Authentication](/authentication) - User management and OAuth setup 27 | - [Development](/development) - Development guidelines and best practices 28 | - [Deployment](/deployment) - Production deployment guide 29 | 30 | ## OAuth Integration 31 | 32 | Supports social login with: 33 | - Google (profile and email scope) 34 | - GitHub (user:email scope) 35 | 36 | Configure in `.env`: 37 | ```bash 38 | # Google OAuth 39 | GOOGLE_AUTH_ENABLED=true 40 | GOOGLE_OAUTH_CLIENT_ID=your_client_id 41 | GOOGLE_OAUTH_CLIENT_SECRET=your_client_secret 42 | 43 | # GitHub OAuth 44 | GITHUB_AUTH_ENABLED=true 45 | GITHUB_OAUTH_CLIENT_ID=your_client_id 46 | GITHUB_OAUTH_CLIENT_SECRET=your_client_secret 47 | ``` 48 | 49 | ## Cursor Rules 50 | 51 | Enferno now leverages Cursor Rules for intelligent code assistance and generation. This approach provides: 52 | 53 | - Context-aware code generation through modern AI-powered IDEs 54 | - Codebase-specific guidance tailored to Enferno's patterns 55 | - Improved documentation integrated with development tools 56 | - Framework-specific best practices and conventions 57 | - More flexible workflow than template-based generation 58 | 59 | Rules are organized by domain areas like Vue-Jinja integration and UI components to provide targeted assistance exactly when needed. 60 | 61 | ## Source Code 62 | 63 | The source code is available on [GitHub](https://github.com/level09/enferno). 64 | 65 | ## Contributing 66 | 67 | We welcome contributions! Please read our [Contributing Guide](https://github.com/level09/enferno/blob/master/CONTRIBUTING.md) for details. -------------------------------------------------------------------------------- /docs/logo/dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/logo/light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs>=1.5.3,<2.0.0 2 | pygments>=2.16.1,<3.0.0 -------------------------------------------------------------------------------- /docs/roles-management.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level09/enferno/81c827bd32fc06d3d83476a48e9c52f89ecb7a68/docs/roles-management.jpg -------------------------------------------------------------------------------- /docs/users-management.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level09/enferno/81c827bd32fc06d3d83476a48e9c52f89ecb7a68/docs/users-management.jpg -------------------------------------------------------------------------------- /enferno/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Enferno Framework 3 | A modern Python web framework optimized for AI-assisted development. 4 | """ 5 | 6 | __version__ = '11.2.0' # Incrementing to version 11.2.0 to reflect Docker improvements 7 | -------------------------------------------------------------------------------- /enferno/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inspect 3 | 4 | import click 5 | from flask import Flask, render_template 6 | from enferno.settings import Config 7 | from flask_security import Security, SQLAlchemyUserDatastore, current_user 8 | from enferno.user.models import User, Role, WebAuthn, OAuth 9 | from enferno.user.forms import ExtendedRegisterForm 10 | from enferno.extensions import cache, db, mail, debug_toolbar, session, babel 11 | from enferno.public.views import public 12 | from enferno.user.views import bp_user 13 | from enferno.portal.views import portal 14 | from flask_dance.contrib.google import make_google_blueprint 15 | from flask_dance.contrib.github import make_github_blueprint 16 | from flask_dance.consumer.storage.sqla import SQLAlchemyStorage 17 | import enferno.commands as commands 18 | 19 | 20 | def create_app(config_object=Config): 21 | app = Flask(__name__) 22 | app.config.from_object(config_object) 23 | 24 | register_blueprints(app) 25 | register_extensions(app) 26 | register_errorhandlers(app) 27 | register_shellcontext(app) 28 | register_commands(app, commands) 29 | return app 30 | 31 | def locale_selector(): 32 | return 'en' 33 | 34 | def register_extensions(app): 35 | cache.init_app(app) 36 | db.init_app(app) 37 | user_datastore = SQLAlchemyUserDatastore(db, User, Role, webauthn_model=WebAuthn) 38 | security = Security(app, user_datastore, register_form=ExtendedRegisterForm) 39 | mail.init_app(app) 40 | debug_toolbar.init_app(app) 41 | 42 | # Session initialization 43 | session.init_app(app) 44 | 45 | babel.init_app(app, locale_selector=locale_selector, default_domain="messages", default_locale="en") 46 | 47 | return None 48 | 49 | 50 | def register_blueprints(app): 51 | app.register_blueprint(bp_user) 52 | app.register_blueprint(public) 53 | app.register_blueprint(portal) 54 | 55 | # Setup OAuth if enabled 56 | if app.config.get('GOOGLE_AUTH_ENABLED') and app.config.get('GOOGLE_OAUTH_CLIENT_ID'): 57 | google_bp = make_google_blueprint( 58 | client_id=app.config.get('GOOGLE_OAUTH_CLIENT_ID'), 59 | client_secret=app.config.get('GOOGLE_OAUTH_CLIENT_SECRET'), 60 | scope=[ 61 | "https://www.googleapis.com/auth/userinfo.profile", 62 | "https://www.googleapis.com/auth/userinfo.email", 63 | "openid" 64 | ], 65 | reprompt_select_account=False 66 | ) 67 | google_bp.storage = SQLAlchemyStorage(OAuth, db.session, user=current_user) 68 | app.register_blueprint(google_bp, url_prefix="/login") 69 | 70 | if app.config.get('GITHUB_AUTH_ENABLED') and app.config.get('GITHUB_OAUTH_CLIENT_ID'): 71 | github_bp = make_github_blueprint( 72 | client_id=app.config.get('GITHUB_OAUTH_CLIENT_ID'), 73 | client_secret=app.config.get('GITHUB_OAUTH_CLIENT_SECRET'), 74 | scope=["user:email"] 75 | ) 76 | github_bp.storage = SQLAlchemyStorage(OAuth, db.session, user=current_user) 77 | app.register_blueprint(github_bp, url_prefix="/login") 78 | 79 | return None 80 | 81 | 82 | def register_errorhandlers(app): 83 | def render_error(error): 84 | error_code = getattr(error, 'code', 500) 85 | return render_template("{0}.html".format(error_code)), error_code 86 | 87 | for errcode in [401, 404, 500]: 88 | app.errorhandler(errcode)(render_error) 89 | return None 90 | 91 | 92 | def register_shellcontext(app): 93 | """Register shell context objects.""" 94 | 95 | def shell_context(): 96 | """Shell context objects.""" 97 | return { 98 | 'db': db, 99 | 'User': User, 100 | 'Role': Role 101 | } 102 | 103 | app.shell_context_processor(shell_context) 104 | 105 | 106 | def register_commands(app: Flask, commands_module): 107 | """ 108 | Automatically register all Click commands and command groups in the given module. 109 | 110 | Args: 111 | - app: Flask application instance to register commands to. 112 | - commands_module: The module containing Click commands and command groups. 113 | """ 114 | for name, obj in inspect.getmembers(commands_module): 115 | if isinstance(obj, (click.Command, click.Group)): 116 | app.cli.add_command(obj) 117 | -------------------------------------------------------------------------------- /enferno/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Click commands.""" 3 | 4 | import click 5 | from flask.cli import with_appcontext, AppGroup 6 | from flask_security.utils import hash_password 7 | from rich.console import Console 8 | import secrets 9 | import string 10 | import os 11 | 12 | from enferno.extensions import db 13 | from enferno.user.models import User 14 | 15 | console = Console() 16 | 17 | 18 | @click.command() 19 | @with_appcontext 20 | def create_db(): 21 | """creates db tables - import your models within commands.py to create the models. 22 | """ 23 | db.create_all() 24 | print('Database structure created successfully') 25 | 26 | 27 | @click.command() 28 | @with_appcontext 29 | def install(): 30 | """Install a default admin user and add an admin role to it. 31 | """ 32 | # check if admin exists 33 | from enferno.user.models import Role 34 | # create admin role if it doesn't exist 35 | admin_role = Role.query.filter(Role.name == 'admin').first() 36 | if not admin_role: 37 | admin_role = Role(name='admin').save() 38 | 39 | # check if admin users already installed 40 | admin_user = User.query.filter(User.roles.any(Role.name == 'admin')).first() 41 | if admin_user: 42 | console.print(f"[yellow]An admin user already exists:[/] [blue]{admin_user.username}[/]") 43 | return 44 | 45 | # else : create a new admin user 46 | username = click.prompt('Admin username', default='admin') 47 | # Generate a secure password 48 | password = ''.join(secrets.choice(string.ascii_letters + string.digits + '@#$%^&*') for _ in range(32)) 49 | 50 | user = User(username=username, name='Super Admin', password=hash_password(password), active=1) 51 | user.roles.append(admin_role) 52 | user.save() 53 | 54 | console.print("\n[green]✓[/] Admin user created successfully!") 55 | console.print(f"[blue]Username:[/] {username}") 56 | console.print(f"[blue]Password:[/] [red]{password}[/]") 57 | console.print("\n[yellow]⚠️ Please save this password securely - you will not see it again![/]") 58 | 59 | 60 | @click.command() 61 | @click.option('-e', '--email', prompt=True, default=None) 62 | @click.option('-p', '--password', prompt=True, default=None) 63 | @with_appcontext 64 | def create(email, password): 65 | """Creates a user using an email. 66 | """ 67 | a = User.query.filter(User.email == email).first() 68 | if a != None: 69 | print('User already exists!') 70 | else: 71 | user = User(email=email, password=hash_password(password), active=True) 72 | user.save() 73 | 74 | 75 | @click.command() 76 | @click.option('-e', '--email', prompt=True, default=None) 77 | @click.option('-r', '--role', prompt=True, default='admin') 78 | @with_appcontext 79 | def add_role(email, role): 80 | """Adds a role to the specified user. 81 | """ 82 | from enferno.user.models import Role 83 | u = User.query.filter(User.email == email).first() 84 | 85 | if u is None: 86 | print('Sorry, this user does not exist!') 87 | else: 88 | r = db.session.execute(db.select(Role).filter_by(name=role)).scalar_one() 89 | if r is None: 90 | print('Sorry, this role does not exist!') 91 | u = click.prompt('Would you like to create one? Y/N', default='N') 92 | if u.lower() == 'y': 93 | r = Role(name=role) 94 | try: 95 | db.session.add(r) 96 | db.session.commit() 97 | print('Role created successfully, you may add it now to the user') 98 | except Exception as e: 99 | db.session.rollback() 100 | # add role to user 101 | u.roles.append(r) 102 | 103 | 104 | @click.command() 105 | @click.option('-e', '--email', prompt='Email or username', default=None) 106 | @click.option('-p', '--password', hide_input=True, prompt=True, default=None) 107 | @with_appcontext 108 | def reset(email, password): 109 | """Reset a user password using email or username 110 | """ 111 | try: 112 | pwd = hash_password(password) 113 | # Check if user exists with provided email or username 114 | u = User.query.filter((User.email == email) | (User.username == email)).first() 115 | if not u: 116 | print(f'User with email or username "{email}" not found.') 117 | return 118 | 119 | u.password = pwd 120 | try: 121 | db.session.commit() 122 | print('User password has been reset successfully.') 123 | except: 124 | db.session.rollback() 125 | print('Error committing to database.') 126 | except Exception as e: 127 | print('Error resetting user password: %s' % e) 128 | 129 | 130 | i18n_cli = AppGroup('i18n') 131 | 132 | 133 | @i18n_cli.command() 134 | @click.argument('lang') 135 | def init(lang): 136 | """Initialize a new language""" 137 | if os.system("pybabel init -i messages.pot -d enferno/translations -l " + lang): 138 | raise RuntimeError("init command failed") 139 | 140 | 141 | @i18n_cli.command() 142 | def extract(): 143 | """Extract messages from code""" 144 | if os.system("pybabel extract -F babel.cfg -k _l -o messages.pot ."): 145 | raise RuntimeError("Extract command failed") 146 | 147 | 148 | @i18n_cli.command() 149 | def update(): 150 | """Update translations""" 151 | if os.system("pybabel update -i messages.pot -d enferno/translations"): 152 | raise RuntimeError("Update command failed") 153 | 154 | 155 | @i18n_cli.command() 156 | def compile(): 157 | """Compile translations""" 158 | if os.system("pybabel compile -d enferno/translations"): 159 | raise RuntimeError("Compile command failed") -------------------------------------------------------------------------------- /enferno/extensions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Extensions module. Each extension is initialized in the app factory located 3 | in app.py 4 | """ 5 | from sqlalchemy.orm import DeclarativeBase 6 | 7 | class BaseModel(DeclarativeBase): 8 | pass 9 | 10 | from flask_sqlalchemy import SQLAlchemy 11 | db = SQLAlchemy(model_class=BaseModel) 12 | 13 | from flask_caching import Cache 14 | cache = Cache() 15 | 16 | from flask_mail import Mail 17 | mail = Mail() 18 | 19 | from flask_debugtoolbar import DebugToolbarExtension 20 | debug_toolbar = DebugToolbarExtension() 21 | 22 | from flask_session import Session 23 | session = Session() 24 | 25 | from flask_babel import Babel 26 | babel = Babel() -------------------------------------------------------------------------------- /enferno/portal/views.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask.templating import render_template 3 | from flask_security import auth_required 4 | 5 | portal = Blueprint('portal', __name__, static_folder='../static') 6 | 7 | @portal.before_request 8 | @auth_required('session') 9 | def before_request(): 10 | pass 11 | @portal.after_request 12 | def add_header(response): 13 | response.headers['Cache-Control'] = 'public, max-age=10800' 14 | return response 15 | 16 | 17 | @portal.route('/dashboard/') 18 | def dashboard(): 19 | return render_template('dashboard.html') 20 | 21 | -------------------------------------------------------------------------------- /enferno/public/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level09/enferno/81c827bd32fc06d3d83476a48e9c52f89ecb7a68/enferno/public/__init__.py -------------------------------------------------------------------------------- /enferno/public/models.py: -------------------------------------------------------------------------------- 1 | from enferno.extensions import db 2 | 3 | 4 | 5 | #your models can go here 6 | ''' 7 | class MyModel(db.Model): 8 | id = db.Column(db.Integer, primary_key=True) 9 | created_at = db.Column(db.DateTime, default=datetime.datetime.now, nullable=False) 10 | field = db.Column(db.String(255), nullable=False) 11 | 12 | ''' -------------------------------------------------------------------------------- /enferno/public/views.py: -------------------------------------------------------------------------------- 1 | from flask import request, Blueprint, send_from_directory, flash, redirect, url_for, current_app 2 | from flask.templating import render_template 3 | from flask_security import login_user, current_user, logout_user 4 | from flask_dance.consumer import oauth_authorized, oauth_error 5 | from sqlalchemy.orm.exc import NoResultFound 6 | from enferno.extensions import db 7 | from enferno.user.models import User, OAuth 8 | import datetime 9 | from oauthlib.oauth2.rfc6749.errors import OAuth2Error 10 | 11 | public = Blueprint('public',__name__, static_folder='../static') 12 | 13 | def get_real_ip(): 14 | """Get real IP address with Cloudflare and proxy support.""" 15 | # First check for Cloudflare 16 | cf_connecting_ip = request.headers.get('CF-Connecting-IP') 17 | if cf_connecting_ip: 18 | return cf_connecting_ip 19 | 20 | # Then check X-Forwarded-For 21 | x_forwarded_for = request.headers.get('X-Forwarded-For') 22 | if x_forwarded_for: 23 | # Get the first IP in the list 24 | return x_forwarded_for.split(',')[0].strip() 25 | 26 | # Then check X-Real-IP 27 | x_real_ip = request.headers.get('X-Real-IP') 28 | if x_real_ip: 29 | return x_real_ip 30 | 31 | # Finally fall back to remote_addr 32 | return request.remote_addr 33 | 34 | def update_user_login_info(user, ip_address): 35 | """Update user login tracking information.""" 36 | now = datetime.datetime.now() 37 | user.last_login_at = user.current_login_at 38 | user.current_login_at = now 39 | user.last_login_ip = user.current_login_ip 40 | user.current_login_ip = ip_address 41 | user.login_count = (user.login_count or 0) + 1 42 | db.session.add(user) 43 | db.session.commit() 44 | 45 | def create_oauth_user(provider_data, oauth_token, ip_address): 46 | """Create a new user from OAuth data.""" 47 | now = datetime.datetime.now() 48 | user = User( 49 | email=provider_data.get("email"), 50 | username=provider_data.get("email"), 51 | name=provider_data.get("name", ""), 52 | password=User.random_password(), 53 | active=True, 54 | confirmed_at=now, 55 | current_login_at=now, 56 | current_login_ip=ip_address, 57 | login_count=1 58 | ) 59 | return user 60 | 61 | def get_oauth_user_data(blueprint): 62 | """Get user data from OAuth provider.""" 63 | if not blueprint: 64 | return None 65 | 66 | if blueprint.name == 'google': 67 | resp = blueprint.session.get("/oauth2/v2/userinfo") 68 | if not resp.ok: 69 | current_app.logger.error(f"Failed to fetch user info: {resp.text}") 70 | return None 71 | return resp.json() 72 | elif blueprint.name == 'github': 73 | # Get user profile 74 | resp = blueprint.session.get("/user") 75 | if not resp.ok: 76 | current_app.logger.error(f"Failed to fetch GitHub user info: {resp.text}") 77 | return None 78 | data = resp.json() 79 | 80 | # GitHub doesn't return email in basic profile if private, need separate call 81 | if not data.get('email'): 82 | email_resp = blueprint.session.get("/user/emails") 83 | if email_resp.ok: 84 | emails = email_resp.json() 85 | primary_email = next((email['email'] for email in emails if email['primary']), None) 86 | if primary_email: 87 | data['email'] = primary_email 88 | 89 | # Normalize the data structure to match our needs 90 | return { 91 | 'id': str(data['id']), # Convert to string to match Google's ID format 92 | 'email': data.get('email'), 93 | 'name': data.get('name') or data.get('login'), # Fallback to username if name not set 94 | } 95 | return None 96 | 97 | @public.route('/') 98 | def index(): 99 | return render_template('index.html') 100 | 101 | @public.route('/robots.txt') 102 | def static_from_root(): 103 | return send_from_directory(public.static_folder, request.path[1:]) 104 | 105 | # Handle pre-OAuth login check 106 | def before_oauth_login(): 107 | if request.endpoint in ['google.login', 'github.login'] and current_user.is_authenticated: 108 | flash("Please sign out before proceeding.", category="warning") 109 | return redirect(url_for('portal.dashboard')) 110 | 111 | # Register the check for all OAuth routes 112 | public.before_app_request(before_oauth_login) 113 | 114 | @oauth_authorized.connect 115 | def oauth_logged_in(blueprint, token): 116 | if not token: 117 | current_app.logger.error(f"OAuth login failed: No token received for {blueprint.name}") 118 | flash("Authentication failed.", category="error") 119 | return False 120 | 121 | try: 122 | # Get user info from provider 123 | provider_data = get_oauth_user_data(blueprint) 124 | if not provider_data: 125 | return False 126 | 127 | user_id = provider_data["id"] 128 | real_ip = get_real_ip() 129 | 130 | # First check if we already have OAuth entry 131 | query = OAuth.query.filter_by( 132 | provider=blueprint.name, 133 | provider_user_id=user_id 134 | ) 135 | try: 136 | oauth = query.one() 137 | except NoResultFound: 138 | oauth = OAuth( 139 | provider=blueprint.name, 140 | provider_user_id=user_id, 141 | token=token 142 | ) 143 | 144 | if oauth.user: 145 | if current_user.is_authenticated and current_user.id != oauth.user.id: 146 | logout_user() 147 | update_user_login_info(oauth.user, real_ip) 148 | login_user(oauth.user) 149 | else: 150 | # Check if user exists with this email 151 | existing_user = User.query.filter_by(email=provider_data.get("email")).first() 152 | if existing_user: 153 | # Link OAuth to existing user 154 | oauth.user = existing_user 155 | db.session.add(oauth) 156 | db.session.commit() 157 | update_user_login_info(existing_user, real_ip) 158 | login_user(existing_user) 159 | flash("Account linked successfully.", category="success") 160 | else: 161 | # Create new user 162 | user = create_oauth_user(provider_data, token, real_ip) 163 | oauth.user = user 164 | db.session.add_all([user, oauth]) 165 | db.session.commit() 166 | login_user(user) 167 | flash("Successfully signed in.") 168 | 169 | return redirect(url_for("portal.dashboard")) 170 | 171 | except OAuth2Error as e: 172 | current_app.logger.error(f"OAuth2Error during {blueprint.name} login: {str(e)}") 173 | flash("Authentication failed.", category="error") 174 | return False 175 | 176 | @oauth_error.connect 177 | def oauth_error(blueprint, message, response): 178 | current_app.logger.error(f"OAuth error from {blueprint.name}: {message}, Response: {response}") 179 | flash("Authentication failed.", category="error") -------------------------------------------------------------------------------- /enferno/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import timedelta 3 | 4 | import bleach 5 | import os 6 | import redis 7 | from dotenv import load_dotenv 8 | 9 | os_env = os.environ 10 | load_dotenv() 11 | 12 | 13 | def uia_username_mapper(identity): 14 | # we allow pretty much anything - but we bleach it. 15 | return bleach.clean(identity, strip=True) 16 | 17 | 18 | class Config(object): 19 | SECRET_KEY = os.environ.get('SECRET_KEY', '3nF3Rn0') 20 | APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory 21 | PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) 22 | DEBUG_TB_ENABLED = os.environ.get('DEBUG_TB_ENABLED') 23 | DEBUG_TB_INTERCEPT_REDIRECTS = False 24 | CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc. 25 | SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql:///enferno') 26 | # for postgres 27 | # SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI', 'postgresql:///enferno') 28 | SQLALCHEMY_TRACK_MODIFICATIONS = True 29 | 30 | CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/2') 31 | CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/3') 32 | 33 | # security 34 | SECURITY_REGISTERABLE = True 35 | SECURITY_RECOVERABLE = False 36 | SECURITY_CONFIRMABLE = False 37 | SECURITY_CHANGEABLE = True 38 | SECURITY_TRACKABLE = True 39 | SECURITY_PASSWORD_HASH = 'pbkdf2_sha512' 40 | SECURITY_PASSWORD_SALT = os.environ.get('SECURITY_PASSWORD_SALT', 'e89c4039b51f72b0519d1ee033ff537c7c48902e1f497f74c7a0923c9e4e0996') 41 | SECURITY_USER_IDENTITY_ATTRIBUTES = [{"username": {"mapper": uia_username_mapper, "case_insensitive": True}}, ] 42 | SECURITY_USERNAME_ENABLE = True 43 | 44 | SECURITY_POST_LOGIN_VIEW = '/dashboard' 45 | SECURITY_POST_CONFIRM_VIEW = '/dashboard' 46 | SECURITY_POST_REGISTER_VIEW = '/login' 47 | 48 | SECURITY_MULTI_FACTOR_RECOVERY_CODES = True 49 | SECURITY_MULTI_FACTOR_RECOVERY_CODES_N = 3 50 | SECURITY_MULTI_FACTOR_RECOVERY_CODES_KEYS = None 51 | SECURITY_MULTI_FACTOR_RECOVERY_CODE_TTL = None 52 | 53 | SECURITY_TWO_FACTOR_ENABLED_METHODS = [ 54 | "authenticator" 55 | ] 56 | SECURITY_TWO_FACTOR = True 57 | SECURITY_API_ENABLED_METHODS = ["session"] 58 | 59 | SECURITY_FRESHNESS = timedelta(minutes=60) 60 | SECURITY_FRESHNESS_GRACE_PERIOD = timedelta(minutes=60) 61 | SECURITY_PASSWORD_LENGTH_MIN = 12 62 | 63 | SECURITY_TOTP_SECRETS = {"1": os.environ.get("SECURITY_TOTP_SECRETS")} 64 | SECURITY_TOTP_ISSUER = "Enferno" 65 | 66 | SECURITY_WEBAUTHN = True 67 | SECURITY_WAN_ALLOW_AS_FIRST_FACTOR = True 68 | SECURITY_WAN_ALLOW_AS_MULTI_FACTOR = True 69 | SECURITY_WAN_ALLOW_AS_VERIFY = ["first", "secondary"] 70 | SECURITY_WAN_ALLOW_USER_HINTS = True 71 | 72 | SESSION_PROTECTION = "strong" 73 | 74 | # Session configuration 75 | SESSION_TYPE = 'redis' 76 | SESSION_REDIS = redis.from_url(os.environ.get('REDIS_SESSION', 'redis://localhost:6379/1')) 77 | SESSION_KEY_PREFIX = 'session:' 78 | SESSION_USE_SIGNER = True 79 | PERMANENT_SESSION_LIFETIME = 3600 80 | SESSION_COOKIE_SECURE = os.environ.get('SESSION_COOKIE_SECURE', 'False').lower() == 'true' 81 | SESSION_COOKIE_HTTPONLY = os.environ.get('SESSION_COOKIE_HTTPONLY', 'True').lower() == 'true' 82 | SESSION_COOKIE_SAMESITE = os.environ.get('SESSION_COOKIE_SAMESITE', 'Lax') 83 | 84 | # flask mail settings 85 | MAIL_SERVER = os.environ.get('MAIL_SERVER') 86 | MAIL_PORT = 465 87 | MAIL_USE_SSL = True 88 | MAIL_USERNAME = os.environ.get('MAIL_USERNAME') 89 | MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') 90 | SECURITY_EMAIL_SENDER = os.environ.get('SECURITY_EMAIL_SENDER', 'info@domain.com') 91 | SECURITY_SEND_PASSWORD_CHANGE_EMAIL = False 92 | 93 | # Google OAuth Settings 94 | GOOGLE_AUTH_ENABLED = os.environ.get('GOOGLE_AUTH_ENABLED', 'False').lower() == 'true' 95 | GOOGLE_OAUTH_CLIENT_ID = os.environ.get('GOOGLE_OAUTH_CLIENT_ID') 96 | GOOGLE_OAUTH_CLIENT_SECRET = os.environ.get('GOOGLE_OAUTH_CLIENT_SECRET') 97 | GOOGLE_OAUTH_REDIRECT_URI = os.environ.get('GOOGLE_OAUTH_REDIRECT_URI') # Let the OAuth handler construct it dynamically 98 | OAUTHLIB_INSECURE_TRANSPORT = os.environ.get('OAUTHLIB_INSECURE_TRANSPORT', '1') # Remove in production 99 | 100 | # GitHub OAuth Settings 101 | GITHUB_AUTH_ENABLED = os.environ.get('GITHUB_AUTH_ENABLED', 'False').lower() == 'true' 102 | GITHUB_OAUTH_CLIENT_ID = os.environ.get('GITHUB_OAUTH_CLIENT_ID') 103 | GITHUB_OAUTH_CLIENT_SECRET = os.environ.get('GITHUB_OAUTH_CLIENT_SECRET') -------------------------------------------------------------------------------- /enferno/static/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } -------------------------------------------------------------------------------- /enferno/static/img/auth-bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level09/enferno/81c827bd32fc06d3d83476a48e9c52f89ecb7a68/enferno/static/img/auth-bg.webp -------------------------------------------------------------------------------- /enferno/static/img/enferno.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /enferno/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level09/enferno/81c827bd32fc06d3d83476a48e9c52f89ecb7a68/enferno/static/img/favicon.ico -------------------------------------------------------------------------------- /enferno/static/js/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enferno Framework - Central Configuration 3 | * Coinbase-inspired color palette 4 | */ 5 | 6 | 7 | const config = { 8 | // Common Vue settings 9 | delimiters: ['${', '}'], 10 | 11 | // Vuetify configuration 12 | vuetifyConfig: { 13 | defaults: { 14 | VTextField: { 15 | variant: 'outlined' 16 | }, 17 | VSelect: { 18 | variant: 'outlined' 19 | }, 20 | VTextarea: { 21 | variant: 'outlined' 22 | }, 23 | VCombobox: { 24 | variant: 'outlined' 25 | }, 26 | VChip: { 27 | size: 'small' 28 | }, 29 | VCard: { 30 | elevation: 0 31 | }, 32 | VBtn: { 33 | variant: 'elevated', 34 | size: 'small' 35 | }, 36 | VDataTableServer: { 37 | itemsPerPage: 25 , 38 | itemsPerPageOptions: [ 25, 50, 100] 39 | } 40 | }, 41 | theme: { 42 | defaultTheme: window.__settings__?.dark ? 'dark' : 'light', 43 | themes: { 44 | light: { 45 | dark: false, 46 | colors: { 47 | // Coinbase-inspired colors 48 | primary: '#0052FF', // Coinbase Blue 49 | secondary: '#1652F0', // Secondary Blue 50 | accent: '#05D2DD', // Teal Accent 51 | error: '#FF7452', // Error Red 52 | info: '#56B4FC', // Light Blue 53 | success: '#05BE7A', // Green 54 | warning: '#F6B74D', // Orange/Yellow 55 | background: '#FFFFFF', // White 56 | surface: '#F9FBFD', // Light Gray 57 | } 58 | }, 59 | dark: { 60 | dark: true, 61 | colors: { 62 | // Coinbase dark theme 63 | primary: '#1652F0', // Coinbase Blue (slightly darker) 64 | secondary: '#0A46E4', // Secondary Blue 65 | accent: '#00B4D8', // Teal Accent 66 | error: '#E94B35', // Error Red 67 | info: '#3A9BF4', // Light Blue 68 | success: '#00A661', // Green 69 | warning: '#DEA54B', // Orange/Yellow 70 | background: '#0A0B0D', // Very Dark Gray (near black) 71 | surface: '#1E2026', // Dark Gray for cards 72 | } 73 | } 74 | } 75 | }, 76 | icons: { 77 | defaultSet: 'mdi' 78 | } 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /enferno/static/mdi/.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Disclaimer: 2 | Hi there, thanks for contributing! Before anything else, please ensure you didn't mean to create an issue on the main MaterialDesign repo instead. 3 | If this is intentional, just erase this message. Thanks! 4 | -------------------------------------------------------------------------------- /enferno/static/mdi/fonts/materialdesignicons-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level09/enferno/81c827bd32fc06d3d83476a48e9c52f89ecb7a68/enferno/static/mdi/fonts/materialdesignicons-webfont.eot -------------------------------------------------------------------------------- /enferno/static/mdi/fonts/materialdesignicons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level09/enferno/81c827bd32fc06d3d83476a48e9c52f89ecb7a68/enferno/static/mdi/fonts/materialdesignicons-webfont.ttf -------------------------------------------------------------------------------- /enferno/static/mdi/fonts/materialdesignicons-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level09/enferno/81c827bd32fc06d3d83476a48e9c52f89ecb7a68/enferno/static/mdi/fonts/materialdesignicons-webfont.woff -------------------------------------------------------------------------------- /enferno/static/mdi/fonts/materialdesignicons-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level09/enferno/81c827bd32fc06d3d83476a48e9c52f89ecb7a68/enferno/static/mdi/fonts/materialdesignicons-webfont.woff2 -------------------------------------------------------------------------------- /enferno/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * -------------------------------------------------------------------------------- /enferno/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from celery import Celery 4 | from enferno.settings import Config as cfg 5 | 6 | celery = Celery('enferno.tasks', broker=cfg.CELERY_BROKER_URL, backend=cfg.CELERY_RESULT_BACKEND, 7 | broker_connection_retry_on_startup=True) 8 | 9 | celery.conf.add_defaults(cfg) 10 | 11 | 12 | class ContextTask(celery.Task): 13 | abstract = True 14 | 15 | def __call__(self, *args, **kwargs): 16 | from enferno.app import create_app 17 | with create_app(cfg).app_context(): 18 | return super(ContextTask, self).__call__(*args, **kwargs) 19 | 20 | 21 | celery.Task = ContextTask 22 | 23 | 24 | @celery.task 25 | def task(): 26 | pass 27 | -------------------------------------------------------------------------------- /enferno/templates/401.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | 4 | {% block page_title %}Unauthorized{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

401

10 |

You are not authorized to see this page. 11 |

12 |
13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /enferno/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | {% block page_title %}{% endblock %} 3 | 4 | {% block content %} 5 | 6 |
7 |

404

8 |

Sorry, that page doesn't exist.

9 | 10 |
11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /enferno/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | 4 | {% block page_title %}Server error{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

500

10 |

Sorry, something went wrong on our system. Don't panic, we are fixing it! Please try again later.

11 |
12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /enferno/templates/auth_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Project Enferno 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% block css %}{% endblock %} 16 | 17 | 18 | {% block content %}{% endblock %} 19 | 20 | 21 | 22 | 23 | {% block js %}{% endblock %} 24 | 25 | -------------------------------------------------------------------------------- /enferno/templates/cms/activities.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block css %} 3 | {% endblock %} 4 | {% block sidebar %} 5 | {% endblock %} 6 | {% block layout_classes %} align-center {% endblock %} 7 | {% block content %} 8 | 9 | 10 | 11 | Activity Logs 12 | 13 | 14 | 15 | 16 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Activity Data 40 | 41 | 42 | 43 | 44 | 45 |
${JSON.stringify(currentData, null, 2)}
46 |
47 |
48 |
49 | 50 | 51 | ${snackMessage} 52 | 55 | 56 | 57 | {% endblock %} {% block js %} 58 | 59 | 140 | {% endblock %} -------------------------------------------------------------------------------- /enferno/templates/cms/roles.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block css %} 3 | {% endblock %} 4 | {% block sidebar %} 5 | {% endblock %} 6 | {% block layout_classes %} align-center {% endblock %} 7 | {% block content %} 8 | 9 | 10 | 11 | Roles Dashboard 12 | 13 | 14 | 15 | 16 | 25 | 30 | 31 | 34 | 35 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Role Editor 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | Save 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | ${snackMessage} 82 | 85 | 86 | 87 | {% endblock %} {% block js %} 88 | 89 | 217 | {% endblock %} 218 | -------------------------------------------------------------------------------- /enferno/templates/cms/users.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} {% block css %} {% endblock %} {% block sidebar %} 2 | {% endblock %} {% block layout_classes %} align-center {% endblock %} 3 | {% block content %} 4 | 5 | 6 | 7 | Users Dashboard 8 | 9 | 10 | 11 | 12 | 21 | 22 | 28 | 34 | 35 | 40 | 41 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | User Editor 69 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | Save 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | ${snackMessage} 110 | 113 | 114 | 115 | 116 | {% endblock %} {% block js %} 117 | 118 | 122 | 123 | 266 | {% endblock %} 267 | -------------------------------------------------------------------------------- /enferno/templates/core/api.jinja2: -------------------------------------------------------------------------------- 1 | @portal.route('/projects/') 2 | def projects(): 3 | return render_template('cms/projects.html') 4 | 5 | @portal.post('/api/projects') 6 | def api_projects(): 7 | 8 | options = request.json.get('options', {}) 9 | page = options.get('page', 1) 10 | per_page = options.get('itemsPerPage', PER_PAGE) 11 | q = request.json.get('q', '') 12 | 13 | query = db.session.query(Project) 14 | 15 | 16 | 17 | pagination = query.paginate(page=page, per_page=per_page, error_out=False) 18 | 19 | items = 20 | 21 | response_data = { 22 | 'items': items, 23 | 'perPage': pagination.per_page, 24 | 'page': pagination.page, 25 | 'total': pagination.total, 26 | 'pages': pagination.pages, 27 | 'has_prev': pagination.has_prev, 28 | 'has_next': pagination.has_next, 29 | 'prev_num': pagination.prev_num, 30 | 'next_num': pagination.next_num 31 | } 32 | 33 | return Response(json.dumps(response_data), content_type='application/json') 34 | 35 | @portal.route('/api/project/', methods=['POST']) 36 | def api_project_create(): 37 | item_data = request.json.get('item', {}) 38 | item = Project() 39 | item.from_dict(item_data) 40 | db.session.add(item) 41 | try: 42 | db.session.commit() 43 | return {'message': 'Project successfully created!'} 44 | except Exception as e: 45 | db.session.rollback() 46 | return {'message': 'Error creating project', 'error': str(e)}, 412 47 | 48 | @portal.post('/api/project/') 49 | def api_project_update(id): 50 | item = db.get_or_404(Project, id) 51 | item_data = request.json.get('item', {}) 52 | item.from_dict(item_data) 53 | db.session.commit() 54 | return {'message': 'Project successfully updated!'} 55 | 56 | @portal.route('/api/project/', methods=['DELETE']) 57 | def api_project_delete(id): 58 | item = db.session.query(Project).get_or_404(id) 59 | db.session.delete(item) 60 | db.session.commit() 61 | return {'message': 'Project successfully deleted!'} 62 | -------------------------------------------------------------------------------- /enferno/templates/core/dashboard.jinja2: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block css %} 3 | {% endblock %} 4 | {% block sidebar %} 5 | {% endblock %} 6 | {% block layout_classes %} align-center {% endblock %} 7 | 8 | {% block content %} 9 | 10 | 11 | 12 | Project Dashboard 13 | 14 | 15 | 16 | 25 | 26 | 27 | 28 | 29 | 38 | 39 | 45 | 46 | 47 | 50 | 51 | 54 | 55 | 58 | 59 | 60 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | Project Editor 87 | 88 | 89 | 90 | 91 | 93 | 94 | 96 | 97 | 99 | 100 | 101 | 102 | 103 | 104 | Save 105 | 106 | 107 | 108 | 109 | 110 | ${snackMessage} 111 | 114 | 115 | 116 | {% endblock %} 117 | {% block js %} 118 | 119 | 243 | 244 | 245 | {% endblock %} 246 | -------------------------------------------------------------------------------- /enferno/templates/core/model.jinja2: -------------------------------------------------------------------------------- 1 | @dataclasses.dataclass 2 | class Project(db.Model, BaseMixin): 3 | id = db.Column(db.Integer, primary_key=True) 4 | url = db.Column(db.String, nullable=True) 5 | name = db.Column(db.String, nullable=True) 6 | description = db.Column(db.String, nullable=True) 7 | 8 | -------------------------------------------------------------------------------- /enferno/templates/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 | 4 | 5 | 6 | 7 |
Welcome, {{ current_user.username }}
8 | Last login — {{ current_user.last_login_at.strftime('%B %-d, %Y at %-I:%M %p') if current_user.last_login_at else 'Never' }} 9 |
10 | 11 | Logout 12 | 13 |
14 | 15 | {% endblock %} 16 | 17 | {% block js %} 18 | 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /enferno/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block body_class %} 3 | home 4 | {% endblock %} 5 | {% block content %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | Welcome to Enferno 13 | 14 |

Your development environment is ready. Use this framework to build your application.

15 | 16 | 17 | 18 |
19 |

Getting Started:

20 |

1. Edit templates in enferno/templates/

21 |

2. Add public routes in enferno/public/views.py

22 |

3. Create models in enferno/public/models.py or enferno/portal/models.py

23 |
24 | 25 | 26 |

Current environment: ${ config.ENV || 'development' }

27 |

Debug mode: ${ config.DEBUG || 'false' }

28 |
29 |
30 | 31 | 32 | {% if not current_user.is_authenticated %} 33 | Login 34 | Create Account 35 | {% else %} 36 | Dashboard 37 | Logout 38 | {% endif %} 39 | 40 |
41 | 42 |
43 |

Enferno Framework • Documentation

44 |
45 |
46 |
47 |
48 |
49 | {% endblock %} 50 | 51 | {% block js %} 52 | 78 | {% endblock %} 79 | -------------------------------------------------------------------------------- /enferno/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Project Enferno 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% block css %}{% endblock %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% if current_user.is_authenticated %} 32 | 37 | 50 | 51 | 52 | 53 | 57 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | My Account 67 | 68 | 69 | 70 | Change Password 71 | 72 | 73 | 74 | Logout 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | {% endif %} 83 | 84 | 85 | 86 | 87 | 88 | {% if current_user.has_role('admin') %} 89 | 90 | 91 | 92 | {% endif %} 93 | 94 | 95 | 96 | 97 | 98 | {% block content %} 99 | {% endblock %} 100 | 101 | 102 | 103 | {% with messages = get_flashed_messages(with_categories=true) %} 104 | {% if messages %} 105 |
106 | {% for category, message in messages %} 107 | 115 | {{ message }} 116 | 117 | {% endfor %} 118 |
119 | 120 | 129 | {% endif %} 130 | {% endwith %} 131 |
132 |
133 | 134 | 135 | 136 | 137 | {% block js %}{% endblock %} 138 | 139 | 140 | -------------------------------------------------------------------------------- /enferno/templates/security/_macros.html: -------------------------------------------------------------------------------- 1 | {% macro render_field_with_errors(field) %} 2 |
3 | {{ field.label }} {{ field(**kwargs)|safe }} 4 | {% if field.errors %} 5 |
    6 | {% for error in field.errors %}
  • {{ error }}
  • {% endfor %} 7 |
8 | {% endif %} 9 |
10 | {% endmacro %} 11 | 12 | {% macro render_field(field) %} 13 |
{{ field(**kwargs)|safe }}
14 | {% endmacro %} 15 | 16 | {% macro render_field_errors(field) %} 17 |
18 | {% if field and field.errors %} 19 |
    20 | {% for error in field.errors %}
  • {{ error }}
  • {% endfor %} 21 |
22 | {% endif %} 23 |
24 | {% endmacro %} 25 | 26 | {# render WTForms (>3.0) form level errors #} 27 | {% macro render_form_errors(form) %} 28 | {% if form.form_errors %} 29 |
30 |
    31 | {% for error in form.form_errors %}
  • {{ error }}
  • {% endfor %} 32 |
33 |
34 | {% endif %} 35 | {% endmacro %} 36 | 37 | {% macro prop_next() -%} 38 | {% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %} 39 | {%- endmacro %} 40 | -------------------------------------------------------------------------------- /enferno/templates/security/_messages.html: -------------------------------------------------------------------------------- 1 | {%- with messages = get_flashed_messages(with_categories=true) -%} 2 | {% if messages %} 3 |
    4 | {% for category, message in messages %} 5 |
  • {{ message }}
  • 6 | {% endfor %} 7 |
8 | {% endif %} 9 | {%- endwith %} -------------------------------------------------------------------------------- /enferno/templates/security/change_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 | 4 | 5 | 6 | 7 | Change password 8 | 9 | 10 |
11 | {{ change_password_form.hidden_tag() }} 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% if change_password_form.password.errors %} 19 | {% for error in change_password_form.password.errors %} 20 | {{ error }} 21 | {% endfor %} 22 | {% endif %} 23 | 24 | {% if change_password_form.new_password.errors %} 25 | {% for error in change_password_form.new_password.errors %} 26 | {{ error }} 27 | {% endfor %} 28 | {% endif %} 29 | 30 | {% if change_password_form.new_password_confirm.errors %} 31 | {% for error in change_password_form.new_password_confirm.errors %} 32 | {{ error }} 33 | {% endfor %} 34 | {% endif %} 35 | 36 | 37 | 38 | 39 | Change Password 40 | 41 |
42 |
43 |
44 | 45 | 46 | ${snackMessage} 47 | Close 48 | 49 |
50 | {% endblock %} 51 | 52 | {% block js %} 53 | 78 | {% endblock %} 79 | -------------------------------------------------------------------------------- /enferno/templates/security/confirm_email.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 |
7 |

Thank for your registration

8 |

9 | Please confirm your Email to continue. 10 |

11 |
12 |
13 | 14 | ${snackMessage} 15 | Close 16 | 17 |
18 | {% endblock %} 19 | 20 | {% block js %} 21 | 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /enferno/templates/security/email/change_notice.html: -------------------------------------------------------------------------------- 1 |

Your password has been changed.

2 | {% if security.recoverable %} 3 |

If you did not change your password, click here to reset it.

4 | {% endif %} 5 | -------------------------------------------------------------------------------- /enferno/templates/security/email/change_notice.txt: -------------------------------------------------------------------------------- 1 | Your password has been changed 2 | {% if security.recoverable %} 3 | If you did not change your password, click the link below to reset it. 4 | {{ url_for_security('forgot_password', _external=True) }} 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /enferno/templates/security/email/confirmation_instructions.html: -------------------------------------------------------------------------------- 1 |

Please confirm your email through the link below:

2 | 3 |

Confirm my account

-------------------------------------------------------------------------------- /enferno/templates/security/email/confirmation_instructions.txt: -------------------------------------------------------------------------------- 1 | Please confirm your email through the link below: 2 | 3 | {{ confirmation_link }} -------------------------------------------------------------------------------- /enferno/templates/security/email/login_instructions.html: -------------------------------------------------------------------------------- 1 |

Welcome {{ user.email }}!

2 | 3 |

You can log into your through the link below:

4 | 5 |

Login now

-------------------------------------------------------------------------------- /enferno/templates/security/email/login_instructions.txt: -------------------------------------------------------------------------------- 1 | Welcome {{ user.email }}! 2 | 3 | You can log into your through the link below: 4 | 5 | {{ login_link }} -------------------------------------------------------------------------------- /enferno/templates/security/email/reset_instructions.html: -------------------------------------------------------------------------------- 1 |

Click here to reset your password

-------------------------------------------------------------------------------- /enferno/templates/security/email/reset_instructions.txt: -------------------------------------------------------------------------------- 1 | Click the link below to reset your password: 2 | 3 | {{ reset_link }} -------------------------------------------------------------------------------- /enferno/templates/security/email/reset_notice.html: -------------------------------------------------------------------------------- 1 |

Your password has been reset

-------------------------------------------------------------------------------- /enferno/templates/security/email/reset_notice.txt: -------------------------------------------------------------------------------- 1 | Your password has been reset -------------------------------------------------------------------------------- /enferno/templates/security/email/welcome.html: -------------------------------------------------------------------------------- 1 |

Welcome {{ user.email }}!

2 | 3 | {% if security.confirmable %} 4 |

You can confirm your email through the link below:

5 | 6 |

Confirm my account

7 | {% endif %} -------------------------------------------------------------------------------- /enferno/templates/security/email/welcome.txt: -------------------------------------------------------------------------------- 1 | Welcome {{ user.email }}! 2 | 3 | {% if security.confirmable %} 4 | You can confirm your email through the link below: 5 | 6 | {{ confirmation_link }} 7 | {% endif %} -------------------------------------------------------------------------------- /enferno/templates/security/forgot_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 | 7 | 8 | Reset Password 9 | 10 |
12 | 13 | {{ forgot_password_form.hidden_tag() }} 14 | 15 | 16 | 17 | {% if forgot_password_form.errors %} 18 | {% for k, err in forgot_password_form.errors.items() %} 19 | {{ err[0] }} 20 | {% endfor %} 21 | {% endif %} 22 | 23 | 24 | Send Instructions 25 | 26 |
27 |
28 |
29 | 30 | ${snackMessage} 31 | Close 32 | 33 |
34 | {% endblock %} 35 | 36 | {% block js %} 37 | 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /enferno/templates/security/login_user.html: -------------------------------------------------------------------------------- 1 | {% extends 'auth_layout.html' %} 2 | 3 | {% block css %} 4 | 31 | {% endblock %} 32 | 33 | {% block sidebar %} 34 | {% endblock %} 35 | 36 | {% block layout_classes %} 37 | {% endblock %} 38 | 39 | {% block content %} 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | Logo 49 | Log in to your account 50 |
51 |
52 | 53 | 54 | 55 | 61 | {{ login_user_form.hidden_tag() }} 62 | 63 | 72 | 73 | 83 | 84 | {% if login_user_form.username.errors or login_user_form.password.errors %} 85 | Invalid username or password! 86 | {% endif %} 87 | 88 | {% if security.recoverable %} 89 |
90 | Forgot password? 91 |
92 | {% endif %} 93 | 94 | 101 | Log In 102 | 103 |
104 | 105 |
106 | 107 | or 108 | 109 |
110 | 111 | 112 |
113 | {% if config.GOOGLE_AUTH_ENABLED %} 114 | 122 | 130 | Continue with Google 131 | 132 | {% endif %} 133 | 134 | {% if config.GITHUB_AUTH_ENABLED %} 135 | 143 | 148 | Continue with GitHub 149 | 150 | {% endif %} 151 |
152 | 153 |
154 |

155 | By continuing, you agree to Enferno's 156 | Terms of Service and 157 | Privacy Policy. 158 |

159 |
160 | 161 | {% if security.registerable %} 162 |
163 | New to Enferno? 164 | Sign up 165 |
166 | {% endif %} 167 |
168 |
169 |
170 | 171 | 172 | 173 | 174 | 175 | 176 |
Secure. Scalable. Swift.
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 | {% endblock %} 185 | 186 | {% block js %} 187 | 211 | {% endblock %} 212 | -------------------------------------------------------------------------------- /enferno/templates/security/reset_password.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 | 7 | Reset Password 8 | {% from "security/_macros.html" import render_field_with_errors, render_field %} 9 | {% include "security/_messages.html" %} 10 | 11 |
13 | {{ reset_password_form.hidden_tag() }} 14 | 15 | 17 | 18 | Reset Password 19 |
20 |
21 |
22 | 23 | ${snackMessage} 24 | Close 25 | 26 |
27 | {% endblock %} 28 | 29 | {% block js %} 30 | 53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /enferno/templates/security/send_confirmation.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 |
7 |

Resend confirmation instructions

8 | 9 |
10 | {{ send_confirmation_form.hidden_tag() }} 11 | 12 | 13 | Resend Instructions 14 |
15 |
16 | 17 | ${snackMessage} 18 | Close 19 | 20 |
21 |
22 | {% endblock %} 23 | 24 | {% block js %} 25 | 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /enferno/templates/security/send_login.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | {% block content %} 3 |
4 |

Login

5 | 6 |
7 | {{ send_login_form.hidden_tag() }} 8 | 9 | Login 10 |
11 | 12 |
13 | {% endblock %} 14 | 15 | {% block js %} 16 | 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /enferno/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level09/enferno/81c827bd32fc06d3d83476a48e9c52f89ecb7a68/enferno/user/__init__.py -------------------------------------------------------------------------------- /enferno/user/forms.py: -------------------------------------------------------------------------------- 1 | from flask_security.forms import RegisterForm 2 | from wtforms import StringField 3 | 4 | 5 | 6 | class ExtendedRegisterForm(RegisterForm): 7 | name = StringField('Full Name') 8 | 9 | 10 | 11 | 12 | 13 | class UserInfoForm(): 14 | pass 15 | 16 | -------------------------------------------------------------------------------- /enferno/user/models.py: -------------------------------------------------------------------------------- 1 | import json, dataclasses 2 | from typing import Dict 3 | from uuid import uuid4 4 | from enferno.utils.base import BaseMixin 5 | from enferno.extensions import db 6 | import secrets 7 | import string 8 | 9 | from flask_security.core import UserMixin, RoleMixin 10 | from datetime import datetime 11 | from sqlalchemy import String, DateTime, Integer, Boolean, Column, ForeignKey, Table, ARRAY, LargeBinary, JSON 12 | from flask_security.utils import hash_password 13 | from sqlalchemy.orm import Mapped, mapped_column, relationship, declared_attr 14 | from sqlalchemy.ext.mutable import MutableList 15 | from flask_security import AsaList 16 | from flask_dance.consumer.storage.sqla import OAuthConsumerMixin 17 | 18 | roles_users: Table = db.Table( 19 | 'roles_users', 20 | Column('user_id', Integer, ForeignKey('user.id'), primary_key=True), 21 | Column('role_id', Integer, ForeignKey('role.id'), primary_key=True) 22 | ) 23 | 24 | 25 | @dataclasses.dataclass 26 | class Role(db.Model, RoleMixin, BaseMixin): 27 | id = db.Column(db.Integer, primary_key=True) 28 | name = db.Column(db.String(80), unique=True, nullable=True) 29 | description = db.Column(db.String(255), nullable=True) 30 | 31 | def to_dict(self) -> Dict: 32 | return { 33 | 'id': self.id, 34 | 'name': self.name, 35 | 'description': self.description 36 | } 37 | 38 | def from_dict(self, json_dict): 39 | self.name = json_dict.get('name', self.name) 40 | self.description = json_dict.get('description', self.description) 41 | return self 42 | 43 | 44 | @dataclasses.dataclass 45 | class User(UserMixin, db.Model, BaseMixin): 46 | id = db.Column(db.Integer, primary_key=True) 47 | username = db.Column(db.String(255), unique=True, nullable=True) 48 | fs_uniquifier = db.Column(db.String(255), unique=True, nullable=False, 49 | default=(lambda _: uuid4().hex)) 50 | name = db.Column(db.String(255), nullable=True) 51 | created_at = db.Column(db.DateTime, default=datetime.now, nullable=False) 52 | email = db.Column(db.String(255), nullable=True) 53 | password = db.Column(db.String(255), nullable=False) 54 | active = db.Column(db.Boolean, default=False, nullable=True) 55 | 56 | roles = relationship('Role', secondary=roles_users, backref="users") 57 | 58 | confirmed_at = db.Column(db.DateTime, nullable=True) 59 | last_login_at = db.Column(db.DateTime, nullable=True) 60 | current_login_at = db.Column(db.DateTime, nullable=True) 61 | last_login_ip = db.Column(db.String(255), nullable=True) 62 | current_login_ip = db.Column(db.String(255), nullable=True) 63 | login_count = db.Column(db.Integer, nullable=True) 64 | 65 | # web authn 66 | fs_webauthn_user_handle = db.Column(db.String(64), unique=True, nullable=True) 67 | tf_phone_number = db.Column(db.String(64), nullable=True) 68 | tf_primary_method = db.Column(db.String(140), nullable=True) 69 | tf_totp_secret = db.Column(db.String(255), nullable=True) 70 | mf_recovery_codes = db.Column(db.JSON, nullable=True) 71 | 72 | 73 | 74 | 75 | @declared_attr 76 | def webauthn(cls): 77 | return relationship("WebAuthn", backref="users", cascade="all, delete") 78 | 79 | def to_dict(self): 80 | return { 81 | 'id': self.id, 82 | 'active': self.active, 83 | 'name': self.name, 84 | 'username': self.username, 85 | 'email': self.email, 86 | 'roles': [role.to_dict() for role in self.roles] 87 | } 88 | 89 | def from_dict(self, json_dict): 90 | self.name = json_dict.get('name', self.name) 91 | self.username = json_dict.get('username', self.username) 92 | self.email = json_dict.get('email', self.email) 93 | if 'password' in json_dict: # Only hash password if provided, to avoid hashing None 94 | self.password = hash_password(json_dict['password']) 95 | # Update roles if specified, otherwise leave unchanged 96 | if 'roles' in json_dict: 97 | role_ids = [r.get('id') for r in json_dict['roles']] 98 | self.roles = Role.query.filter(Role.id.in_(role_ids)).all() if role_ids else self.roles 99 | self.active = json_dict.get('active', self.active) 100 | return self 101 | 102 | def __str__(self) -> str: 103 | """ 104 | Return the string representation of the object, typically using its ID. 105 | """ 106 | return f'{self.id}' 107 | 108 | def __repr__(self) -> str: 109 | """ 110 | Return an unambiguous string representation of the object. 111 | """ 112 | return f"{self.username} {self.id} {self.email}" 113 | 114 | meta = { 115 | 'allow_inheritance': True, 116 | 'indexes': ['-created_at', 'email', 'username'], 117 | 'ordering': ['-created_at'] 118 | } 119 | 120 | @staticmethod 121 | def random_password(length=32): 122 | alphabet = string.ascii_letters + string.digits + string.punctuation 123 | password = ''.join(secrets.choice(alphabet) for i in range(length)) 124 | return hash_password(password) 125 | 126 | 127 | 128 | 129 | class WebAuthn(db.Model): 130 | id = db.Column(db.Integer, primary_key=True) 131 | credential_id = db.Column(db.LargeBinary(1024), index=True, nullable=False, unique=True) 132 | public_key = db.Column(db.LargeBinary(1024), nullable=False) 133 | sign_count = db.Column(db.Integer, default=0, nullable=False) 134 | transports = db.Column(MutableList.as_mutable(AsaList()), nullable=True) 135 | extensions = db.Column(db.String(255), nullable=True) 136 | lastuse_datetime = db.Column(db.DateTime, nullable=False) 137 | name = db.Column(db.String(64), nullable=False) 138 | usage = db.Column(db.String(64), nullable=False) 139 | backup_state = db.Column(db.Boolean, nullable=False) 140 | device_type = db.Column(db.String(64), nullable=False) 141 | 142 | @declared_attr 143 | def user_id(cls): 144 | return db.Column( 145 | db.String(64), 146 | db.ForeignKey("user.fs_webauthn_user_handle", ondelete="CASCADE"), 147 | nullable=False 148 | ) 149 | 150 | def get_user_mapping(self): 151 | """ 152 | Return the mapping from webauthn back to User 153 | """ 154 | return dict(id=self.user_id) 155 | 156 | class OAuth(OAuthConsumerMixin, db.Model): 157 | __tablename__ = 'oauth' 158 | provider_user_id = db.Column(db.String(256), unique=True, nullable=False) 159 | user_id = db.Column(db.Integer, db.ForeignKey(User.id), nullable=False) 160 | user = db.relationship(User, backref=db.backref('oauth_accounts', 161 | cascade='all, delete-orphan', 162 | lazy='dynamic')) 163 | 164 | 165 | class Activity(db.Model, BaseMixin): 166 | id = db.Column(db.Integer, primary_key=True) 167 | user_id = db.Column(db.Integer, nullable=False) 168 | action = db.Column(db.String(255), nullable=False) 169 | data = db.Column(db.JSON, nullable=True) 170 | created_at = db.Column(db.DateTime, default=datetime.now, nullable=False) 171 | 172 | @classmethod 173 | def register(cls, user_id, action, data=None): 174 | """Register an activity for audit purposes""" 175 | activity = cls(user_id=user_id, action=action, data=data) 176 | db.session.add(activity) 177 | try: 178 | db.session.commit() 179 | return activity 180 | except Exception as e: 181 | print(f"Error registering activity: {e}") 182 | db.session.rollback() 183 | return None 184 | -------------------------------------------------------------------------------- /enferno/user/views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import orjson as json 4 | from flask import Blueprint, request, flash, g, Response, render_template 5 | from flask_security import current_user, auth_required, roles_required 6 | 7 | from enferno.extensions import db 8 | from enferno.user.models import User, Role, Activity 9 | 10 | bp_user = Blueprint('users', __name__, static_folder='../static') 11 | 12 | PER_PAGE = 25 13 | 14 | 15 | @bp_user.before_request 16 | @auth_required('session') 17 | @roles_required('admin') 18 | def before_request(): 19 | pass 20 | 21 | 22 | @bp_user.route('/users/') 23 | def users(): 24 | roles = Role.query.all() 25 | roles = [r.to_dict() for r in roles] 26 | return render_template('cms/users.html', roles=roles) 27 | 28 | 29 | @bp_user.route('/api/users') 30 | def api_user(): 31 | page = request.args.get('page', 1, type=int) 32 | per_page = request.args.get('per_page', PER_PAGE, type=int) 33 | 34 | # Start with base query - this pattern makes it easy to add filters later 35 | query = db.select(User) 36 | 37 | # Paginate results 38 | pagination = db.paginate(query, page=page, per_page=per_page) 39 | 40 | # Convert users to dictionaries 41 | items = [user.to_dict() for user in pagination.items] 42 | 43 | # Create consistent response structure with metadata 44 | response_data = { 45 | 'items': items, 46 | 'total': pagination.total, 47 | 'perPage': pagination.per_page 48 | } 49 | 50 | return Response(json.dumps(response_data), content_type='application/json') 51 | 52 | 53 | @bp_user.post('/api/user/') 54 | def api_user_create(): 55 | user_data = request.json.get('item', {}) 56 | user = User() 57 | user.from_dict(user_data) # Assuming from_dict is correctly implemented 58 | user.confirmed_at = datetime.datetime.now() 59 | db.session.add(user) 60 | try: 61 | db.session.commit() 62 | # Register activity 63 | Activity.register(current_user.id, 'User Create', user.to_dict()) 64 | return {'message': 'User successfully created!'} 65 | except Exception as e: 66 | db.session.rollback() 67 | return { 68 | 'message': 'Error creating user', 69 | 'error': str(e) 70 | }, 412 71 | 72 | 73 | @bp_user.post('/api/user/') 74 | def api_user_update(id): 75 | user = db.get_or_404(User, id) 76 | user_data = request.json.get('item', {}) 77 | # Store old user data for activity log 78 | old_user_data = user.to_dict() 79 | user.from_dict(user_data) 80 | db.session.commit() 81 | # Register activity 82 | Activity.register(current_user.id, 'User Update', { 83 | 'old': old_user_data, 84 | 'new': user.to_dict() 85 | }) 86 | return {'message': 'User successfully updated!'} 87 | 88 | 89 | @bp_user.route('/api/user/', methods=['DELETE']) 90 | def api_user_delete(id): 91 | user = db.get_or_404(User, id) 92 | # Store user data for activity log before deletion 93 | user_data = user.to_dict() 94 | db.session.delete(user) 95 | db.session.commit() 96 | # Register activity 97 | Activity.register(current_user.id, 'User Delete', user_data) 98 | return {'message': 'User successfully deleted!'} 99 | 100 | 101 | @bp_user.route('/roles/') 102 | def roles(): 103 | return render_template('cms/roles.html') 104 | 105 | 106 | @bp_user.route('/api/roles', methods=['GET']) 107 | def api_roles(): 108 | page = request.args.get('page', 1, type=int) 109 | per_page = request.args.get('per_page', PER_PAGE, type=int) 110 | 111 | # Start with base query 112 | query = db.select(Role) 113 | 114 | # Paginate results 115 | pagination = db.paginate(query, page=page, per_page=per_page) 116 | 117 | # Convert roles to dictionaries 118 | items = [role.to_dict() for role in pagination.items] 119 | 120 | # Create consistent response structure 121 | response_data = { 122 | 'items': items, 123 | 'total': pagination.total, 124 | 'perPage': pagination.per_page 125 | } 126 | 127 | return Response(json.dumps(response_data), content_type='application/json') 128 | 129 | 130 | @bp_user.route('/api/role/', methods=['POST']) 131 | def api_role_create(): 132 | role_data = request.json.get('item', {}) 133 | role = Role() 134 | role.from_dict(role_data) 135 | db.session.add(role) 136 | try: 137 | db.session.commit() 138 | # Register activity 139 | Activity.register(current_user.id, 'Role Create', role.to_dict()) 140 | return {'message': 'Role successfully created!'} 141 | except Exception as e: 142 | db.session.rollback() 143 | return {'message': 'Error creating role', 'error': str(e)}, 412 144 | 145 | 146 | @bp_user.post('/api/role/') 147 | def api_role_update(id): 148 | role = db.get_or_404(Role, id) 149 | # Store old role data for activity log 150 | old_role_data = role.to_dict() 151 | role_data = request.json.get('item', {}) 152 | role.from_dict(role_data) 153 | db.session.commit() 154 | # Register activity 155 | Activity.register(current_user.id, 'Role Update', { 156 | 'old': old_role_data, 157 | 'new': role.to_dict() 158 | }) 159 | return {'message': 'Role successfully updated!'} 160 | 161 | 162 | @bp_user.route('/api/role/', methods=['DELETE']) 163 | def api_role_delete(id): 164 | role = db.get_or_404(Role, id) 165 | # Store role data for activity log before deletion 166 | role_data = role.to_dict() 167 | db.session.delete(role) 168 | db.session.commit() 169 | # Register activity 170 | Activity.register(current_user.id, 'Role Delete', role_data) 171 | return {'message': 'Role successfully deleted!'} 172 | 173 | 174 | @bp_user.route('/activities/') 175 | def activities(): 176 | return render_template('cms/activities.html') 177 | 178 | 179 | @bp_user.route('/api/activities') 180 | def api_activities(): 181 | page = request.args.get('page', 1, type=int) 182 | per_page = request.args.get('per_page', PER_PAGE, type=int) 183 | 184 | # Start with base query - newest activities first 185 | query = db.select(Activity).order_by(Activity.created_at.desc()) 186 | 187 | # Paginate results 188 | pagination = db.paginate(query, page=page, per_page=per_page) 189 | 190 | # Convert activities to dictionaries 191 | items = [] 192 | for activity in pagination.items: 193 | # Get user info if available 194 | user = db.session.get(User, activity.user_id) 195 | username = user.username if user else f"User ID: {activity.user_id}" 196 | 197 | items.append({ 198 | 'id': activity.id, 199 | 'user': username, 200 | 'action': activity.action, 201 | 'data': activity.data, 202 | 'created_at': activity.created_at.strftime('%Y-%m-%d %H:%M:%S') 203 | }) 204 | 205 | # Create consistent response structure with metadata 206 | response_data = { 207 | 'items': items, 208 | 'total': pagination.total, 209 | 'perPage': pagination.per_page 210 | } 211 | 212 | return Response(json.dumps(response_data), content_type='application/json') 213 | -------------------------------------------------------------------------------- /enferno/utils/base.py: -------------------------------------------------------------------------------- 1 | from enferno.extensions import db 2 | from sqlalchemy import exc 3 | 4 | class BaseMixin(): 5 | 6 | def save(self, commit=True): 7 | db.session.add(self) 8 | if commit: 9 | try: 10 | db.session.commit() 11 | return self 12 | except exc.SQLAlchemyError as e: 13 | print(e) 14 | db.session.rollback() 15 | return None 16 | 17 | def delete(self, commit=True): 18 | db.session.delete(self) 19 | if commit: 20 | try: 21 | db.session.commit() 22 | return self 23 | except exc.SQLAlchemyError as e: 24 | print(e) 25 | db.session.rollback() 26 | return None 27 | -------------------------------------------------------------------------------- /nginx/enferno.conf: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 80; 4 | server_name 0.0.0.0; 5 | charset utf-8; 6 | 7 | # static assets 8 | location /static { 9 | alias /app/static; 10 | expires 3600; 11 | } 12 | 13 | location / { 14 | proxy_pass http://website:5000; 15 | proxy_set_header Host $host; 16 | proxy_set_header X-Real-IP $remote_addr; 17 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 18 | 19 | # Add retry settings for upstream 20 | proxy_connect_timeout 300s; 21 | proxy_send_timeout 300s; 22 | proxy_read_timeout 300s; 23 | proxy_next_upstream error timeout http_500 http_502 http_503 http_504; 24 | proxy_next_upstream_tries 3; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # Based on https://www.nginx.com/resources/wiki/start/topics/examples/full/#nginx-conf 2 | # user www www; ## Default: nobody 3 | 4 | worker_processes auto; 5 | error_log "/var/log/nginx/error.log"; 6 | pid "/tmp/nginx.pid"; 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | http { 13 | include "/etc/nginx/mime.types"; 14 | default_type application/octet-stream; 15 | log_format main '$remote_addr - $remote_user [$time_local] ' 16 | '"$request" $status $body_bytes_sent "$http_referer" ' 17 | '"$http_user_agent" "$http_x_forwarded_for"'; 18 | access_log "/var/log/nginx/access.log" main; 19 | add_header X-Frame-Options SAMEORIGIN; 20 | 21 | client_body_temp_path "/tmp/client_temp"; 22 | proxy_temp_path "/tmp/proxy_temp"; 23 | fastcgi_temp_path "/tmp/fastcgi_temp"; 24 | scgi_temp_path "/tmp/scgi_temp"; 25 | uwsgi_temp_path "/tmp/uwsgi_temp"; 26 | 27 | sendfile on; 28 | tcp_nopush on; 29 | tcp_nodelay off; 30 | gzip on; 31 | gzip_http_version 1.0; 32 | gzip_comp_level 2; 33 | gzip_proxied any; 34 | gzip_types text/plain text/css application/javascript text/xml application/xml+rss; 35 | keepalive_timeout 65; 36 | ssl_protocols TLSv1.2 TLSv1.3; 37 | ssl_ciphers HIGH:!aNULL:!MD5; 38 | client_max_body_size 500M; 39 | server_tokens off; 40 | 41 | absolute_redirect off; 42 | port_in_redirect off; 43 | 44 | include "/etc/nginx/conf.d/*.conf"; 45 | 46 | # HTTP Server 47 | server { 48 | # Port to listen on, can also be set in IP:PORT format 49 | listen 8080; 50 | 51 | include "/opt/bitnami/nginx/conf/bitnami/*.conf"; 52 | 53 | location /status { 54 | stub_status on; 55 | access_log off; 56 | allow 127.0.0.1; 57 | deny all; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | amqp==5.3.1 2 | Babel==2.17.0 3 | bcrypt==4.3.0 4 | bleach==6.2.0 5 | blinker==1.9.0 6 | celery==5.5.1 7 | cffi==1.17.1 8 | click==8.1.8 9 | cryptography==44.0.2 10 | email-validator==2.2.0 11 | Flask==3.1.1 12 | Flask-Caching==2.3.1 13 | Flask-Dance==7.1.0 14 | Flask-DebugToolbar==0.16.0 15 | Flask-Login==0.6.3 16 | Flask-Mail==0.10.0 17 | Flask-Script==2.0.6 18 | Flask-Security-Too==5.6.1 19 | Flask-Session==0.8.0 20 | Flask-SQLAlchemy==3.1.1 21 | Flask-WTF==1.2.2 22 | itsdangerous==2.2.0 23 | Jinja2==3.1.6 24 | kombu==5.5.3 25 | Mako==1.3.10 26 | MarkupSafe==3.0.2 27 | passlib==1.7.4 28 | Pillow==11.2.1 29 | psycopg2-binary==2.9.10 30 | pycparser==2.22 31 | python-dateutil==2.9.0.post0 32 | python-dotenv==1.1.0 33 | python-editor==1.0.4 34 | pytz==2025.2 35 | qrcode==8.1 36 | redis==5.2.1 37 | six==1.17.0 38 | speaklater==1.3 39 | SQLAlchemy==2.0.40 40 | uWSGI==2.0.29 41 | vine==5.1.0 42 | Werkzeug==3.1.3 43 | webauthn==2.5.2 44 | WTForms==3.2.1 45 | orjson==3.10.16 46 | flask-babel==4.0.0 47 | rich==14.0.0 48 | setuptools>=78.1.0 49 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from enferno.app import create_app 2 | from enferno.settings import Config 3 | app = create_app(Config) 4 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e # Exit on error 3 | 4 | # Colors 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | YELLOW='\033[0;33m' 8 | NC='\033[0m' 9 | 10 | # Find latest Python 3 version 11 | PYTHON_CMD="" 12 | for cmd in python3.13 python3.12 python3.11 python3.10 python3.9 python3.8 python3.7 python3; do 13 | if command -v $cmd &> /dev/null; then 14 | PYTHON_CMD=$cmd 15 | break 16 | fi 17 | done 18 | 19 | if [ -z "$PYTHON_CMD" ]; then 20 | echo -e "${RED}Error: No Python 3.x installation found${NC}" 21 | exit 1 22 | fi 23 | 24 | echo -e "${GREEN}Using Python: $($PYTHON_CMD --version)${NC}" 25 | 26 | # Check if uv is installed 27 | if ! command -v uv &> /dev/null; then 28 | echo -e "${RED}Error: uv is not installed. Please install uv using: 'pip install uv' or 'curl -sSf https://astral.sh/uv/install.sh | bash'${NC}" 29 | exit 1 30 | fi 31 | 32 | # Create and activate virtual environment 33 | if [ -d ".venv" ]; then 34 | read -p "Virtual environment '.venv' already exists. Recreate? (y/N) " -n 1 -r 35 | echo 36 | if [[ $REPLY =~ ^[Yy]$ ]]; then 37 | echo -e "${GREEN}Recreating virtual environment...${NC}" 38 | rm -rf .venv 39 | uv venv 40 | else 41 | echo -e "${GREEN}Using existing virtual environment '.venv'${NC}" 42 | # Skip venv creation, proceed to activation and installation 43 | fi 44 | else 45 | echo -e "${GREEN}Creating virtual environment...${NC}" 46 | uv venv 47 | fi 48 | 49 | # Activate virtual environment 50 | source .venv/bin/activate 51 | 52 | # Install requirements 53 | echo -e "${GREEN}Installing requirements...${NC}" 54 | uv pip install -r requirements.txt 55 | 56 | # Check for required commands 57 | for cmd in tr openssl awk; do 58 | if ! command -v $cmd &> /dev/null; then 59 | echo -e "${RED}Error: $cmd is required but not installed.${NC}" 60 | exit 1 61 | fi 62 | done 63 | 64 | # Function to generate secure random string 65 | generate_secure_string() { 66 | local length=$1 67 | if command -v openssl &> /dev/null; then 68 | openssl rand -base64 $length | tr -dc 'A-Za-z0-9@#$%^&*' | head -c $length 69 | else 70 | LC_ALL=C tr -dc 'A-Za-z0-9@#$%^&*' < /dev/urandom | head -c $length 71 | fi 72 | } 73 | 74 | # Check if .env-sample exists 75 | if [ ! -f .env-sample ]; then 76 | echo -e "${RED}Error: .env-sample file not found${NC}" 77 | exit 1 78 | fi 79 | 80 | # Ask about Docker configuration 81 | read -p "Would you like to configure Docker settings? (y/N) " -n 1 -r 82 | echo 83 | DOCKER_CONFIG=false 84 | if [[ $REPLY =~ ^[Yy]$ ]]; then 85 | DOCKER_CONFIG=true 86 | echo -e "${GREEN}Docker configuration will be included.${NC}" 87 | else 88 | echo -e "${YELLOW}Skipping Docker configuration.${NC}" 89 | fi 90 | 91 | # Check if .env already exists 92 | if [ -f .env ]; then 93 | read -p "A .env file already exists. Do you want to overwrite it? (y/N) " -n 1 -r 94 | echo 95 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 96 | exit 1 97 | fi 98 | # Backup existing .env 99 | BACKUP_FILE=".env.backup.$(date +%Y%m%d_%H%M%S)" 100 | if ! cp .env "$BACKUP_FILE"; then 101 | echo -e "${RED}Error: Failed to create backup file${NC}" 102 | exit 1 103 | fi 104 | echo -e "${GREEN}Created backup of existing .env at $BACKUP_FILE${NC}" 105 | fi 106 | 107 | # Copy the sample file 108 | if ! cp .env-sample .env; then 109 | echo -e "${RED}Error: Failed to copy .env-sample to .env${NC}" 110 | exit 1 111 | fi 112 | 113 | # Generate secure random values 114 | SECRET_KEY=$(generate_secure_string 32) 115 | TOTP_SECRET1=$(generate_secure_string 32) 116 | TOTP_SECRET2=$(generate_secure_string 32) 117 | PASSWORD_SALT=$(generate_secure_string 32) 118 | REDIS_PASSWORD=$(generate_secure_string 20) 119 | DB_PASSWORD=$(generate_secure_string 20) 120 | DOCKER_UID=$(id -u) 121 | 122 | # Validate generated values 123 | if [ -z "$SECRET_KEY" ] || [ -z "$TOTP_SECRET1" ] || [ -z "$TOTP_SECRET2" ] || [ -z "$PASSWORD_SALT" ]; then 124 | echo -e "${RED}Error: Failed to generate secure values${NC}" 125 | exit 1 126 | fi 127 | 128 | # Process the .env file 129 | if ! awk -v sk="$SECRET_KEY" -v ts1="$TOTP_SECRET1" -v ts2="$TOTP_SECRET2" -v ps="$PASSWORD_SALT" \ 130 | -v docker="$DOCKER_CONFIG" -v rpass="$REDIS_PASSWORD" -v dbpass="$DB_PASSWORD" -v uid="$DOCKER_UID" ' 131 | { 132 | if ($0 ~ /^SECRET_KEY=/) { 133 | print "SECRET_KEY=\"" sk "\"" 134 | } 135 | else if ($0 ~ /^SECURITY_TOTP_SECRETS=/) { 136 | print "SECURITY_TOTP_SECRETS=\"" ts1 "," ts2 "\"" 137 | } 138 | else if ($0 ~ /^SECURITY_PASSWORD_SALT=/) { 139 | print "SECURITY_PASSWORD_SALT=\"" ps "\"" 140 | } 141 | else if ($0 ~ /^SQLALCHEMY_DATABASE_URI=/) { 142 | print "SQLALCHEMY_DATABASE_URI=sqlite:///enferno.sqlite3" 143 | print "# PostgreSQL alternative:" 144 | print "# SQLALCHEMY_DATABASE_URI=postgresql://postgres:pass@localhost/dbname" 145 | } 146 | else if (docker == "true" && $0 ~ /^#REDIS_PASSWORD=/) { 147 | print "REDIS_PASSWORD=" rpass 148 | } 149 | else if (docker == "true" && $0 ~ /^#DB_PASSWORD=/) { 150 | print "DB_PASSWORD=" dbpass 151 | } 152 | else if (docker == "true" && $0 ~ /^#SQLALCHEMY_DATABASE_URI=postgresql:/) { 153 | print "SQLALCHEMY_DATABASE_URI=postgresql://enferno:${DB_PASSWORD}@postgres/enferno" 154 | } 155 | else if (docker == "true" && $0 ~ /^#REDIS_SESSION=/) { 156 | print "REDIS_SESSION=redis://:${REDIS_PASSWORD}@redis:6379/1" 157 | } 158 | else if (docker == "true" && $0 ~ /^#CELERY_BROKER_URL=redis:\/\/:/) { 159 | print "CELERY_BROKER_URL=redis://:${REDIS_PASSWORD}@redis:6379/2" 160 | } 161 | else if (docker == "true" && $0 ~ /^#CELERY_RESULT_BACKEND=redis:\/\/:/) { 162 | print "CELERY_RESULT_BACKEND=redis://:${REDIS_PASSWORD}@redis:6379/3" 163 | } 164 | else if (docker == "true" && $0 ~ /^# Docker-specific settings/) { 165 | print "# Docker-specific settings" 166 | print "DOCKER_UID=" uid 167 | print 168 | } 169 | else { 170 | print $0 171 | } 172 | }' .env > .env.new; then 173 | echo -e "${RED}Error: Failed to process .env file${NC}" 174 | exit 1 175 | fi 176 | 177 | if ! mv .env.new .env; then 178 | echo -e "${RED}Error: Failed to save .env file${NC}" 179 | rm -f .env.new 180 | exit 1 181 | fi 182 | 183 | # Verify the file was created 184 | if [ ! -f .env ]; then 185 | echo -e "${RED}Error: .env file was not created${NC}" 186 | exit 1 187 | fi 188 | 189 | echo -e "${GREEN}Successfully generated .env file with secure keys${NC}" 190 | echo -e "${GREEN}Generated secure values for: SECRET_KEY, SECURITY_TOTP_SECRETS, SECURITY_PASSWORD_SALT${NC}" 191 | if [ "$DOCKER_CONFIG" = true ]; then 192 | echo -e "${GREEN}Docker configuration enabled with secure passwords for Redis and PostgreSQL${NC}" 193 | fi 194 | echo -e "${GREEN}SQLite database configured at: enferno.sqlite3${NC}" 195 | echo 196 | echo -e "${GREEN}Next steps:${NC}" 197 | echo -e "1. Update the remaining values in your .env file (mail settings, etc.)" 198 | echo -e "2. Activate the virtual environment: ${GREEN}source .venv/bin/activate${NC}" 199 | if [ "$DOCKER_CONFIG" = true ]; then 200 | echo -e "3. To use Docker, run: ${GREEN}docker compose up -d${NC}" 201 | echo -e "4. Or for traditional setup, run: ${GREEN}flask create-db${NC} and ${GREEN}flask install${NC}" 202 | else 203 | echo -e "3. Run ${GREEN}flask create-db${NC} to initialize the database" 204 | echo -e "4. Run ${GREEN}flask install${NC} to create the first admin user" 205 | fi --------------------------------------------------------------------------------