Sergidev commited on
Commit
a6db6a6
·
verified ·
1 Parent(s): 3acdb01
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,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import logging
8
+
9
+ db = SQLAlchemy()
10
+ login_manager = LoginManager()
11
+ scheduler = BackgroundScheduler()
12
+
13
+ def create_app():
14
+ app = Flask(__name__,
15
+ template_folder=os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'templates'),
16
+ static_folder=os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'static'))
17
+ app.config.from_object(Config)
18
+
19
+ # Set up logging
20
+ logging.basicConfig(filename='grimvault.log', level=logging.INFO,
21
+ format='%(asctime)s:%(levelname)s:%(message)s')
22
+
23
+ db.init_app(app)
24
+ login_manager.init_app(app)
25
+ login_manager.login_view = 'auth.login'
26
+
27
+ from .routes import main, auth, files, admin
28
+ app.register_blueprint(main)
29
+ app.register_blueprint(auth)
30
+ app.register_blueprint(files)
31
+ app.register_blueprint(admin, url_prefix='/admin')
32
+
33
+ with app.app_context():
34
+ db.create_all()
35
+ create_admin_user()
36
+
37
+ scheduler.add_job(delete_inactive_users, 'interval', hours=1)
38
+ scheduler.start()
39
+
40
+ return app
41
+
42
+ def create_admin_user():
43
+ from .models import User
44
+ from .utils import create_user
45
+
46
+ admin_username = os.getenv('ADMIN_USERNAME')
47
+ admin_password = os.getenv('ADMIN_PASSWORD')
48
+
49
+ if admin_username and admin_password:
50
+ existing_admin = User.query.filter_by(username=admin_username).first()
51
+ if not existing_admin:
52
+ result = create_user(admin_username, admin_password)
53
+ if "successfully" in result:
54
+ admin_user = User.query.filter_by(username=admin_username).first()
55
+ admin_user.is_admin = True
56
+ admin_user.storage_limit = 0 # Admin has no storage
57
+ db.session.commit()
58
+ logging.info(f"Admin user '{admin_username}' created successfully.")
59
+ else:
60
+ logging.error(f"Failed to create admin user: {result}")
61
+ else:
62
+ logging.info(f"Admin user '{admin_username}' already exists.")
63
+ else:
64
+ logging.warning("Admin credentials not provided in environment variables.")
65
+
66
+ def delete_inactive_users():
67
+ from .models import User
68
+ from datetime import datetime, timedelta
69
+ with db.app.app_context():
70
+ inactive_threshold = datetime.utcnow() - timedelta(hours=42)
71
+ inactive_users = User.query.filter(User.last_active < inactive_threshold).all()
72
+ for user in inactive_users:
73
+ db.session.delete(user)
74
+ db.session.commit()
75
+ logging.info(f"Deleted {len(inactive_users)} inactive users.")
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,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, request, jsonify, send_file, abort, redirect, url_for, current_app
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
+ check_password_strength)
11
+ from flask_limiter import Limiter
12
+ from flask_limiter.util import get_remote_address
13
+ import io
14
+ import logging
15
+
16
+ main = Blueprint('main', __name__)
17
+ auth = Blueprint('auth', __name__)
18
+ files = Blueprint('files', __name__)
19
+ admin = Blueprint('admin', __name__)
20
+
21
+ limiter = Limiter(key_func=get_remote_address)
22
+
23
+ @main.route('/')
24
+ def index():
25
+ if current_user.is_authenticated:
26
+ current_user.update_last_active()
27
+ if current_user.is_admin:
28
+ return redirect(url_for('admin.admin_dashboard'))
29
+ return redirect(url_for('files.dashboard'))
30
+ return render_template('index.html')
31
+
32
+ @auth.route('/login', methods=['GET', 'POST'])
33
+ @limiter.limit("5 per minute")
34
+ def login():
35
+ if current_user.is_authenticated:
36
+ if current_user.is_admin:
37
+ return redirect(url_for('admin.admin_dashboard'))
38
+ return redirect(url_for('files.dashboard'))
39
+
40
+ if request.method == 'POST':
41
+ username = request.form.get('username')
42
+ password = request.form.get('password')
43
+
44
+ if is_rate_limited(username) or is_account_locked(username):
45
+ return jsonify({"error": "Too many attempts. Please try again later."}), 429
46
+
47
+ user = User.query.filter_by(username=username).first()
48
+ if user and verify_user(username, password):
49
+ if user.is_banned:
50
+ return jsonify({"error": "This account has been banned."}), 403
51
+ login_user(user)
52
+ user.update_last_active()
53
+ record_login_attempt(username, True)
54
+ if user.is_admin:
55
+ logging.info(f"Admin user '{username}' logged in successfully.")
56
+ return jsonify({"message": "Login successful", "redirect": url_for('admin.admin_dashboard')}), 200
57
+ logging.info(f"User '{username}' logged in successfully.")
58
+ return jsonify({"message": "Login successful", "redirect": url_for('files.dashboard')}), 200
59
+ else:
60
+ record_login_attempt(username, False)
61
+ logging.warning(f"Failed login attempt for user '{username}'.")
62
+ return jsonify({"error": "Invalid username or password"}), 401
63
+
64
+ return render_template('login.html')
65
+
66
+ @auth.route('/register', methods=['GET', 'POST'])
67
+ def register():
68
+ if request.method == 'POST':
69
+ username = request.form.get('username')
70
+ password = request.form.get('password')
71
+ confirm_password = request.form.get('confirm_password')
72
+
73
+ if password != confirm_password:
74
+ return jsonify({"error": "Passwords do not match"}), 400
75
+
76
+ password_strength = check_password_strength(password)
77
+ if password_strength != "strong":
78
+ return jsonify({"error": f"Password is not strong enough. Current strength: {password_strength}"}), 400
79
+
80
+ result = create_user(username, password)
81
+ if "successfully" in result:
82
+ logging.info(f"New user '{username}' registered successfully.")
83
+ return jsonify({"message": result, "redirect": url_for('auth.login')}), 201
84
+ else:
85
+ logging.warning(f"Failed registration attempt for username '{username}': {result}")
86
+ return jsonify({"error": result}), 400
87
+
88
+ return render_template('register.html')
89
+
90
+ @auth.route('/logout')
91
+ @login_required
92
+ def logout():
93
+ logging.info(f"User '{current_user.username}' logged out.")
94
+ logout_user()
95
+ return redirect(url_for('main.index'))
96
+
97
+ @files.route('/dashboard')
98
+ @login_required
99
+ def dashboard():
100
+ if current_user.is_admin:
101
+ return redirect(url_for('admin.admin_dashboard'))
102
+ current_user.update_last_active()
103
+ user_files = get_user_files(current_user.username)
104
+ used_storage = current_user.get_used_storage()
105
+ return render_template('dashboard.html', files=user_files, used_storage=used_storage, storage_limit=current_user.storage_limit)
106
+
107
+ @files.route('/upload', methods=['POST'])
108
+ @login_required
109
+ def upload():
110
+ current_user.update_last_active()
111
+ if current_user.is_admin:
112
+ return jsonify({"error": "Admins cannot upload files"}), 403
113
+ if 'file' not in request.files:
114
+ return jsonify({"error": "No file part"}), 400
115
+ file = request.files['file']
116
+ if file.filename == '':
117
+ return jsonify({"error": "No selected file"}), 400
118
+ if file:
119
+ filename = secure_filename(file.filename)
120
+ try:
121
+ result = upload_file(current_user.username, filename, file.read())
122
+ logging.info(f"User '{current_user.username}' uploaded file '{filename}'.")
123
+ return jsonify({"message": result}), 200
124
+ except Exception as e:
125
+ logging.error(f"Error uploading file for user '{current_user.username}': {str(e)}")
126
+ return jsonify({"error": "An error occurred while uploading the file. Please try again."}), 500
127
+
128
+ @files.route('/download/<filename>')
129
+ @login_required
130
+ def download(filename):
131
+ current_user.update_last_active()
132
+ if current_user.is_admin:
133
+ return jsonify({"error": "Admins cannot download files"}), 403
134
+ file_content = download_file(current_user.username, filename)
135
+ if file_content:
136
+ logging.info(f"User '{current_user.username}' downloaded file '{filename}'.")
137
+ return send_file(
138
+ io.BytesIO(file_content),
139
+ mimetype='application/octet-stream',
140
+ as_attachment=True,
141
+ download_name=filename
142
+ )
143
+ else:
144
+ logging.warning(f"File '{filename}' not found for user '{current_user.username}'.")
145
+ return jsonify({"error": "File not found"}), 404
146
+
147
+ @files.route('/delete/<filename>', methods=['DELETE'])
148
+ @login_required
149
+ def delete(filename):
150
+ current_user.update_last_active()
151
+ if current_user.is_admin:
152
+ return jsonify({"error": "Admins cannot delete files"}), 403
153
+ result = delete_file(current_user.username, filename)
154
+ logging.info(f"User '{current_user.username}' deleted file '{filename}'.")
155
+ return jsonify({"message": result}), 200
156
+
157
+ @files.route('/empty', methods=['POST'])
158
+ @login_required
159
+ def empty():
160
+ current_user.update_last_active()
161
+ if current_user.is_admin:
162
+ return jsonify({"error": "Admins cannot empty vault"}), 403
163
+ password = request.form.get('password')
164
+ if verify_user(current_user.username, password):
165
+ result = empty_vault(current_user.username)
166
+ logging.info(f"User '{current_user.username}' emptied their vault.")
167
+ return jsonify({"message": result}), 200
168
+ else:
169
+ logging.warning(f"Failed attempt to empty vault for user '{current_user.username}'.")
170
+ return jsonify({"error": "Invalid password"}), 401
171
+
172
+ @admin.route('/dashboard')
173
+ @login_required
174
+ def admin_dashboard():
175
+ if not current_user.is_admin:
176
+ abort(403)
177
+ current_user.update_last_active()
178
+ accounts = get_all_accounts()
179
+ return render_template('admindash.html', accounts=accounts)
180
+
181
+ @admin.route('/update_storage', methods=['POST'])
182
+ @login_required
183
+ def update_storage():
184
+ if not current_user.is_admin:
185
+ return jsonify({"error": "Access denied"}), 403
186
+ current_user.update_last_active()
187
+ username = request.json.get('username')
188
+ new_limit = request.json.get('new_limit')
189
+ try:
190
+ new_limit = int(float(new_limit) * 1024 * 1024 * 1024) # Convert GB to bytes
191
+ result = update_storage_limit(username, new_limit)
192
+ logging.info(f"Admin '{current_user.username}' updated storage limit for user '{username}' to {new_limit} bytes.")
193
+ return jsonify({"message": result}), 200
194
+ except ValueError:
195
+ logging.error(f"Invalid storage limit value provided by admin '{current_user.username}' for user '{username}'.")
196
+ return jsonify({"error": "Invalid storage limit value"}), 400
197
+
198
+ @admin.route('/ban_user', methods=['POST'])
199
+ @login_required
200
+ def ban_user_route():
201
+ if not current_user.is_admin:
202
+ return jsonify({"error": "Access denied"}), 403
203
+ current_user.update_last_active()
204
+ username = request.json.get('username')
205
+ ban_status = request.json.get('ban_status')
206
+ result = ban_user(username, ban_status)
207
+ action = "banned" if ban_status else "unbanned"
208
+ logging.info(f"Admin '{current_user.username}' {action} user '{username}'.")
209
+ return jsonify({"message": result}), 200
210
+
211
+ @admin.route('/delete/<username>', methods=['DELETE'])
212
+ @login_required
213
+ def admin_delete_account(username):
214
+ if not current_user.is_admin:
215
+ return jsonify({"error": "Access denied"}), 403
216
+ current_user.update_last_active()
217
+ result = delete_account(username)
218
+ logging.info(f"Admin '{current_user.username}' deleted account for user '{username}'.")
219
+ return jsonify({"message": result}), 200
app/utils.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import re
16
+
17
+ load_dotenv()
18
+
19
+ # Initialize global variables
20
+ MODEL_NAME = os.getenv('SECRET_M')
21
+ HF_TOKEN = os.getenv('HF_TOKEN')
22
+
23
+ tokenizer = None
24
+ model = None
25
+
26
+ # Initialize Argon2 hasher and Fernet cipher
27
+ ph = PasswordHasher()
28
+ cipher_key = Fernet.generate_key()
29
+ cipher = Fernet(cipher_key)
30
+
31
+ def get_embedding(text):
32
+ global tokenizer, model
33
+
34
+ if tokenizer is None or model is None:
35
+ try:
36
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, use_auth_token=HF_TOKEN)
37
+ model = AutoModel.from_pretrained(MODEL_NAME, torch_dtype=torch.float16, use_auth_token=HF_TOKEN)
38
+
39
+ if tokenizer.pad_token is None:
40
+ tokenizer.pad_token = tokenizer.eos_token
41
+
42
+ model.resize_token_embeddings(len(tokenizer))
43
+ except (requests.exceptions.RequestException, OSError) as e:
44
+ print(f"Error loading model: {str(e)}")
45
+ return None
46
+
47
+ inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True, max_length=512)
48
+ with torch.no_grad():
49
+ outputs = model(**inputs)
50
+ return outputs.last_hidden_state.mean(dim=1).squeeze().numpy()
51
+
52
+ def hash_embedding(embedding, salt):
53
+ salted_embedding = np.concatenate([embedding, np.frombuffer(salt, dtype=np.float32)])
54
+ return hashlib.sha256(salted_embedding.tobytes()).hexdigest()
55
+
56
+ def create_user(username, password):
57
+ if User.query.filter_by(username=username).first():
58
+ return "Username already exists."
59
+
60
+ salt = secrets.token_bytes(16)
61
+ embedding = get_embedding(password)
62
+ if embedding is None:
63
+ return "Error creating user. Please try again later."
64
+ embedding_hash = hash_embedding(embedding, salt)
65
+
66
+ new_user = User(username=username, salt=salt.hex(), embedding_hash=embedding_hash)
67
+ new_user.set_password(password)
68
+
69
+ db.session.add(new_user)
70
+ db.session.commit()
71
+
72
+ return "User created successfully."
73
+
74
+ def verify_user(username, password):
75
+ user = User.query.filter_by(username=username).first()
76
+ if not user:
77
+ return False
78
+
79
+ if not user.check_password(password):
80
+ return False
81
+
82
+ embedding = get_embedding(password)
83
+ if embedding is None:
84
+ return False
85
+ embedding_hash = hash_embedding(embedding, bytes.fromhex(user.salt))
86
+ return embedding_hash == user.embedding_hash
87
+
88
+ def get_user_files(username):
89
+ user = User.query.filter_by(username=username).first()
90
+ if not user:
91
+ return []
92
+ return [{"filename": file.filename, "size": file.size} for file in user.files]
93
+
94
+ def upload_file(username, filename, content):
95
+ user = User.query.filter_by(username=username).first()
96
+ if not user:
97
+ return "User not found."
98
+
99
+ if user.get_used_storage() + len(content) > user.storage_limit:
100
+ return "Storage limit exceeded."
101
+
102
+ existing_file = File.query.filter_by(user_id=user.id, filename=filename).first()
103
+ if existing_file:
104
+ return f"File {filename} already exists."
105
+
106
+ encrypted_content = cipher.encrypt(content)
107
+ new_file = File(filename=filename, content=encrypted_content, size=len(content), user_id=user.id)
108
+ db.session.add(new_file)
109
+ db.session.commit()
110
+
111
+ return f"File {filename} uploaded successfully."
112
+
113
+ def download_file(username, filename):
114
+ user = User.query.filter_by(username=username).first()
115
+ if not user:
116
+ return None
117
+
118
+ file = File.query.filter_by(user_id=user.id, filename=filename).first()
119
+ if not file:
120
+ return None
121
+
122
+ return cipher.decrypt(file.content)
123
+
124
+ def delete_file(username, filename):
125
+ user = User.query.filter_by(username=username).first()
126
+ if not user:
127
+ return "User not found."
128
+
129
+ file = File.query.filter_by(user_id=user.id, filename=filename).first()
130
+ if not file:
131
+ return f"File {filename} not found."
132
+
133
+ db.session.delete(file)
134
+ db.session.commit()
135
+ return f"File {filename} deleted successfully."
136
+
137
+ def empty_vault(username):
138
+ user = User.query.filter_by(username=username).first()
139
+ if not user:
140
+ return "User not found."
141
+
142
+ File.query.filter_by(user_id=user.id).delete()
143
+ db.session.commit()
144
+ return "All files in your vault have been deleted."
145
+
146
+ def is_admin(username):
147
+ user = User.query.filter_by(username=username).first()
148
+ return user and user.is_admin
149
+
150
+ def get_all_accounts():
151
+ 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()]
152
+
153
+ def delete_account(username):
154
+ if username == os.getenv('ADMIN_USERNAME'):
155
+ return "Cannot delete admin account."
156
+
157
+ user = User.query.filter_by(username=username).first()
158
+ if not user:
159
+ return "User not found."
160
+
161
+ File.query.filter_by(user_id=user.id).delete()
162
+ db.session.delete(user)
163
+ db.session.commit()
164
+ return f"Account {username} and all associated files have been deleted."
165
+
166
+ def update_storage_limit(username, new_limit):
167
+ user = User.query.filter_by(username=username).first()
168
+ if not user:
169
+ return "User not found."
170
+
171
+ user.storage_limit = new_limit
172
+ db.session.commit()
173
+ return f"Storage limit for {username} updated to {new_limit / (1024 * 1024 * 1024):.2f} GB."
174
+
175
+ def ban_user(username, ban_status):
176
+ user = User.query.filter_by(username=username).first()
177
+ if not user:
178
+ return "User not found."
179
+
180
+ user.is_banned = ban_status
181
+ db.session.commit()
182
+ action = "banned" if ban_status else "unbanned"
183
+ return f"User {username} has been {action}."
184
+
185
+ # Rate limiting
186
+ RATE_LIMIT = 5 # maximum number of requests per minute
187
+ rate_limit_dict = {}
188
+
189
+ def is_rate_limited(username):
190
+ current_time = time.time()
191
+ if username in rate_limit_dict:
192
+ last_request_time, count = rate_limit_dict[username]
193
+ if current_time - last_request_time < 60: # within 1 minute
194
+ if count >= RATE_LIMIT:
195
+ return True
196
+ rate_limit_dict[username] = (last_request_time, count + 1)
197
+ else:
198
+ rate_limit_dict[username] = (current_time, 1)
199
+ else:
200
+ rate_limit_dict[username] = (current_time, 1)
201
+ return False
202
+
203
+ # Account lockout
204
+ MAX_LOGIN_ATTEMPTS = 5
205
+ LOCKOUT_TIME = 300 # 5 minutes
206
+ lockout_dict = {}
207
+
208
+ def is_account_locked(username):
209
+ if username in lockout_dict:
210
+ attempts, lockout_time = lockout_dict[username]
211
+ if attempts >= MAX_LOGIN_ATTEMPTS:
212
+ if time.time() - lockout_time < LOCKOUT_TIME:
213
+ return True
214
+ else:
215
+ del lockout_dict[username]
216
+ return False
217
+
218
+ def record_login_attempt(username, success):
219
+ if username not in lockout_dict:
220
+ lockout_dict[username] = [0, 0]
221
+
222
+ if success:
223
+ del lockout_dict[username]
224
+ else:
225
+ lockout_dict[username][0] += 1
226
+ lockout_dict[username][1] = time.time()
227
+
228
+ def check_password_strength(password):
229
+ # Check password length
230
+ if len(password) < 8:
231
+ return "weak"
232
+
233
+ # Check for uppercase, lowercase, digit, and special character
234
+ if not re.search(r'[A-Z]', password):
235
+ return "medium"
236
+ if not re.search(r'[a-z]', password):
237
+ return "medium"
238
+ if not re.search(r'\d', password):
239
+ return "medium"
240
+ if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
241
+ return "medium"
242
+
243
+ return "strong"
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,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ .warning {
232
+ color: var(--danger-color);
233
+ margin-bottom: 10px;
234
+ }
235
+
236
+ #password-strength {
237
+ height: 5px;
238
+ margin-top: 5px;
239
+ transition: all 0.3s ease;
240
+ }
241
+
242
+ #password-strength.weak {
243
+ background-color: #ff4136;
244
+ width: 33.33%;
245
+ }
246
+
247
+ #password-strength.medium {
248
+ background-color: #ffdc00;
249
+ width: 66.66%;
250
+ }
251
+
252
+ #password-strength.strong {
253
+ background-color: #2ecc40;
254
+ width: 100%;
255
+ }
256
+
257
+ @media (max-width: 768px) {
258
+ .file-actions {
259
+ flex-direction: column;
260
+ }
261
+
262
+ .file-actions > * {
263
+ margin-bottom: 10px;
264
+ width: 100%;
265
+ }
266
+
267
+ #file-list li {
268
+ flex-direction: column;
269
+ align-items: flex-start;
270
+ }
271
+
272
+ #file-list li button {
273
+ margin-top: 10px;
274
+ }
275
+
276
+ table {
277
+ font-size: 0.9rem;
278
+ }
279
+
280
+ th,
281
+ td {
282
+ padding: 8px;
283
+ }
284
+ }
285
+
286
+ @media (max-width: 480px) {
287
+ .container {
288
+ padding: 10px;
289
+ }
290
+
291
+ h1 {
292
+ font-size: 1.5rem;
293
+ }
294
+
295
+ .btn {
296
+ font-size: 0.9rem;
297
+ padding: 8px 16px;
298
+ }
299
+
300
+ table {
301
+ font-size: 0.8rem;
302
+ }
303
+
304
+ th,
305
+ td {
306
+ padding: 6px;
307
+ }
308
+ }
static/js/admindash.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ const userAccounts = document.getElementById("user-accounts");
3
+
4
+ userAccounts.addEventListener("click", async (e) => {
5
+ const username = e.target.dataset.username;
6
+
7
+ if (e.target.classList.contains("update-storage")) {
8
+ const newLimit = prompt("Enter new storage limit in GB:");
9
+ if (newLimit) {
10
+ try {
11
+ const response = await axios.post("/admin/update_storage", {
12
+ username: username,
13
+ new_limit: newLimit,
14
+ });
15
+ alert(response.data.message);
16
+ location.reload();
17
+ } catch (error) {
18
+ alert(
19
+ "Failed to update storage: " +
20
+ (error.response?.data?.error || error.message),
21
+ );
22
+ }
23
+ }
24
+ } else if (e.target.classList.contains("toggle-ban")) {
25
+ const currentStatus = e.target.dataset.banned === "true";
26
+ const newStatus = !currentStatus;
27
+ try {
28
+ const response = await axios.post("/admin/ban_user", {
29
+ username: username,
30
+ ban_status: newStatus,
31
+ });
32
+ alert(response.data.message);
33
+ location.reload();
34
+ } catch (error) {
35
+ alert(
36
+ "Failed to update ban status: " +
37
+ (error.response?.data?.error || error.message),
38
+ );
39
+ }
40
+ } else if (e.target.classList.contains("delete-account")) {
41
+ if (
42
+ confirm(`Are you sure you want to delete the account for ${username}?`)
43
+ ) {
44
+ try {
45
+ const response = await axios.delete(`/admin/delete/${username}`);
46
+ alert(response.data.message);
47
+ location.reload();
48
+ } catch (error) {
49
+ alert(
50
+ "Failed to delete account: " +
51
+ (error.response?.data?.error || error.message),
52
+ );
53
+ }
54
+ }
55
+ }
56
+ });
57
+ });
static/js/dashboard.js ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ const uploadForm = document.getElementById("upload-form");
3
+ const emptyVaultBtn = document.getElementById("empty-vault");
4
+ const fileList = document.getElementById("file-list");
5
+
6
+ uploadForm.addEventListener("submit", async (e) => {
7
+ e.preventDefault();
8
+ const formData = new FormData(e.target);
9
+ try {
10
+ const response = await axios.post("/upload", formData, {
11
+ headers: { "Content-Type": "multipart/form-data" },
12
+ });
13
+ alert(response.data.message);
14
+ location.reload();
15
+ } catch (error) {
16
+ alert("Upload failed: " + (error.response?.data?.error || error.message));
17
+ }
18
+ });
19
+
20
+ emptyVaultBtn.addEventListener("click", async () => {
21
+ const confirmed = confirm(
22
+ "Are you sure you want to empty your vault? This action cannot be undone.",
23
+ );
24
+ if (confirmed) {
25
+ const password = prompt(
26
+ "Enter your password to confirm emptying your vault:",
27
+ );
28
+ if (password) {
29
+ try {
30
+ const response = await axios.post("/empty", { password });
31
+ alert(response.data.message);
32
+ location.reload();
33
+ } catch (error) {
34
+ alert(
35
+ "Failed to empty vault: " +
36
+ (error.response?.data?.error || error.message),
37
+ );
38
+ }
39
+ }
40
+ }
41
+ });
42
+
43
+ fileList.addEventListener("click", async (e) => {
44
+ if (e.target.classList.contains("download-btn")) {
45
+ const filename = e.target.dataset.filename;
46
+ window.location.href = `/download/${filename}`;
47
+ } else if (e.target.classList.contains("delete-btn")) {
48
+ const filename = e.target.dataset.filename;
49
+ if (confirm(`Are you sure you want to delete ${filename}?`)) {
50
+ try {
51
+ const response = await axios.delete(`/delete/${filename}`);
52
+ alert(response.data.message);
53
+ e.target.closest("li").remove();
54
+ } catch (error) {
55
+ alert(
56
+ "Failed to delete file: " +
57
+ (error.response?.data?.error || error.message),
58
+ );
59
+ }
60
+ }
61
+ }
62
+ });
63
+ });
static/js/login.js ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ const form = document.getElementById("login-form");
3
+ const loginMessage = document.getElementById("login-message");
4
+
5
+ form.addEventListener("submit", async (e) => {
6
+ e.preventDefault();
7
+ const formData = new FormData(form);
8
+ try {
9
+ const response = await axios.post("/login", formData);
10
+ loginMessage.textContent = response.data.message;
11
+ loginMessage.style.color = "green";
12
+ window.location.href = response.data.redirect;
13
+ } catch (error) {
14
+ loginMessage.textContent = error.response.data.error;
15
+ loginMessage.style.color = "red";
16
+ }
17
+ });
18
+ });
static/js/register.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ const form = document.getElementById("register-form");
3
+ const passwordInput = document.getElementById("password");
4
+ const confirmPasswordInput = document.getElementById("confirm-password");
5
+ const passwordStrength = document.getElementById("password-strength");
6
+ const submitButton = form.querySelector('button[type="submit"]');
7
+ const registerMessage = document.getElementById("register-message");
8
+
9
+ function updatePasswordStrength() {
10
+ const password = passwordInput.value;
11
+ let strength = "weak";
12
+
13
+ if (
14
+ password.length >= 8 &&
15
+ /[A-Z]/.test(password) &&
16
+ /[a-z]/.test(password) &&
17
+ /\d/.test(password) &&
18
+ /[!@#$%^&*(),.?":{}|<>]/.test(password)
19
+ ) {
20
+ strength = "strong";
21
+ } else if (password.length >= 8) {
22
+ strength = "medium";
23
+ }
24
+
25
+ passwordStrength.className = strength;
26
+ submitButton.disabled =
27
+ strength !== "strong" ||
28
+ passwordInput.value !== confirmPasswordInput.value;
29
+ }
30
+
31
+ passwordInput.addEventListener("input", updatePasswordStrength);
32
+ confirmPasswordInput.addEventListener("input", updatePasswordStrength);
33
+
34
+ form.addEventListener("submit", async (e) => {
35
+ e.preventDefault();
36
+ const formData = new FormData(form);
37
+ try {
38
+ const response = await axios.post("/register", formData);
39
+ registerMessage.textContent = response.data.message;
40
+ registerMessage.style.color = "green";
41
+ setTimeout(() => {
42
+ window.location.href = response.data.redirect;
43
+ }, 2000);
44
+ } catch (error) {
45
+ registerMessage.textContent = error.response.data.error;
46
+ registerMessage.style.color = "red";
47
+ }
48
+ });
49
+ });
templates/admindash.html ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <a
57
+ href="{{ url_for('auth.logout') }}"
58
+ class="btn btn-secondary"
59
+ id="logout-btn"
60
+ >Logout</a
61
+ >
62
+ </div>
63
+ {% endblock %} {% block scripts %}
64
+ <script src="{{ url_for('static', filename='js/admindash.js') }}"></script>
65
+ {% endblock %}
templates/base.html ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 %} {% else %}
30
+ <li><a href="{{ url_for('auth.login') }}">Login</a></li>
31
+ <li>
32
+ <a href="{{ url_for('auth.register') }}">Register</a>
33
+ </li>
34
+ {% endif %}
35
+ </ul>
36
+ </nav>
37
+ </header>
38
+
39
+ <main>
40
+ <div id="loading-indicator" class="loading" style="display: none">
41
+ Loading...
42
+ </div>
43
+ {% block content %}{% endblock %}
44
+ </main>
45
+
46
+ <script src="{{ url_for('static', filename='js/main.js') }}"></script>
47
+ {% block scripts %}{% endblock %}
48
+ </body>
49
+ </html>
templates/dashboard.html ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <a
22
+ href="{{ url_for('auth.logout') }}"
23
+ class="btn btn-secondary"
24
+ id="logout-btn"
25
+ >Logout</a
26
+ >
27
+ </div>
28
+ <ul id="file-list">
29
+ {% for file in files %}
30
+ <li>
31
+ <span>{{ file.filename }}</span>
32
+ <span>{{ file.size | filesizeformat }}</span>
33
+ <button
34
+ class="btn btn-secondary download-btn"
35
+ data-filename="{{ file.filename }}"
36
+ >
37
+ Download
38
+ </button>
39
+ <button
40
+ class="btn btn-danger delete-btn"
41
+ data-filename="{{ file.filename }}"
42
+ >
43
+ Delete
44
+ </button>
45
+ </li>
46
+ {% endfor %}
47
+ </ul>
48
+ </div>
49
+ {% endblock %} {% block scripts %}
50
+ <script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
51
+ {% 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,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 src="{{ url_for('static', filename='js/login.js') }}"></script>
20
+ {% endblock %}
templates/register.html ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 id="password-strength"></div>
14
+ </div>
15
+ <div class="form-group">
16
+ <label for="confirm-password">Confirm Password:</label>
17
+ <input
18
+ type="password"
19
+ id="confirm-password"
20
+ name="confirm_password"
21
+ required
22
+ />
23
+ </div>
24
+ <p class="warning">
25
+ Warning: There is no password recovery option. Please remember your
26
+ password.
27
+ </p>
28
+ <button type="submit" class="btn btn-primary" disabled>Register</button>
29
+ </form>
30
+ <p id="register-message"></p>
31
+ </div>
32
+ {% endblock %} {% block scripts %}
33
+ <script src="{{ url_for('static', filename='js/register.js') }}"></script>
34
+ {% endblock %}