Prune unverified accounts
This commit is contained in:
@@ -39,8 +39,16 @@ docker-compose up
|
|||||||
|
|
||||||
# Debug mode with hot-reload
|
# Debug mode with hot-reload
|
||||||
docker-compose -f docker-compose.debug.yml up
|
docker-compose -f docker-compose.debug.yml up
|
||||||
|
|
||||||
|
# Manual cleanup of unverified accounts
|
||||||
|
docker exec timetrack-timetrack-1 python cleanup_unverified_accounts.py
|
||||||
|
|
||||||
|
# Dry run to see what would be deleted
|
||||||
|
docker exec timetrack-timetrack-1 python cleanup_unverified_accounts.py --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** Unverified accounts are automatically cleaned up every hour via cron job in the Docker container.
|
||||||
|
|
||||||
## Database Operations
|
## Database Operations
|
||||||
|
|
||||||
### Flask-Migrate Commands
|
### Flask-Migrate Commands
|
||||||
@@ -187,6 +195,7 @@ When modifying billing/invoice features:
|
|||||||
- Two-factor authentication (2FA) using TOTP
|
- Two-factor authentication (2FA) using TOTP
|
||||||
- Session-based authentication with "Remember Me" option
|
- Session-based authentication with "Remember Me" option
|
||||||
- Email verification for new accounts (configurable)
|
- Email verification for new accounts (configurable)
|
||||||
|
- Automatic cleanup of unverified accounts after 24 hours
|
||||||
|
|
||||||
### Mobile UI Features
|
### Mobile UI Features
|
||||||
- Progressive Web App (PWA) manifest for installability
|
- Progressive Web App (PWA) manifest for installability
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
build-essential \
|
build-essential \
|
||||||
python3-dev \
|
python3-dev \
|
||||||
postgresql-client \
|
postgresql-client \
|
||||||
|
cron \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@@ -35,6 +36,12 @@ RUN pip install gunicorn==21.2.0
|
|||||||
# Copy the rest of the application
|
# Copy the rest of the application
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Setup cron job for cleanup
|
||||||
|
COPY docker-cron /etc/cron.d/cleanup-cron
|
||||||
|
RUN chmod 0644 /etc/cron.d/cleanup-cron && \
|
||||||
|
crontab /etc/cron.d/cleanup-cron && \
|
||||||
|
touch /var/log/cron.log
|
||||||
|
|
||||||
# Create the SQLite database directory with proper permissions
|
# Create the SQLite database directory with proper permissions
|
||||||
RUN mkdir -p /app/instance && chmod 777 /app/instance
|
RUN mkdir -p /app/instance && chmod 777 /app/instance
|
||||||
|
|
||||||
|
|||||||
58
app.py
58
app.py
@@ -102,14 +102,14 @@ def force_http_scheme():
|
|||||||
if app.debug or os.environ.get('FLASK_ENV') == 'debug':
|
if app.debug or os.environ.get('FLASK_ENV') == 'debug':
|
||||||
from flask import url_for as original_url_for
|
from flask import url_for as original_url_for
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
@functools.wraps(original_url_for)
|
@functools.wraps(original_url_for)
|
||||||
def url_for_http(*args, **kwargs):
|
def url_for_http(*args, **kwargs):
|
||||||
# Force _scheme to http if _external is True
|
# Force _scheme to http if _external is True
|
||||||
if kwargs.get('_external'):
|
if kwargs.get('_external'):
|
||||||
kwargs['_scheme'] = 'http'
|
kwargs['_scheme'] = 'http'
|
||||||
return original_url_for(*args, **kwargs)
|
return original_url_for(*args, **kwargs)
|
||||||
|
|
||||||
app.jinja_env.globals['url_for'] = url_for_http
|
app.jinja_env.globals['url_for'] = url_for_http
|
||||||
|
|
||||||
# Configure Flask-Mail
|
# Configure Flask-Mail
|
||||||
@@ -365,7 +365,7 @@ def robots_txt():
|
|||||||
def sitemap_xml():
|
def sitemap_xml():
|
||||||
"""Generate XML sitemap for search engines"""
|
"""Generate XML sitemap for search engines"""
|
||||||
pages = []
|
pages = []
|
||||||
|
|
||||||
# Static pages accessible without login
|
# Static pages accessible without login
|
||||||
static_pages = [
|
static_pages = [
|
||||||
{'loc': '/', 'priority': '1.0', 'changefreq': 'daily'},
|
{'loc': '/', 'priority': '1.0', 'changefreq': 'daily'},
|
||||||
@@ -373,7 +373,7 @@ def sitemap_xml():
|
|||||||
{'loc': '/register', 'priority': '0.9', 'changefreq': 'monthly'},
|
{'loc': '/register', 'priority': '0.9', 'changefreq': 'monthly'},
|
||||||
{'loc': '/forgot_password', 'priority': '0.5', 'changefreq': 'monthly'},
|
{'loc': '/forgot_password', 'priority': '0.5', 'changefreq': 'monthly'},
|
||||||
]
|
]
|
||||||
|
|
||||||
for page in static_pages:
|
for page in static_pages:
|
||||||
pages.append({
|
pages.append({
|
||||||
'loc': request.host_url[:-1] + page['loc'],
|
'loc': request.host_url[:-1] + page['loc'],
|
||||||
@@ -381,10 +381,10 @@ def sitemap_xml():
|
|||||||
'priority': page['priority'],
|
'priority': page['priority'],
|
||||||
'changefreq': page['changefreq']
|
'changefreq': page['changefreq']
|
||||||
})
|
})
|
||||||
|
|
||||||
sitemap_xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
sitemap_xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
sitemap_xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
sitemap_xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
|
||||||
|
|
||||||
for page in pages:
|
for page in pages:
|
||||||
sitemap_xml += ' <url>\n'
|
sitemap_xml += ' <url>\n'
|
||||||
sitemap_xml += f' <loc>{page["loc"]}</loc>\n'
|
sitemap_xml += f' <loc>{page["loc"]}</loc>\n'
|
||||||
@@ -392,9 +392,9 @@ def sitemap_xml():
|
|||||||
sitemap_xml += f' <changefreq>{page["changefreq"]}</changefreq>\n'
|
sitemap_xml += f' <changefreq>{page["changefreq"]}</changefreq>\n'
|
||||||
sitemap_xml += f' <priority>{page["priority"]}</priority>\n'
|
sitemap_xml += f' <priority>{page["priority"]}</priority>\n'
|
||||||
sitemap_xml += ' </url>\n'
|
sitemap_xml += ' </url>\n'
|
||||||
|
|
||||||
sitemap_xml += '</urlset>'
|
sitemap_xml += '</urlset>'
|
||||||
|
|
||||||
return Response(sitemap_xml, mimetype='application/xml')
|
return Response(sitemap_xml, mimetype='application/xml')
|
||||||
|
|
||||||
@app.route('/site.webmanifest')
|
@app.route('/site.webmanifest')
|
||||||
@@ -986,11 +986,11 @@ def forgot_password():
|
|||||||
"""Handle forgot password requests"""
|
"""Handle forgot password requests"""
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
username_or_email = request.form.get('username_or_email', '').strip()
|
username_or_email = request.form.get('username_or_email', '').strip()
|
||||||
|
|
||||||
if not username_or_email:
|
if not username_or_email:
|
||||||
flash('Please enter your username or email address.', 'error')
|
flash('Please enter your username or email address.', 'error')
|
||||||
return render_template('forgot_password.html', title='Forgot Password')
|
return render_template('forgot_password.html', title='Forgot Password')
|
||||||
|
|
||||||
# Try to find user by username or email
|
# Try to find user by username or email
|
||||||
user = User.query.filter(
|
user = User.query.filter(
|
||||||
db.or_(
|
db.or_(
|
||||||
@@ -998,11 +998,11 @@ def forgot_password():
|
|||||||
User.email == username_or_email
|
User.email == username_or_email
|
||||||
)
|
)
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if user and user.email:
|
if user and user.email:
|
||||||
# Generate reset token
|
# Generate reset token
|
||||||
token = user.generate_password_reset_token()
|
token = user.generate_password_reset_token()
|
||||||
|
|
||||||
# Send reset email
|
# Send reset email
|
||||||
reset_url = url_for('reset_password', token=token, _external=True)
|
reset_url = url_for('reset_password', token=token, _external=True)
|
||||||
msg = Message(
|
msg = Message(
|
||||||
@@ -1023,7 +1023,7 @@ If you did not request a password reset, please ignore this email.
|
|||||||
Best regards,
|
Best regards,
|
||||||
The {g.branding.app_name if g.branding else "TimeTrack"} Team
|
The {g.branding.app_name if g.branding else "TimeTrack"} Team
|
||||||
'''
|
'''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
logger.info(f"Password reset email sent to user {user.username}")
|
logger.info(f"Password reset email sent to user {user.username}")
|
||||||
@@ -1031,11 +1031,11 @@ The {g.branding.app_name if g.branding else "TimeTrack"} Team
|
|||||||
logger.error(f"Failed to send password reset email: {str(e)}")
|
logger.error(f"Failed to send password reset email: {str(e)}")
|
||||||
flash('Failed to send reset email. Please contact support.', 'error')
|
flash('Failed to send reset email. Please contact support.', 'error')
|
||||||
return render_template('forgot_password.html', title='Forgot Password')
|
return render_template('forgot_password.html', title='Forgot Password')
|
||||||
|
|
||||||
# Always show success message to prevent user enumeration
|
# Always show success message to prevent user enumeration
|
||||||
flash('If an account exists with that username or email address, we have sent a password reset link.', 'success')
|
flash('If an account exists with that username or email address, we have sent a password reset link.', 'success')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
return render_template('forgot_password.html', title='Forgot Password')
|
return render_template('forgot_password.html', title='Forgot Password')
|
||||||
|
|
||||||
@app.route('/reset_password/<token>', methods=['GET', 'POST'])
|
@app.route('/reset_password/<token>', methods=['GET', 'POST'])
|
||||||
@@ -1043,42 +1043,42 @@ def reset_password(token):
|
|||||||
"""Handle password reset with token"""
|
"""Handle password reset with token"""
|
||||||
# Find user by reset token
|
# Find user by reset token
|
||||||
user = User.query.filter_by(password_reset_token=token).first()
|
user = User.query.filter_by(password_reset_token=token).first()
|
||||||
|
|
||||||
if not user or not user.verify_password_reset_token(token):
|
if not user or not user.verify_password_reset_token(token):
|
||||||
flash('Invalid or expired reset link.', 'error')
|
flash('Invalid or expired reset link.', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
password = request.form.get('password')
|
password = request.form.get('password')
|
||||||
confirm_password = request.form.get('confirm_password')
|
confirm_password = request.form.get('confirm_password')
|
||||||
|
|
||||||
# Validate input
|
# Validate input
|
||||||
error = None
|
error = None
|
||||||
if not password:
|
if not password:
|
||||||
error = 'Password is required'
|
error = 'Password is required'
|
||||||
elif password != confirm_password:
|
elif password != confirm_password:
|
||||||
error = 'Passwords do not match'
|
error = 'Passwords do not match'
|
||||||
|
|
||||||
# Validate password strength
|
# Validate password strength
|
||||||
if not error:
|
if not error:
|
||||||
validator = PasswordValidator()
|
validator = PasswordValidator()
|
||||||
is_valid, password_errors = validator.validate(password)
|
is_valid, password_errors = validator.validate(password)
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
error = password_errors[0]
|
error = password_errors[0]
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
flash(error, 'error')
|
flash(error, 'error')
|
||||||
return render_template('reset_password.html', token=token, title='Reset Password')
|
return render_template('reset_password.html', token=token, title='Reset Password')
|
||||||
|
|
||||||
# Update password
|
# Update password
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
user.clear_password_reset_token()
|
user.clear_password_reset_token()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
logger.info(f"Password reset successful for user {user.username}")
|
logger.info(f"Password reset successful for user {user.username}")
|
||||||
flash('Your password has been reset successfully. Please log in with your new password.', 'success')
|
flash('Your password has been reset successfully. Please log in with your new password.', 'success')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
return render_template('reset_password.html', token=token, title='Reset Password')
|
return render_template('reset_password.html', token=token, title='Reset Password')
|
||||||
|
|
||||||
@app.route('/dashboard')
|
@app.route('/dashboard')
|
||||||
@@ -2931,14 +2931,14 @@ def render_markdown():
|
|||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
content = data.get('content', '')
|
content = data.get('content', '')
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return jsonify({'html': '<p class="preview-placeholder">Start typing to see the preview...</p>'})
|
return jsonify({'html': '<p class="preview-placeholder">Start typing to see the preview...</p>'})
|
||||||
|
|
||||||
# Parse frontmatter and extract body
|
# Parse frontmatter and extract body
|
||||||
from frontmatter_utils import parse_frontmatter
|
from frontmatter_utils import parse_frontmatter
|
||||||
metadata, body = parse_frontmatter(content)
|
metadata, body = parse_frontmatter(content)
|
||||||
|
|
||||||
# Render markdown to HTML
|
# Render markdown to HTML
|
||||||
try:
|
try:
|
||||||
import markdown
|
import markdown
|
||||||
@@ -2947,13 +2947,13 @@ def render_markdown():
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
# Fallback if markdown not installed
|
# Fallback if markdown not installed
|
||||||
html = f'<pre>{body}</pre>'
|
html = f'<pre>{body}</pre>'
|
||||||
|
|
||||||
return jsonify({'html': html})
|
return jsonify({'html': html})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error rendering markdown: {str(e)}")
|
logger.error(f"Error rendering markdown: {str(e)}")
|
||||||
return jsonify({'html': '<p class="error">Error rendering markdown</p>'})
|
return jsonify({'html': '<p class="error">Error rendering markdown</p>'})
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
port = int(os.environ.get('PORT', 5000))
|
port = int(os.environ.get('PORT', 5000))
|
||||||
app.run(debug=True, host='0.0.0.0', port=port)
|
app.run(debug=True, host='0.0.0.0', port=port)
|
||||||
|
|||||||
@@ -73,6 +73,16 @@ if [ -f "migrations_old/run_postgres_migrations.py" ]; then
|
|||||||
echo "Found old migration system. Consider removing after confirming Flask-Migrate is working."
|
echo "Found old migration system. Consider removing after confirming Flask-Migrate is working."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Start cron service for scheduled tasks
|
||||||
|
echo ""
|
||||||
|
echo "=== Starting Cron Service ==="
|
||||||
|
service cron start
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "✅ Cron service started for scheduled cleanup tasks"
|
||||||
|
else
|
||||||
|
echo "⚠️ Failed to start cron service, cleanup tasks won't run automatically"
|
||||||
|
fi
|
||||||
|
|
||||||
# Start the Flask application with gunicorn
|
# Start the Flask application with gunicorn
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Starting Application ==="
|
echo "=== Starting Application ==="
|
||||||
|
|||||||
@@ -8,72 +8,72 @@ from models import db
|
|||||||
|
|
||||||
class BaseRepository:
|
class BaseRepository:
|
||||||
"""Base repository with common database operations"""
|
"""Base repository with common database operations"""
|
||||||
|
|
||||||
def __init__(self, model):
|
def __init__(self, model):
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|
||||||
def get_by_id(self, id):
|
def get_by_id(self, id):
|
||||||
"""Get entity by ID"""
|
"""Get entity by ID"""
|
||||||
return self.model.query.get(id)
|
return self.model.query.get(id)
|
||||||
|
|
||||||
def get_by_company(self, company_id=None):
|
def get_by_company(self, company_id=None):
|
||||||
"""Get all entities for a company"""
|
"""Get all entities for a company"""
|
||||||
if company_id is None and hasattr(g, 'user') and g.user:
|
if company_id is None and hasattr(g, 'user') and g.user:
|
||||||
company_id = g.user.company_id
|
company_id = g.user.company_id
|
||||||
|
|
||||||
if company_id is None:
|
if company_id is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return self.model.query.filter_by(company_id=company_id).all()
|
return self.model.query.filter_by(company_id=company_id).all()
|
||||||
|
|
||||||
def get_by_company_ordered(self, company_id=None, order_by=None):
|
def get_by_company_ordered(self, company_id=None, order_by=None):
|
||||||
"""Get all entities for a company with ordering"""
|
"""Get all entities for a company with ordering"""
|
||||||
if company_id is None and hasattr(g, 'user') and g.user:
|
if company_id is None and hasattr(g, 'user') and g.user:
|
||||||
company_id = g.user.company_id
|
company_id = g.user.company_id
|
||||||
|
|
||||||
if company_id is None:
|
if company_id is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
query = self.model.query.filter_by(company_id=company_id)
|
query = self.model.query.filter_by(company_id=company_id)
|
||||||
|
|
||||||
if order_by is not None:
|
if order_by is not None:
|
||||||
query = query.order_by(order_by)
|
query = query.order_by(order_by)
|
||||||
|
|
||||||
return query.all()
|
return query.all()
|
||||||
|
|
||||||
def exists_by_name_in_company(self, name, company_id=None, exclude_id=None):
|
def exists_by_name_in_company(self, name, company_id=None, exclude_id=None):
|
||||||
"""Check if entity with name exists in company"""
|
"""Check if entity with name exists in company"""
|
||||||
if company_id is None and hasattr(g, 'user') and g.user:
|
if company_id is None and hasattr(g, 'user') and g.user:
|
||||||
company_id = g.user.company_id
|
company_id = g.user.company_id
|
||||||
|
|
||||||
query = self.model.query.filter_by(name=name, company_id=company_id)
|
query = self.model.query.filter_by(name=name, company_id=company_id)
|
||||||
|
|
||||||
if exclude_id is not None:
|
if exclude_id is not None:
|
||||||
query = query.filter(self.model.id != exclude_id)
|
query = query.filter(self.model.id != exclude_id)
|
||||||
|
|
||||||
return query.first() is not None
|
return query.first() is not None
|
||||||
|
|
||||||
def create(self, **kwargs):
|
def create(self, **kwargs):
|
||||||
"""Create new entity"""
|
"""Create new entity"""
|
||||||
entity = self.model(**kwargs)
|
entity = self.model(**kwargs)
|
||||||
db.session.add(entity)
|
db.session.add(entity)
|
||||||
return entity
|
return entity
|
||||||
|
|
||||||
def update(self, entity, **kwargs):
|
def update(self, entity, **kwargs):
|
||||||
"""Update entity with given attributes"""
|
"""Update entity with given attributes"""
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
if hasattr(entity, key):
|
if hasattr(entity, key):
|
||||||
setattr(entity, key, value)
|
setattr(entity, key, value)
|
||||||
return entity
|
return entity
|
||||||
|
|
||||||
def delete(self, entity):
|
def delete(self, entity):
|
||||||
"""Delete entity"""
|
"""Delete entity"""
|
||||||
db.session.delete(entity)
|
db.session.delete(entity)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Commit changes to database"""
|
"""Commit changes to database"""
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def rollback(self):
|
def rollback(self):
|
||||||
"""Rollback database changes"""
|
"""Rollback database changes"""
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
@@ -81,42 +81,42 @@ class BaseRepository:
|
|||||||
|
|
||||||
class CompanyScopedRepository(BaseRepository):
|
class CompanyScopedRepository(BaseRepository):
|
||||||
"""Repository for entities scoped to a company"""
|
"""Repository for entities scoped to a company"""
|
||||||
|
|
||||||
def get_by_id_and_company(self, id, company_id=None):
|
def get_by_id_and_company(self, id, company_id=None):
|
||||||
"""Get entity by ID, ensuring it belongs to the company"""
|
"""Get entity by ID, ensuring it belongs to the company"""
|
||||||
if company_id is None and hasattr(g, 'user') and g.user:
|
if company_id is None and hasattr(g, 'user') and g.user:
|
||||||
company_id = g.user.company_id
|
company_id = g.user.company_id
|
||||||
|
|
||||||
if company_id is None:
|
if company_id is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self.model.query.filter_by(id=id, company_id=company_id).first()
|
return self.model.query.filter_by(id=id, company_id=company_id).first()
|
||||||
|
|
||||||
def get_active_by_company(self, company_id=None):
|
def get_active_by_company(self, company_id=None):
|
||||||
"""Get active entities for a company"""
|
"""Get active entities for a company"""
|
||||||
if company_id is None and hasattr(g, 'user') and g.user:
|
if company_id is None and hasattr(g, 'user') and g.user:
|
||||||
company_id = g.user.company_id
|
company_id = g.user.company_id
|
||||||
|
|
||||||
if company_id is None:
|
if company_id is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Assumes model has is_active field
|
# Assumes model has is_active field
|
||||||
if hasattr(self.model, 'is_active'):
|
if hasattr(self.model, 'is_active'):
|
||||||
return self.model.query.filter_by(
|
return self.model.query.filter_by(
|
||||||
company_id=company_id,
|
company_id=company_id,
|
||||||
is_active=True
|
is_active=True
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
return self.get_by_company(company_id)
|
return self.get_by_company(company_id)
|
||||||
|
|
||||||
def count_by_company(self, company_id=None):
|
def count_by_company(self, company_id=None):
|
||||||
"""Count entities for a company"""
|
"""Count entities for a company"""
|
||||||
if company_id is None and hasattr(g, 'user') and g.user:
|
if company_id is None and hasattr(g, 'user') and g.user:
|
||||||
company_id = g.user.company_id
|
company_id = g.user.company_id
|
||||||
|
|
||||||
if company_id is None:
|
if company_id is None:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
return self.model.query.filter_by(company_id=company_id).count()
|
return self.model.query.filter_by(company_id=company_id).count()
|
||||||
|
|
||||||
|
|
||||||
@@ -124,18 +124,18 @@ class CompanyScopedRepository(BaseRepository):
|
|||||||
|
|
||||||
class UserRepository(CompanyScopedRepository):
|
class UserRepository(CompanyScopedRepository):
|
||||||
"""Repository for User operations"""
|
"""Repository for User operations"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
from models import User
|
from models import User
|
||||||
super().__init__(User)
|
super().__init__(User)
|
||||||
|
|
||||||
def get_by_username_and_company(self, username, company_id):
|
def get_by_username_and_company(self, username, company_id):
|
||||||
"""Get user by username within a company"""
|
"""Get user by username within a company"""
|
||||||
return self.model.query.filter_by(
|
return self.model.query.filter_by(
|
||||||
username=username,
|
username=username,
|
||||||
company_id=company_id
|
company_id=company_id
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
def get_by_email(self, email):
|
def get_by_email(self, email):
|
||||||
"""Get user by email (globally unique)"""
|
"""Get user by email (globally unique)"""
|
||||||
return self.model.query.filter_by(email=email).first()
|
return self.model.query.filter_by(email=email).first()
|
||||||
@@ -143,19 +143,19 @@ class UserRepository(CompanyScopedRepository):
|
|||||||
|
|
||||||
class TeamRepository(CompanyScopedRepository):
|
class TeamRepository(CompanyScopedRepository):
|
||||||
"""Repository for Team operations"""
|
"""Repository for Team operations"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
from models import Team
|
from models import Team
|
||||||
super().__init__(Team)
|
super().__init__(Team)
|
||||||
|
|
||||||
def get_with_member_count(self, company_id=None):
|
def get_with_member_count(self, company_id=None):
|
||||||
"""Get teams with member count"""
|
"""Get teams with member count"""
|
||||||
if company_id is None and hasattr(g, 'user') and g.user:
|
if company_id is None and hasattr(g, 'user') and g.user:
|
||||||
company_id = g.user.company_id
|
company_id = g.user.company_id
|
||||||
|
|
||||||
if company_id is None:
|
if company_id is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# This would need a more complex query with joins
|
# This would need a more complex query with joins
|
||||||
teams = self.get_by_company(company_id)
|
teams = self.get_by_company(company_id)
|
||||||
for team in teams:
|
for team in teams:
|
||||||
@@ -165,27 +165,27 @@ class TeamRepository(CompanyScopedRepository):
|
|||||||
|
|
||||||
class ProjectRepository(CompanyScopedRepository):
|
class ProjectRepository(CompanyScopedRepository):
|
||||||
"""Repository for Project operations"""
|
"""Repository for Project operations"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
from models import Project
|
from models import Project
|
||||||
super().__init__(Project)
|
super().__init__(Project)
|
||||||
|
|
||||||
def get_by_code_and_company(self, code, company_id):
|
def get_by_code_and_company(self, code, company_id):
|
||||||
"""Get project by code within a company"""
|
"""Get project by code within a company"""
|
||||||
return self.model.query.filter_by(
|
return self.model.query.filter_by(
|
||||||
code=code,
|
code=code,
|
||||||
company_id=company_id
|
company_id=company_id
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
def get_accessible_by_user(self, user):
|
def get_accessible_by_user(self, user):
|
||||||
"""Get projects accessible by a user"""
|
"""Get projects accessible by a user"""
|
||||||
if not user:
|
if not user:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Admin/Supervisor can see all company projects
|
# Admin/Supervisor can see all company projects
|
||||||
if user.role.value in ['Administrator', 'Supervisor', 'System Administrator']:
|
if user.role.value in ['Administrator', 'Supervisor', 'System Administrator']:
|
||||||
return self.get_by_company(user.company_id)
|
return self.get_by_company(user.company_id)
|
||||||
|
|
||||||
# Team members see team projects + unassigned projects
|
# Team members see team projects + unassigned projects
|
||||||
from models import Project
|
from models import Project
|
||||||
return Project.query.filter(
|
return Project.query.filter(
|
||||||
@@ -194,4 +194,4 @@ class ProjectRepository(CompanyScopedRepository):
|
|||||||
Project.team_id == user.team_id,
|
Project.team_id == user.team_id,
|
||||||
Project.team_id.is_(None)
|
Project.team_id.is_(None)
|
||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
|
|||||||
Reference in New Issue
Block a user