Sergidev commited on
Commit
d4672d0
·
verified ·
1 Parent(s): 30344f1
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.10 as the base image
2
+ FROM python:3.10
3
+
4
+ # Set environment variables
5
+ RUN useradd -m -u 1000 user
6
+ USER user
7
+ ENV PATH="/home/user/.local/bin:$PATH"
8
+
9
+ WORKDIR /app
10
+
11
+ COPY --chown=user ./requirements.txt requirements.txt
12
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
13
+
14
+ COPY --chown=user . /app
15
+
16
+ # Set environment variables
17
+ ENV FLASK_APP=run.py
18
+ ENV FLASK_ENV=production
19
+
20
+ # Run the application
21
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "run:app"]
app/__init__.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask
2
+ from flask_sqlalchemy import SQLAlchemy
3
+ from flask_login import LoginManager
4
+ from .config import Config
5
+ import os
6
+ from apscheduler.schedulers.background import BackgroundScheduler
7
+
8
+ db = SQLAlchemy()
9
+ login_manager = LoginManager()
10
+ scheduler = BackgroundScheduler()
11
+
12
+ def create_app():
13
+ app = Flask(__name__,
14
+ template_folder=os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'templates'),
15
+ static_folder=os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'static'))
16
+ app.config.from_object(Config)
17
+
18
+ db.init_app(app)
19
+ login_manager.init_app(app)
20
+ login_manager.login_view = 'auth.login'
21
+
22
+ from .routes import main, auth, files, admin
23
+ app.register_blueprint(main)
24
+ app.register_blueprint(auth)
25
+ app.register_blueprint(files)
26
+ app.register_blueprint(admin, url_prefix='/admin')
27
+
28
+ with app.app_context():
29
+ db.create_all()
30
+ create_admin_user()
31
+
32
+ scheduler.add_job(delete_inactive_users, 'interval', hours=1)
33
+ scheduler.start()
34
+
35
+ return app
36
+
37
+ def create_admin_user():
38
+ from .models import User
39
+ from .utils import create_user
40
+
41
+ admin_username = os.getenv('ADMIN_USERNAME')
42
+ admin_password = os.getenv('ADMIN_PASSWORD')
43
+
44
+ if admin_username and admin_password:
45
+ existing_admin = User.query.filter_by(username=admin_username).first()
46
+ if not existing_admin:
47
+ result = create_user(admin_username, admin_password)
48
+ if "successfully" in result:
49
+ admin_user = User.query.filter_by(username=admin_username).first()
50
+ admin_user.is_admin = True
51
+ admin_user.storage_limit = 1024 * 1024 * 1024 * 10 # 10 GB for admin
52
+ db.session.commit()
53
+ print(f"Admin user '{admin_username}' created successfully.")
54
+ else:
55
+ print(f"Failed to create admin user: {result}")
56
+ else:
57
+ print(f"Admin user '{admin_username}' already exists.")
58
+ else:
59
+ print("Admin credentials not provided in environment variables.")
60
+
61
+ def delete_inactive_users():
62
+ from .models import User
63
+ from datetime import datetime, timedelta
64
+ with db.app.app_context():
65
+ inactive_threshold = datetime.utcnow() - timedelta(hours=42)
66
+ inactive_users = User.query.filter(User.last_active < inactive_threshold).all()
67
+ for user in inactive_users:
68
+ db.session.delete(user)
69
+ db.session.commit()
app/app_test.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ branchtest
app/config.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import timedelta
3
+
4
+ basedir = os.path.abspath(os.path.dirname(__file__))
5
+
6
+ class Config:
7
+ SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
8
+ SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
9
+ 'sqlite:///' + os.path.join(basedir, '..', 'grimvault.db')
10
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
11
+
12
+ # File upload settings
13
+ MAX_CONTENT_LENGTH = 5 * 1024 * 1024 * 1024 # 5 GB
14
+ UPLOAD_FOLDER = os.path.join(basedir, '..', 'uploads')
15
+
16
+ # Session settings
17
+ PERMANENT_SESSION_LIFETIME = timedelta(minutes=5)
18
+
19
+ # Custom settings
20
+ ADMIN_USERNAME = os.environ.get('ADMIN_USERNAME')
21
+ ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD')
22
+ HF_TOKEN = os.environ.get('HF_TOKEN')
23
+ SECRET_M = os.environ.get('SECRET_M')
24
+
25
+ # Rate limiting
26
+ RATELIMIT_DEFAULT = "5 per minute"
27
+ RATELIMIT_STORAGE_URL = "memory://"
28
+
29
+ # Default storage limit (5 GB in bytes)
30
+ DEFAULT_STORAGE_LIMIT = 5 * 1024 * 1024 * 1024
app/models.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from . import db
2
+ from flask_login import UserMixin
3
+ from werkzeug.security import generate_password_hash, check_password_hash
4
+ from datetime import datetime
5
+ from .config import Config
6
+
7
+ class User(UserMixin, db.Model):
8
+ id = db.Column(db.Integer, primary_key=True)
9
+ username = db.Column(db.String(64), index=True, unique=True)
10
+ password_hash = db.Column(db.String(128))
11
+ embedding_hash = db.Column(db.String(256))
12
+ salt = db.Column(db.String(32))
13
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
14
+ last_active = db.Column(db.DateTime, default=datetime.utcnow)
15
+ is_admin = db.Column(db.Boolean, default=False)
16
+ is_banned = db.Column(db.Boolean, default=False)
17
+ storage_limit = db.Column(db.BigInteger, default=Config.DEFAULT_STORAGE_LIMIT)
18
+ files = db.relationship('File', backref='owner', lazy='dynamic', cascade="all, delete-orphan")
19
+
20
+ def set_password(self, password):
21
+ self.password_hash = generate_password_hash(password)
22
+
23
+ def check_password(self, password):
24
+ return check_password_hash(self.password_hash, password)
25
+
26
+ def get_used_storage(self):
27
+ return sum(file.size for file in self.files)
28
+
29
+ def update_last_active(self):
30
+ self.last_active = datetime.utcnow()
31
+ db.session.commit()
32
+
33
+ class File(db.Model):
34
+ id = db.Column(db.Integer, primary_key=True)
35
+ filename = db.Column(db.String(256))
36
+ content = db.Column(db.LargeBinary)
37
+ size = db.Column(db.BigInteger)
38
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
39
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
40
+
41
+ from . import login_manager
42
+
43
+ @login_manager.user_loader
44
+ def load_user(id):
45
+ return User.query.get(int(id))
app/routes.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, request, jsonify, send_file, abort, redirect, url_for
2
+ from flask_login import login_required, current_user, login_user, logout_user
3
+ from werkzeug.utils import secure_filename
4
+ from .models import User, File
5
+ from . import db
6
+ from .utils import (create_user, verify_user, get_user_files, upload_file,
7
+ download_file, delete_file, empty_vault, is_admin,
8
+ get_all_accounts, delete_account, is_rate_limited,
9
+ is_account_locked, record_login_attempt, update_storage_limit, ban_user)
10
+ from flask_limiter import Limiter
11
+ from flask_limiter.util import get_remote_address
12
+ import io
13
+
14
+ main = Blueprint('main', __name__)
15
+ auth = Blueprint('auth', __name__)
16
+ files = Blueprint('files', __name__)
17
+ admin = Blueprint('admin', __name__)
18
+
19
+ limiter = Limiter(key_func=get_remote_address)
20
+
21
+ @main.route('/')
22
+ def index():
23
+ if current_user.is_authenticated:
24
+ current_user.update_last_active()
25
+ if current_user.is_admin:
26
+ return redirect(url_for('admin.admin_dashboard'))
27
+ return redirect(url_for('files.dashboard'))
28
+ return render_template('index.html')
29
+
30
+ @auth.route('/login', methods=['GET', 'POST'])
31
+ @limiter.limit("5 per minute")
32
+ def login():
33
+ if current_user.is_authenticated:
34
+ if current_user.is_admin:
35
+ return redirect(url_for('admin.admin_dashboard'))
36
+ return redirect(url_for('files.dashboard'))
37
+
38
+ if request.method == 'POST':
39
+ username = request.form.get('username')
40
+ password = request.form.get('password')
41
+
42
+ if is_rate_limited(username) or is_account_locked(username):
43
+ return jsonify({"error": "Too many attempts. Please try again later."}), 429
44
+
45
+ user = User.query.filter_by(username=username).first()
46
+ if user and verify_user(username, password):
47
+ if user.is_banned:
48
+ return jsonify({"error": "This account has been banned."}), 403
49
+ login_user(user)
50
+ user.update_last_active()
51
+ record_login_attempt(username, True)
52
+ if user.is_admin:
53
+ return jsonify({"message": "Login successful", "redirect": url_for('admin.admin_dashboard')}), 200
54
+ return jsonify({"message": "Login successful", "redirect": url_for('files.dashboard')}), 200
55
+ else:
56
+ record_login_attempt(username, False)
57
+ return jsonify({"error": "Invalid username or password"}), 401
58
+
59
+ return render_template('login.html')
60
+
61
+ @auth.route('/register', methods=['GET', 'POST'])
62
+ def register():
63
+ if request.method == 'POST':
64
+ username = request.form.get('username')
65
+ password = request.form.get('password')
66
+ result = create_user(username, password)
67
+ if "successfully" in result:
68
+ return jsonify({"message": result, "redirect": url_for('auth.login')}), 201
69
+ else:
70
+ return jsonify({"error": result}), 400
71
+
72
+ return render_template('register.html')
73
+
74
+ @auth.route('/logout')
75
+ @login_required
76
+ def logout():
77
+ logout_user()
78
+ return redirect(url_for('main.index'))
79
+
80
+ @files.route('/dashboard')
81
+ @login_required
82
+ def dashboard():
83
+ if current_user.is_admin:
84
+ return redirect(url_for('admin.admin_dashboard'))
85
+ current_user.update_last_active()
86
+ user_files = get_user_files(current_user.username)
87
+ used_storage = current_user.get_used_storage()
88
+ return render_template('dashboard.html', files=user_files, used_storage=used_storage, storage_limit=current_user.storage_limit)
89
+
90
+ @files.route('/upload', methods=['POST'])
91
+ @login_required
92
+ def upload():
93
+ current_user.update_last_active()
94
+ if current_user.is_admin:
95
+ return jsonify({"error": "Admins cannot upload files"}), 403
96
+ if 'file' not in request.files:
97
+ return jsonify({"error": "No file part"}), 400
98
+ file = request.files['file']
99
+ if file.filename == '':
100
+ return jsonify({"error": "No selected file"}), 400
101
+ if file:
102
+ filename = secure_filename(file.filename)
103
+ result = upload_file(current_user.username, filename, file.read())
104
+ return jsonify({"message": result}), 200
105
+
106
+ @files.route('/download/<filename>')
107
+ @login_required
108
+ def download(filename):
109
+ current_user.update_last_active()
110
+ if current_user.is_admin:
111
+ return jsonify({"error": "Admins cannot download files"}), 403
112
+ file_content = download_file(current_user.username, filename)
113
+ if file_content:
114
+ return send_file(
115
+ io.BytesIO(file_content),
116
+ mimetype='application/octet-stream',
117
+ as_attachment=True,
118
+ download_name=filename
119
+ )
120
+ else:
121
+ return jsonify({"error": "File not found"}), 404
122
+
123
+ @files.route('/delete/<filename>', methods=['DELETE'])
124
+ @login_required
125
+ def delete(filename):
126
+ current_user.update_last_active()
127
+ if current_user.is_admin:
128
+ return jsonify({"error": "Admins cannot delete files"}), 403
129
+ result = delete_file(current_user.username, filename)
130
+ return jsonify({"message": result}), 200
131
+
132
+ @files.route('/empty', methods=['POST'])
133
+ @login_required
134
+ def empty():
135
+ current_user.update_last_active()
136
+ if current_user.is_admin:
137
+ return jsonify({"error": "Admins cannot empty vault"}), 403
138
+ password = request.form.get('password')
139
+ if verify_user(current_user.username, password):
140
+ result = empty_vault(current_user.username)
141
+ return jsonify({"message": result}), 200
142
+ else:
143
+ return jsonify({"error": "Invalid password"}), 401
144
+
145
+ @admin.route('/dashboard')
146
+ @login_required
147
+ def admin_dashboard():
148
+ if not current_user.is_admin:
149
+ abort(403)
150
+ current_user.update_last_active()
151
+ accounts = get_all_accounts()
152
+ return render_template('admindash.html', accounts=accounts)
153
+
154
+ @admin.route('/update_storage', methods=['POST'])
155
+ @login_required
156
+ def update_storage():
157
+ if not current_user.is_admin:
158
+ return jsonify({"error": "Access denied"}), 403
159
+ current_user.update_last_active()
160
+ username = request.form.get('username')
161
+ new_limit = request.form.get('new_limit')
162
+ result = update_storage_limit(username, int(new_limit))
163
+ return jsonify({"message": result}), 200
164
+
165
+ @admin.route('/ban_user', methods=['POST'])
166
+ @login_required
167
+ def ban_user_route():
168
+ if not current_user.is_admin:
169
+ return jsonify({"error": "Access denied"}), 403
170
+ current_user.update_last_active()
171
+ username = request.form.get('username')
172
+ ban_status = request.form.get('ban_status') == 'true'
173
+ result = ban_user(username, ban_status)
174
+ return jsonify({"message": result}), 200
175
+
176
+ @admin.route('/delete/<username>', methods=['DELETE'])
177
+ @login_required
178
+ def admin_delete_account(username):
179
+ if not current_user.is_admin:
180
+ return jsonify({"error": "Access denied"}), 403
181
+ current_user.update_last_active()
182
+ result = delete_account(username)
183
+ return jsonify({"message": result}), 200
app/utils.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import secrets
3
+ import hashlib
4
+ import time
5
+ from argon2 import PasswordHasher
6
+ from cryptography.fernet import Fernet
7
+ from transformers import AutoTokenizer, AutoModel
8
+ import torch
9
+ import numpy as np
10
+ from .models import User, File
11
+ from . import db
12
+ from huggingface_hub import hf_hub_download
13
+ import requests
14
+ from dotenv import load_dotenv
15
+
16
+ load_dotenv()
17
+
18
+ # Initialize global variables
19
+ MODEL_NAME = os.getenv('SECRET_M')
20
+ HF_TOKEN = os.getenv('HF_TOKEN')
21
+
22
+ tokenizer = None
23
+ model = None
24
+
25
+ # Initialize Argon2 hasher and Fernet cipher
26
+ ph = PasswordHasher()
27
+ cipher_key = Fernet.generate_key()
28
+ cipher = Fernet(cipher_key)
29
+
30
+ def get_embedding(text):
31
+ global tokenizer, model
32
+
33
+ if tokenizer is None or model is None:
34
+ try:
35
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, use_auth_token=HF_TOKEN)
36
+ model = AutoModel.from_pretrained(MODEL_NAME, torch_dtype=torch.float16, use_auth_token=HF_TOKEN)
37
+
38
+ if tokenizer.pad_token is None:
39
+ tokenizer.pad_token = tokenizer.eos_token
40
+
41
+ model.resize_token_embeddings(len(tokenizer))
42
+ except (requests.exceptions.RequestException, OSError) as e:
43
+ print(f"Error loading model: {str(e)}")
44
+ return None
45
+
46
+ inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=512)
47
+ with torch.no_grad():
48
+ outputs = model(**inputs)
49
+ return outputs.last_hidden_state.mean(dim=1).squeeze().numpy()
50
+
51
+ def hash_embedding(embedding, salt):
52
+ salted_embedding = np.concatenate([embedding, np.frombuffer(salt, dtype=np.float32)])
53
+ return hashlib.sha256(salted_embedding.tobytes()).hexdigest()
54
+
55
+ def create_user(username, password):
56
+ if User.query.filter_by(username=username).first():
57
+ return "Username already exists."
58
+
59
+ salt = secrets.token_bytes(16)
60
+ embedding = get_embedding(password)
61
+ if embedding is None:
62
+ return "Error creating user. Please try again later."
63
+ embedding_hash = hash_embedding(embedding, salt)
64
+
65
+ new_user = User(username=username, salt=salt.hex(), embedding_hash=embedding_hash)
66
+ new_user.set_password(password)
67
+
68
+ db.session.add(new_user)
69
+ db.session.commit()
70
+
71
+ return "User created successfully."
72
+
73
+ def verify_user(username, password):
74
+ user = User.query.filter_by(username=username).first()
75
+ if not user:
76
+ return False
77
+
78
+ if not user.check_password(password):
79
+ return False
80
+
81
+ embedding = get_embedding(password)
82
+ if embedding is None:
83
+ return False
84
+ embedding_hash = hash_embedding(embedding, bytes.fromhex(user.salt))
85
+ return embedding_hash == user.embedding_hash
86
+
87
+ def get_user_files(username):
88
+ user = User.query.filter_by(username=username).first()
89
+ if not user:
90
+ return []
91
+ return [{"filename": file.filename, "size": file.size} for file in user.files]
92
+
93
+ def upload_file(username, filename, content):
94
+ user = User.query.filter_by(username=username).first()
95
+ if not user:
96
+ return "User not found."
97
+
98
+ if user.get_used_storage() + len(content) > user.storage_limit:
99
+ return "Storage limit exceeded."
100
+
101
+ existing_file = File.query.filter_by(user_id=user.id, filename=filename).first()
102
+ if existing_file:
103
+ return f"File {filename} already exists."
104
+
105
+ encrypted_content = cipher.encrypt(content)
106
+ new_file = File(filename=filename, content=encrypted_content, size=len(content), user_id=user.id)
107
+ db.session.add(new_file)
108
+ db.session.commit()
109
+
110
+ return f"File {filename} uploaded successfully."
111
+
112
+ def download_file(username, filename):
113
+ user = User.query.filter_by(username=username).first()
114
+ if not user:
115
+ return None
116
+
117
+ file = File.query.filter_by(user_id=user.id, filename=filename).first()
118
+ if not file:
119
+ return None
120
+
121
+ return cipher.decrypt(file.content)
122
+
123
+ def delete_file(username, filename):
124
+ user = User.query.filter_by(username=username).first()
125
+ if not user:
126
+ return "User not found."
127
+
128
+ file = File.query.filter_by(user_id=user.id, filename=filename).first()
129
+ if not file:
130
+ return f"File {filename} not found."
131
+
132
+ db.session.delete(file)
133
+ db.session.commit()
134
+ return f"File {filename} deleted successfully."
135
+
136
+ def empty_vault(username):
137
+ user = User.query.filter_by(username=username).first()
138
+ if not user:
139
+ return "User not found."
140
+
141
+ File.query.filter_by(user_id=user.id).delete()
142
+ db.session.commit()
143
+ return "All files in your vault have been deleted."
144
+
145
+ def is_admin(username):
146
+ user = User.query.filter_by(username=username).first()
147
+ return user and user.is_admin
148
+
149
+ def get_all_accounts():
150
+ return [{"username": user.username, "created_at": user.created_at, "last_active": user.last_active, "storage_used": user.get_used_storage(), "storage_limit": user.storage_limit, "is_banned": user.is_banned} for user in User.query.all()]
151
+
152
+ def delete_account(username):
153
+ if username == os.getenv('ADMIN_USERNAME'):
154
+ return "Cannot delete admin account."
155
+
156
+ user = User.query.filter_by(username=username).first()
157
+ if not user:
158
+ return "User not found."
159
+
160
+ File.query.filter_by(user_id=user.id).delete()
161
+ db.session.delete(user)
162
+ db.session.commit()
163
+ return f"Account {username} and all associated files have been deleted."
164
+
165
+ def update_storage_limit(username, new_limit):
166
+ user = User.query.filter_by(username=username).first()
167
+ if not user:
168
+ return "User not found."
169
+
170
+ user.storage_limit = new_limit
171
+ db.session.commit()
172
+ return f"Storage limit for {username} updated to {new_limit} bytes."
173
+
174
+ def ban_user(username, ban_status):
175
+ user = User.query.filter_by(username=username).first()
176
+ if not user:
177
+ return "User not found."
178
+
179
+ user.is_banned = ban_status
180
+ db.session.commit()
181
+ action = "banned" if ban_status else "unbanned"
182
+ return f"User {username} has been {action}."
183
+
184
+ # Rate limiting
185
+ RATE_LIMIT = 5 # maximum number of requests per minute
186
+ rate_limit_dict = {}
187
+
188
+ def is_rate_limited(username):
189
+ current_time = time.time()
190
+ if username in rate_limit_dict:
191
+ last_request_time, count = rate_limit_dict[username]
192
+ if current_time - last_request_time < 60: # within 1 minute
193
+ if count >= RATE_LIMIT:
194
+ return True
195
+ rate_limit_dict[username] = (last_request_time, count + 1)
196
+ else:
197
+ rate_limit_dict[username] = (current_time, 1)
198
+ else:
199
+ rate_limit_dict[username] = (current_time, 1)
200
+ return False
201
+
202
+ # Account lockout
203
+ MAX_LOGIN_ATTEMPTS = 5
204
+ LOCKOUT_TIME = 300 # 5 minutes
205
+ lockout_dict = {}
206
+
207
+ def is_account_locked(username):
208
+ if username in lockout_dict:
209
+ attempts, lockout_time = lockout_dict[username]
210
+ if attempts >= MAX_LOGIN_ATTEMPTS:
211
+ if time.time() - lockout_time < LOCKOUT_TIME:
212
+ return True
213
+ else:
214
+ del lockout_dict[username]
215
+ return False
216
+
217
+ def record_login_attempt(username, success):
218
+ if username not in lockout_dict:
219
+ lockout_dict[username] = [0, 0]
220
+
221
+ if success:
222
+ del lockout_dict[username]
223
+ else:
224
+ lockout_dict[username][0] += 1
225
+ lockout_dict[username][1] = time.time()
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Flask
2
+ Flask-SQLAlchemy
3
+ Flask-Login
4
+ flask-limiter
5
+ Flask-WTF
6
+ SQLAlchemy
7
+ Werkzeug
8
+ argon2-cffi
9
+ cryptography
10
+ python-dotenv
11
+ gunicorn
12
+ transformers
13
+ torch
14
+ numpy
15
+ huggingface-hub
16
+ apscheduler
run.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app import create_app, db
2
+ from app.models import User, File
3
+
4
+ app = create_app()
5
+
6
+ @app.shell_context_processor
7
+ def make_shell_context():
8
+ return {'db': db, 'User': User, 'File': File}
9
+
10
+ if __name__ == '__main__':
11
+ app.run(debug=True)
static/css/styles.css ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg-color: #121212;
3
+ --text-color: #e0e0e0;
4
+ --primary-color: #bb86fc;
5
+ --secondary-color: #03dac6;
6
+ --danger-color: #cf6679;
7
+ --card-bg: #1e1e1e;
8
+ --input-bg: #2c2c2c;
9
+ --border-color: #333333;
10
+ }
11
+
12
+ * {
13
+ box-sizing: border-box;
14
+ margin: 0;
15
+ padding: 0;
16
+ }
17
+
18
+ body {
19
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
20
+ background-color: var(--bg-color);
21
+ color: var(--text-color);
22
+ line-height: 1.6;
23
+ margin: 0;
24
+ padding: 0;
25
+ }
26
+
27
+ .container {
28
+ max-width: 1200px;
29
+ margin: 0 auto;
30
+ padding: 20px;
31
+ }
32
+
33
+ header {
34
+ background-color: var(--card-bg);
35
+ padding: 1rem 0;
36
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
37
+ }
38
+
39
+ nav ul {
40
+ list-style-type: none;
41
+ padding: 0;
42
+ display: flex;
43
+ justify-content: center;
44
+ flex-wrap: wrap;
45
+ }
46
+
47
+ nav ul li {
48
+ margin: 0 15px;
49
+ }
50
+
51
+ nav ul li a {
52
+ color: var(--text-color);
53
+ text-decoration: none;
54
+ font-weight: bold;
55
+ transition: color 0.3s ease;
56
+ }
57
+
58
+ nav ul li a:hover {
59
+ color: var(--secondary-color);
60
+ }
61
+
62
+ h1,
63
+ h2,
64
+ h3 {
65
+ color: var(--primary-color);
66
+ margin-bottom: 1rem;
67
+ }
68
+
69
+ .btn {
70
+ display: inline-block;
71
+ background-color: var(--primary-color);
72
+ color: var(--bg-color);
73
+ padding: 10px 20px;
74
+ border: none;
75
+ border-radius: 5px;
76
+ cursor: pointer;
77
+ transition:
78
+ background-color 0.3s ease,
79
+ transform 0.1s ease;
80
+ text-decoration: none;
81
+ font-size: 1rem;
82
+ margin: 5px;
83
+ }
84
+
85
+ .btn:hover {
86
+ background-color: #9a67ea;
87
+ transform: translateY(-2px);
88
+ }
89
+
90
+ .btn:active {
91
+ transform: translateY(0);
92
+ }
93
+
94
+ .btn-secondary {
95
+ background-color: var(--secondary-color);
96
+ }
97
+
98
+ .btn-secondary:hover {
99
+ background-color: #04b7a1;
100
+ }
101
+
102
+ .btn-danger {
103
+ background-color: var(--danger-color);
104
+ }
105
+
106
+ .btn-danger:hover {
107
+ background-color: #ff5c8d;
108
+ }
109
+
110
+ form {
111
+ background-color: var(--card-bg);
112
+ padding: 20px;
113
+ border-radius: 8px;
114
+ margin-bottom: 20px;
115
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
116
+ }
117
+
118
+ .form-group {
119
+ margin-bottom: 1rem;
120
+ }
121
+
122
+ input[type="text"],
123
+ input[type="password"],
124
+ input[type="file"] {
125
+ width: 100%;
126
+ padding: 10px;
127
+ margin-bottom: 10px;
128
+ border: 1px solid var(--border-color);
129
+ border-radius: 4px;
130
+ background-color: var(--input-bg);
131
+ color: var(--text-color);
132
+ font-size: 1rem;
133
+ }
134
+
135
+ input[type="file"] {
136
+ padding: 5px;
137
+ }
138
+
139
+ label {
140
+ display: block;
141
+ margin-bottom: 5px;
142
+ color: var(--secondary-color);
143
+ }
144
+
145
+ #file-list {
146
+ list-style-type: none;
147
+ padding: 0;
148
+ }
149
+
150
+ #file-list li {
151
+ background-color: var(--card-bg);
152
+ margin-bottom: 10px;
153
+ padding: 15px;
154
+ border-radius: 5px;
155
+ display: flex;
156
+ justify-content: space-between;
157
+ align-items: center;
158
+ flex-wrap: wrap;
159
+ }
160
+
161
+ .file-actions {
162
+ display: flex;
163
+ justify-content: space-between;
164
+ margin-bottom: 20px;
165
+ flex-wrap: wrap;
166
+ }
167
+
168
+ table {
169
+ width: 100%;
170
+ border-collapse: collapse;
171
+ margin-top: 20px;
172
+ background-color: var(--card-bg);
173
+ border-radius: 8px;
174
+ overflow: hidden;
175
+ }
176
+
177
+ th,
178
+ td {
179
+ text-align: left;
180
+ padding: 12px;
181
+ border-bottom: 1px solid var(--border-color);
182
+ }
183
+
184
+ th {
185
+ background-color: var(--primary-color);
186
+ color: var(--bg-color);
187
+ }
188
+
189
+ tr:nth-child(even) {
190
+ background-color: rgba(255, 255, 255, 0.05);
191
+ }
192
+
193
+ .loading {
194
+ position: fixed;
195
+ top: 50%;
196
+ left: 50%;
197
+ transform: translate(-50%, -50%);
198
+ background-color: rgba(0, 0, 0, 0.7);
199
+ color: var(--text-color);
200
+ padding: 20px;
201
+ border-radius: 5px;
202
+ z-index: 1000;
203
+ }
204
+
205
+ .storage-info {
206
+ margin-bottom: 20px;
207
+ }
208
+
209
+ progress {
210
+ width: 100%;
211
+ height: 20px;
212
+ -webkit-appearance: none;
213
+ appearance: none;
214
+ }
215
+
216
+ progress::-webkit-progress-bar {
217
+ background-color: var(--input-bg);
218
+ border-radius: 10px;
219
+ }
220
+
221
+ progress::-webkit-progress-value {
222
+ background-color: var(--primary-color);
223
+ border-radius: 10px;
224
+ }
225
+
226
+ progress::-moz-progress-bar {
227
+ background-color: var(--primary-color);
228
+ border-radius: 10px;
229
+ }
230
+
231
+ @media (max-width: 768px) {
232
+ .file-actions {
233
+ flex-direction: column;
234
+ }
235
+
236
+ .file-actions > * {
237
+ margin-bottom: 10px;
238
+ width: 100%;
239
+ }
240
+
241
+ #file-list li {
242
+ flex-direction: column;
243
+ align-items: flex-start;
244
+ }
245
+
246
+ #file-list li button {
247
+ margin-top: 10px;
248
+ }
249
+
250
+ table {
251
+ font-size: 0.9rem;
252
+ }
253
+
254
+ th,
255
+ td {
256
+ padding: 8px;
257
+ }
258
+ }
259
+
260
+ @media (max-width: 480px) {
261
+ .container {
262
+ padding: 10px;
263
+ }
264
+
265
+ h1 {
266
+ font-size: 1.5rem;
267
+ }
268
+
269
+ .btn {
270
+ font-size: 0.9rem;
271
+ padding: 8px 16px;
272
+ }
273
+
274
+ table {
275
+ font-size: 0.8rem;
276
+ }
277
+
278
+ th,
279
+ td {
280
+ padding: 6px;
281
+ }
282
+ }
static/js/main.js ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Loading indicator functions
2
+ function showLoading() {
3
+ document.getElementById('loading-indicator').style.display = 'block';
4
+ }
5
+
6
+ function hideLoading() {
7
+ document.getElementById('loading-indicator').style.display = 'none';
8
+ }
9
+
10
+ // Axios interceptors for loading indicator
11
+ axios.interceptors.request.use((config) => {
12
+ showLoading();
13
+ return config;
14
+ }, (error) => {
15
+ hideLoading();
16
+ return Promise.reject(error);
17
+ });
18
+
19
+ axios.interceptors.response.use((response) => {
20
+ hideLoading();
21
+ return response;
22
+ }, (error) => {
23
+ hideLoading();
24
+ return Promise.reject(error);
25
+ });
26
+
27
+ // Logout functionality
28
+ const logoutButton = document.getElementById('logout');
29
+ if (logoutButton) {
30
+ logoutButton.addEventListener('click', async (e) => {
31
+ e.preventDefault();
32
+ try {
33
+ const response = await axios.get('/logout');
34
+ window.location.href = '/';
35
+ } catch (error) {
36
+ console.error('Logout failed:', error);
37
+ alert('Logout failed. Please try again.');
38
+ }
39
+ });
40
+ }
41
+
42
+ // Error handling function
43
+ function handleError(error) {
44
+ console.error('Error:', error);
45
+ if (error.response) {
46
+ alert(`Error: ${error.response.data.error || 'An unexpected error occurred.'}`);
47
+ } else if (error.request) {
48
+ alert('Error: No response received from the server. Please check your internet connection.');
49
+ } else {
50
+ alert('Error: An unexpected error occurred. Please try again.');
51
+ }
52
+ }
templates/admindash.html ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %} {% block title %}Admin Dashboard - Grimvault{%
2
+ endblock %} {% block content %}
3
+ <div class="container">
4
+ <h1>Admin Dashboard</h1>
5
+ <table id="user-accounts">
6
+ <thead>
7
+ <tr>
8
+ <th>Username</th>
9
+ <th>Created At</th>
10
+ <th>Last Active</th>
11
+ <th>Storage Used</th>
12
+ <th>Storage Limit</th>
13
+ <th>Status</th>
14
+ <th>Actions</th>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+ {% for account in accounts %}
19
+ <tr>
20
+ <td>{{ account.username }}</td>
21
+ <td>{{ account.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
22
+ <td>{{ account.last_active.strftime('%Y-%m-%d %H:%M:%S') }}</td>
23
+ <td>
24
+ {{ (account.storage_used / 1024 / 1024) | round(2) }} MB
25
+ </td>
26
+ <td>
27
+ {{ (account.storage_limit / 1024 / 1024 / 1024) | round(2)
28
+ }} GB
29
+ </td>
30
+ <td>{{ 'Banned' if account.is_banned else 'Active' }}</td>
31
+ <td>
32
+ <button
33
+ class="btn btn-secondary update-storage"
34
+ data-username="{{ account.username }}"
35
+ >
36
+ Update Storage
37
+ </button>
38
+ <button
39
+ class="btn btn-warning toggle-ban"
40
+ data-username="{{ account.username }}"
41
+ data-banned="{{ account.is_banned }}"
42
+ >
43
+ {{ 'Unban' if account.is_banned else 'Ban' }}
44
+ </button>
45
+ <button
46
+ class="btn btn-danger delete-account"
47
+ data-username="{{ account.username }}"
48
+ >
49
+ Delete
50
+ </button>
51
+ </td>
52
+ </tr>
53
+ {% endfor %}
54
+ </tbody>
55
+ </table>
56
+ </div>
57
+ {% endblock %} {% block scripts %}
58
+ <script>
59
+ document.addEventListener("DOMContentLoaded", () => {
60
+ const userAccounts = document.getElementById("user-accounts");
61
+
62
+ userAccounts.addEventListener("click", async (e) => {
63
+ const username = e.target.dataset.username;
64
+
65
+ if (e.target.classList.contains("update-storage")) {
66
+ const newLimit = prompt("Enter new storage limit in GB:");
67
+ if (newLimit) {
68
+ try {
69
+ const response = await axios.post(
70
+ '{{ url_for("admin.update_storage") }}',
71
+ {
72
+ username: username,
73
+ new_limit: newLimit * 1024 * 1024 * 1024, // Convert GB to bytes
74
+ },
75
+ );
76
+ alert(response.data.message);
77
+ location.reload();
78
+ } catch (error) {
79
+ alert(
80
+ "Failed to update storage: " +
81
+ error.response.data.error,
82
+ );
83
+ }
84
+ }
85
+ } else if (e.target.classList.contains("toggle-ban")) {
86
+ const currentStatus = e.target.dataset.banned === "true";
87
+ const newStatus = !currentStatus;
88
+ try {
89
+ const response = await axios.post(
90
+ '{{ url_for("admin.ban_user_route") }}',
91
+ {
92
+ username: username,
93
+ ban_status: newStatus,
94
+ },
95
+ );
96
+ alert(response.data.message);
97
+ location.reload();
98
+ } catch (error) {
99
+ alert(
100
+ "Failed to update ban status: " +
101
+ error.response.data.error,
102
+ );
103
+ }
104
+ } else if (e.target.classList.contains("delete-account")) {
105
+ if (
106
+ confirm(
107
+ `Are you sure you want to delete the account for ${username}?`,
108
+ )
109
+ ) {
110
+ try {
111
+ const response = await axios.delete(
112
+ `{{ url_for("admin.admin_delete_account", username="") }}${username}`,
113
+ );
114
+ alert(response.data.message);
115
+ location.reload();
116
+ } catch (error) {
117
+ alert(
118
+ "Failed to delete account: " +
119
+ error.response.data.error,
120
+ );
121
+ }
122
+ }
123
+ }
124
+ });
125
+ });
126
+ </script>
127
+ {% endblock %}
templates/base.html ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{% block title %}Grimvault{% endblock %}</title>
7
+ <link
8
+ rel="stylesheet"
9
+ href="{{ url_for('static', filename='css/styles.css') }}"
10
+ />
11
+ <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
12
+ </head>
13
+ <body>
14
+ <header>
15
+ <nav>
16
+ <ul>
17
+ <li><a href="{{ url_for('main.index') }}">Grimvault</a></li>
18
+ {% if current_user.is_authenticated %} {% if
19
+ current_user.is_admin %}
20
+ <li>
21
+ <a href="{{ url_for('admin.admin_dashboard') }}"
22
+ >Admin Dashboard</a
23
+ >
24
+ </li>
25
+ {% else %}
26
+ <li>
27
+ <a href="{{ url_for('files.dashboard') }}">Dashboard</a>
28
+ </li>
29
+ {% endif %}
30
+ <li><a href="#" id="logout">Logout</a></li>
31
+ {% else %}
32
+ <li><a href="{{ url_for('auth.login') }}">Login</a></li>
33
+ <li>
34
+ <a href="{{ url_for('auth.register') }}">Register</a>
35
+ </li>
36
+ {% endif %}
37
+ </ul>
38
+ </nav>
39
+ </header>
40
+
41
+ <main>
42
+ <div id="loading-indicator" class="loading" style="display: none">
43
+ Loading...
44
+ </div>
45
+ {% block content %}{% endblock %}
46
+ </main>
47
+
48
+ <script src="{{ url_for('static', filename='js/main.js') }}"></script>
49
+ {% block scripts %}{% endblock %}
50
+ </body>
51
+ </html>
templates/dashboard.html ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %} {% block title %}Dashboard - Grimvault{% endblock %}
2
+ {% block content %}
3
+ <div class="container">
4
+ <h1>My Files</h1>
5
+ <div class="storage-info">
6
+ <p>
7
+ Used storage: {{ (used_storage / 1024 / 1024) | round(2) }} MB / {{
8
+ (storage_limit / 1024 / 1024 / 1024) | round(2) }} GB
9
+ </p>
10
+ <progress
11
+ value="{{ used_storage }}"
12
+ max="{{ storage_limit }}"
13
+ ></progress>
14
+ </div>
15
+ <div class="file-actions">
16
+ <form id="upload-form" enctype="multipart/form-data">
17
+ <input type="file" id="file-input" name="file" required />
18
+ <button type="submit" class="btn btn-primary">Upload</button>
19
+ </form>
20
+ <button id="empty-vault" class="btn btn-danger">Empty Vault</button>
21
+ </div>
22
+ <ul id="file-list">
23
+ {% for file in files %}
24
+ <li>
25
+ <span>{{ file.filename }}</span>
26
+ <span>{{ file.size | filesizeformat }}</span>
27
+ <button
28
+ class="btn btn-secondary download-btn"
29
+ data-filename="{{ file.filename }}"
30
+ >
31
+ Download
32
+ </button>
33
+ <button
34
+ class="btn btn-danger delete-btn"
35
+ data-filename="{{ file.filename }}"
36
+ >
37
+ Delete
38
+ </button>
39
+ </li>
40
+ {% endfor %}
41
+ </ul>
42
+ </div>
43
+ {% endblock %} {% block scripts %}
44
+ <script>
45
+ document.addEventListener("DOMContentLoaded", () => {
46
+ const uploadForm = document.getElementById("upload-form");
47
+ const emptyVaultBtn = document.getElementById("empty-vault");
48
+ const fileList = document.getElementById("file-list");
49
+
50
+ uploadForm.addEventListener("submit", async (e) => {
51
+ e.preventDefault();
52
+ const formData = new FormData(e.target);
53
+ try {
54
+ const response = await axios.post(
55
+ '{{ url_for("files.upload") }}',
56
+ formData,
57
+ {
58
+ headers: { "Content-Type": "multipart/form-data" },
59
+ },
60
+ );
61
+ alert(response.data.message);
62
+ location.reload();
63
+ } catch (error) {
64
+ alert("Upload failed: " + error.response.data.error);
65
+ }
66
+ });
67
+
68
+ emptyVaultBtn.addEventListener("click", async () => {
69
+ const password = prompt(
70
+ "Enter your password to confirm emptying your vault:",
71
+ );
72
+ if (password) {
73
+ try {
74
+ const response = await axios.post(
75
+ '{{ url_for("files.empty") }}',
76
+ { password },
77
+ );
78
+ alert(response.data.message);
79
+ location.reload();
80
+ } catch (error) {
81
+ alert(
82
+ "Failed to empty vault: " + error.response.data.error,
83
+ );
84
+ }
85
+ }
86
+ });
87
+
88
+ fileList.addEventListener("click", async (e) => {
89
+ if (e.target.classList.contains("download-btn")) {
90
+ const filename = e.target.dataset.filename;
91
+ window.location.href = `{{ url_for("files.download", filename="") }}${filename}`;
92
+ } else if (e.target.classList.contains("delete-btn")) {
93
+ const filename = e.target.dataset.filename;
94
+ if (confirm(`Are you sure you want to delete ${filename}?`)) {
95
+ try {
96
+ const response = await axios.delete(
97
+ `{{ url_for("files.delete", filename="") }}${filename}`,
98
+ );
99
+ alert(response.data.message);
100
+ e.target.closest("li").remove();
101
+ } catch (error) {
102
+ alert(
103
+ "Failed to delete file: " +
104
+ error.response.data.error,
105
+ );
106
+ }
107
+ }
108
+ }
109
+ });
110
+ });
111
+ </script>
112
+ {% endblock %}
templates/index.html ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %} {% block title %}Welcome to Grimvault{% endblock %} {%
2
+ block content %}
3
+ <div class="container">
4
+ <h1>Welcome to Grimvault</h1>
5
+ <p>42hr encrypted cloud storage for your files.</p>
6
+ {% if current_user.is_authenticated %}
7
+ <p>Welcome back, {{ current_user.username }}!</p>
8
+ {% if current_user.is_admin %}
9
+ <a href="{{ url_for('admin.admin_dashboard') }}" class="btn btn-primary"
10
+ >Go to Admin Dashboard</a
11
+ >
12
+ {% else %}
13
+ <a href="{{ url_for('files.dashboard') }}" class="btn btn-primary"
14
+ >Go to My Files</a
15
+ >
16
+ {% endif %} {% else %}
17
+ <a href="{{ url_for('auth.login') }}" class="btn btn-primary">Login</a>
18
+ <a href="{{ url_for('auth.register') }}" class="btn btn-secondary"
19
+ >Register</a
20
+ >
21
+ {% endif %}
22
+ </div>
23
+ {% endblock %}
templates/login.html ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %} {% block title %}Login - Grimvault{% endblock %} {%
2
+ block content %}
3
+ <div class="container">
4
+ <h1>Login</h1>
5
+ <form id="login-form">
6
+ <div class="form-group">
7
+ <label for="username">Username:</label>
8
+ <input type="text" id="username" name="username" required />
9
+ </div>
10
+ <div class="form-group">
11
+ <label for="password">Password:</label>
12
+ <input type="password" id="password" name="password" required />
13
+ </div>
14
+ <button type="submit" class="btn btn-primary">Login</button>
15
+ </form>
16
+ <p id="login-message"></p>
17
+ </div>
18
+ {% endblock %} {% block scripts %}
19
+ <script>
20
+ document
21
+ .getElementById("login-form")
22
+ .addEventListener("submit", async (e) => {
23
+ e.preventDefault();
24
+ const formData = new FormData(e.target);
25
+ try {
26
+ const response = await axios.post(
27
+ '{{ url_for("auth.login") }}',
28
+ formData,
29
+ );
30
+ document.getElementById("login-message").textContent =
31
+ response.data.message;
32
+ window.location.href = response.data.redirect;
33
+ } catch (error) {
34
+ document.getElementById("login-message").textContent =
35
+ error.response.data.error;
36
+ }
37
+ });
38
+ </script>
39
+ {% endblock %}
templates/register.html ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %} {% block title %}Register - Grimvault{% endblock %} {%
2
+ block content %}
3
+ <div class="container">
4
+ <h1>Register</h1>
5
+ <form id="register-form">
6
+ <div class="form-group">
7
+ <label for="username">Username:</label>
8
+ <input type="text" id="username" name="username" required />
9
+ </div>
10
+ <div class="form-group">
11
+ <label for="password">Password:</label>
12
+ <input type="password" id="password" name="password" required />
13
+ </div>
14
+ <button type="submit" class="btn btn-primary">Register</button>
15
+ </form>
16
+ <p id="register-message"></p>
17
+ </div>
18
+ {% endblock %} {% block scripts %}
19
+ <script>
20
+ document
21
+ .getElementById("register-form")
22
+ .addEventListener("submit", async (e) => {
23
+ e.preventDefault();
24
+ const formData = new FormData(e.target);
25
+ try {
26
+ const response = await axios.post(
27
+ '{{ url_for("auth.register") }}',
28
+ formData,
29
+ );
30
+ document.getElementById("register-message").textContent =
31
+ response.data.message;
32
+ setTimeout(() => {
33
+ window.location.href = response.data.redirect;
34
+ }, 2000);
35
+ } catch (error) {
36
+ document.getElementById("register-message").textContent =
37
+ error.response.data.error;
38
+ }
39
+ });
40
+ </script>
41
+ {% endblock %}