Compare commits
No commits in common. "main" and "main" have entirely different histories.
33
README.md
33
README.md
@ -1,2 +1,31 @@
|
||||
this repo is deprecated
|
||||
https://github.com/spitkov/sxbin
|
||||
## Text sharing, File sharing, link shortener.
|
||||
### frontend and backend, easily configurable (thru editing the html files and the .py)
|
||||
--------------dasdasdasdasdasdadasd--
|
||||
TODO
|
||||
<iframe src="https://office.bence.lol/kanban/#/2/kanban/view/hx6RTcpN0pR7hc1HHkMzG4awMoMdHjR2zbHjG7Xh+wU/embed/"> </iframe>
|
||||
1. Introduction
|
||||
</br>
|
||||
</br>
|
||||
</br>
|
||||
As said above, this is a **simple & private** backend (app.py) and frontend (in /templates), which has these features:
|
||||
- File Sharing (get a short link to share files, and when the link is opened it will show some information)
|
||||
- Text Sharing (kind of a pastebin, it will show the text and when the text was made, again in a short link)
|
||||
- URL Shortener (when hosting this, it will short an url, and give you a link, and when you open it, it will automatically redirect you to that website)
|
||||
|
||||
2. This is 100% **anonymous**, no one can see who created the links, etc.
|
||||
|
||||
3. The UI is simple.
|
||||
|
||||
4. This is very easily self-hostable </br> </br>
|
||||
Rookie Mistake: when you self-host it, you can open it in localhost:7123, when making a file/paste/shortener don't share that localhost link, localhost is only accessible on YOUR network. - instead, u can host it with your domain.
|
||||
</br>
|
||||
</br>
|
||||
5. This project is **COMPLETLY** open source.
|
||||
</br>
|
||||
</br>
|
||||
6. You might need to install some pip packages, if you dont have them installed already download this repo and go into the folder and execute ```pip install -r requirements.txt```
|
||||
</br>
|
||||
</br>
|
||||
7. Demo:
|
||||
<video src="https://cgcristi.xyz/demo.mp4" controls></video>
|
||||
|
||||
|
268
app.py
268
app.py
@ -233,22 +233,11 @@ def serve_user_page(username, filename=None):
|
||||
@app.route('/<vanity>/<password>', methods=['GET', 'POST'])
|
||||
@app.route('/<vanity>/download', methods=['GET', 'POST'])
|
||||
@app.route('/<vanity>/download/<password>', methods=['GET', 'POST'])
|
||||
@app.route('/<vanity>/raw', methods=['GET', 'POST'])
|
||||
@app.route('/<vanity>/raw/<password>', methods=['GET', 'POST'])
|
||||
def redirect_vanity(vanity, password=None):
|
||||
app.logger.info(f"Accessing redirect_vanity: vanity={vanity}, password={password}")
|
||||
app.logger.info(f"Request path: {request.path}")
|
||||
app.logger.info(f"Request method: {request.method}")
|
||||
app.logger.info(f"Request URL: {request.url}")
|
||||
app.logger.info(f"Request endpoint: {request.endpoint}")
|
||||
app.logger.info(f"Request view args: {request.view_args}")
|
||||
|
||||
app.logger.info(f"Accessing vanity: {vanity}, password: {password}")
|
||||
db = get_db()
|
||||
cursor = db.cursor()
|
||||
|
||||
is_download = 'download' in request.path
|
||||
is_raw = 'raw' in request.path
|
||||
|
||||
# First, try to find the content with the full vanity (including extension)
|
||||
cursor.execute("SELECT content.*, users.username FROM content LEFT JOIN users ON content.user_id = users.id WHERE content.vanity = ?", (vanity,))
|
||||
content = cursor.fetchone()
|
||||
@ -263,11 +252,6 @@ def redirect_vanity(vanity, password=None):
|
||||
content_type, content_data, created_at, user_id, is_private, stored_password, username = content[1], content[2], content[3], content[4], content[5], content[6], content[7]
|
||||
app.logger.info(f"Content found: type={content_type}, data={content_data}, is_private={is_private}")
|
||||
|
||||
try:
|
||||
created_at = datetime.strptime(created_at, '%Y-%m-%d %H:%M:%S.%f')
|
||||
except ValueError:
|
||||
created_at = datetime.strptime(created_at, '%Y-%m-%d %H:%M:%S')
|
||||
|
||||
if is_private and stored_password:
|
||||
if password:
|
||||
if password != stored_password:
|
||||
@ -275,58 +259,25 @@ def redirect_vanity(vanity, password=None):
|
||||
elif request.method == 'POST':
|
||||
entered_password = request.form.get('password')
|
||||
if entered_password != stored_password:
|
||||
return render_template('password_prompt.html', vanity=vanity, error="Incorrect password", content_type=content_type)
|
||||
password = entered_password # Set the password for use in raw_url
|
||||
return render_template('password_prompt.html', vanity=vanity, error="Incorrect password")
|
||||
else:
|
||||
return render_template('password_prompt.html', vanity=vanity, error=None, content_type=content_type)
|
||||
return render_template('password_prompt.html', vanity=vanity, error=None)
|
||||
|
||||
if content_type == 'url':
|
||||
return render_template('og_shorturl.html', long_url=content_data, username=username, created_at=created_at, vanity=vanity, is_private=is_private)
|
||||
return redirect(content_data)
|
||||
elif content_type == 'file':
|
||||
file_path = os.path.join(app.config['UPLOAD_FOLDER'], content_data)
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], content_data)
|
||||
app.logger.info(f"Attempting to serve file: {file_path}")
|
||||
if os.path.exists(file_path):
|
||||
file_size = os.path.getsize(file_path)
|
||||
file_extension = os.path.splitext(content_data)[1].lower()
|
||||
is_embeddable = file_extension in ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.pdf']
|
||||
file_url = f"{request.scheme}://{request.host}/{vanity}"
|
||||
raw_url = f"{file_url}/raw"
|
||||
|
||||
if is_private and password:
|
||||
raw_url += f'/{password}'
|
||||
|
||||
if is_download:
|
||||
if 'download' in request.path:
|
||||
return send_file(file_path, as_attachment=True)
|
||||
elif is_raw:
|
||||
return send_file(file_path)
|
||||
else:
|
||||
return render_template('file_info.html',
|
||||
filename=content_data,
|
||||
file_size=file_size,
|
||||
username=username,
|
||||
created_at=created_at,
|
||||
is_embeddable=is_embeddable,
|
||||
file_url=file_url,
|
||||
raw_url=raw_url,
|
||||
vanity=vanity,
|
||||
user_id=user_id,
|
||||
is_private=is_private,
|
||||
password=password)
|
||||
return send_file(file_path)
|
||||
else:
|
||||
app.logger.error(f"File not found: {file_path}")
|
||||
return "File not found", 404
|
||||
elif content_type == 'pastebin':
|
||||
try:
|
||||
lexer = guess_lexer(content_data)
|
||||
except ClassNotFound:
|
||||
lexer = get_lexer_by_name('text')
|
||||
formatter = HtmlFormatter(style='monokai', linenos=True, cssclass="source")
|
||||
highlighted_content = highlight(content_data, lexer, formatter)
|
||||
css = formatter.get_style_defs('.source')
|
||||
return render_template('pastebin.html',
|
||||
content={'data': content_data, 'user_id': user_id, 'username': username},
|
||||
highlighted_content=highlighted_content,
|
||||
css=css,
|
||||
raw_content=content_data,
|
||||
created_at=created_at,
|
||||
vanity=vanity,
|
||||
is_private=is_private)
|
||||
return render_pastebin(content_data, created_at, user_id, username, vanity, is_private)
|
||||
|
||||
app.logger.error(f"Content not found for vanity: {vanity}")
|
||||
return "Not found", 404
|
||||
@ -379,6 +330,17 @@ def raw_vanity(vanity, password=None):
|
||||
|
||||
return content_data, 200, {'Content-Type': 'text/plain; charset=utf-8'}
|
||||
return 'Not Found', 404
|
||||
# Replace the LoginForm and RegistrationForm classes with simple classes
|
||||
class LoginForm:
|
||||
def __init__(self, username, password, remember):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.remember = remember
|
||||
|
||||
class RegistrationForm:
|
||||
def __init__(self, username, password):
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
@ -386,15 +348,16 @@ def login():
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
remember = 'remember' in request.form
|
||||
form = LoginForm(username, password, remember)
|
||||
db = get_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
|
||||
cursor.execute("SELECT * FROM users WHERE username = ?", (form.username,))
|
||||
user = cursor.fetchone()
|
||||
if user and User.verify_password(user[2], password):
|
||||
if user and User.verify_password(user[2], form.password):
|
||||
user_obj = User(user[0], user[1], user[2], user[3])
|
||||
login_user(user_obj, remember=remember)
|
||||
return jsonify({'success': True, 'redirect': url_for('user_files', username=username)})
|
||||
return jsonify({'success': False, 'error': 'Invalid username or password'})
|
||||
login_user(user_obj, remember=form.remember)
|
||||
return redirect(url_for('user_files', username=form.username))
|
||||
return "Invalid username or password"
|
||||
return render_template('login.html')
|
||||
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
@ -402,26 +365,23 @@ def register():
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
|
||||
if len(password) < 5 or not any(c.isupper() for c in password):
|
||||
return jsonify({'success': False, 'error': 'Password does not meet requirements'})
|
||||
|
||||
api_key = User.generate_api_key()
|
||||
api_key = User.generate_api_key() # Generate API key
|
||||
db = get_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
|
||||
if cursor.fetchone():
|
||||
return jsonify({'success': False, 'error': 'Username already exists'})
|
||||
return "Username already exists"
|
||||
hashed_password = User.hash_password(password)
|
||||
cursor.execute("INSERT INTO users (username, password_hash, api_key) VALUES (?, ?, ?)",
|
||||
(username, hashed_password, api_key))
|
||||
db.commit()
|
||||
|
||||
# Create user directory
|
||||
user_folder = os.path.join(app.config['UPLOAD_FOLDER'], username)
|
||||
if not os.path.exists(user_folder):
|
||||
os.makedirs(user_folder)
|
||||
|
||||
return jsonify({'success': True, 'redirect': url_for('login')})
|
||||
return redirect(url_for('login'))
|
||||
return render_template('register.html')
|
||||
|
||||
@app.route('/logout')
|
||||
@ -470,16 +430,12 @@ def user_files(username, subpath=''):
|
||||
|
||||
uploads = []
|
||||
for upload in user_uploads:
|
||||
vanity, content_type, data, created_at, _, is_private = upload[:6]
|
||||
url = f"{request.scheme}://{request.host}/{vanity}"
|
||||
uploads.append({
|
||||
'type': content_type,
|
||||
'vanity': vanity,
|
||||
'data': data,
|
||||
'created_at': created_at,
|
||||
'is_private': is_private,
|
||||
'url': url,
|
||||
'download_url': f"{url}/download" if content_type == 'file' else None
|
||||
'type': upload[1],
|
||||
'vanity': upload[0],
|
||||
'data': upload[2],
|
||||
'created_at': upload[3],
|
||||
'is_private': upload[5]
|
||||
})
|
||||
|
||||
parent_folder = os.path.dirname(subpath.rstrip('/')) if subpath else None
|
||||
@ -668,7 +624,7 @@ def upload_pastebin():
|
||||
vanity = shortuuid.uuid()[:8]
|
||||
print(f"Generated vanity: {vanity}")
|
||||
|
||||
user_id = current_user.id if current_user.is_authenticated else "Anonymous"
|
||||
user_id = current_user.id if current_user.is_authenticated else None
|
||||
print(f"User ID: {user_id}")
|
||||
|
||||
db = get_db()
|
||||
@ -693,7 +649,7 @@ def upload_pastebin():
|
||||
inserted_data = cursor.fetchone()
|
||||
print(f"Inserted data: {inserted_data}")
|
||||
|
||||
short_url = f"{request.scheme}://{request.host}/{vanity}"
|
||||
short_url = url_for('redirect_vanity', vanity=vanity, _external=True)
|
||||
deletion_url = url_for('delete_content', vanity=vanity, _external=True)
|
||||
print(f"Generated short URL: {short_url}")
|
||||
print(f"Generated deletion URL: {deletion_url}")
|
||||
@ -817,19 +773,11 @@ def delete_content(vanity):
|
||||
def content_info(vanity):
|
||||
db = get_db()
|
||||
cursor = db.cursor()
|
||||
|
||||
# First, try to find the content with the full vanity (including extension)
|
||||
cursor.execute("SELECT content.*, users.username FROM content LEFT JOIN users ON content.user_id = users.id WHERE content.vanity = ?", (vanity,))
|
||||
cursor.execute("SELECT * FROM content WHERE vanity = ?", (vanity,))
|
||||
content = cursor.fetchone()
|
||||
|
||||
# If not found, try without the extension
|
||||
if not content:
|
||||
vanity_without_extension = os.path.splitext(vanity)[0]
|
||||
cursor.execute("SELECT content.*, users.username FROM content LEFT JOIN users ON content.user_id = users.id WHERE content.vanity LIKE ?", (f"{vanity_without_extension}%",))
|
||||
content = cursor.fetchone()
|
||||
|
||||
if content:
|
||||
content_type, content_data, created_at, user_id, is_private, password, username = content[1], content[2], content[3], content[4], content[5], content[6], content[7]
|
||||
content_type, content_data, created_at, user_id, is_private, password = content[1], content[2], content[3], content[4], content[5], content[6]
|
||||
|
||||
if is_private and password:
|
||||
if request.method == 'POST':
|
||||
@ -839,6 +787,8 @@ def content_info(vanity):
|
||||
else:
|
||||
return render_template('password_prompt.html', vanity=vanity, error=None)
|
||||
|
||||
username = get_username(user_id)
|
||||
|
||||
file_size = None
|
||||
is_media = False
|
||||
if content_type == 'file':
|
||||
@ -869,7 +819,7 @@ def generate_sharex_config():
|
||||
base_url = request.url_root.replace('http://', 'https://', 1).rstrip('/')
|
||||
config = {
|
||||
"Version": "13.7.0",
|
||||
"Name": "sxbin",
|
||||
"Name": "aCloud",
|
||||
"DestinationType": "ImageUploader, TextUploader, FileUploader, URLShortener",
|
||||
"RequestMethod": "POST",
|
||||
"RequestURL": f"{base_url}/api/upload",
|
||||
@ -886,7 +836,7 @@ def generate_sharex_config():
|
||||
|
||||
response = make_response(json.dumps(config, indent=2))
|
||||
response.headers.set('Content-Type', 'application/json')
|
||||
response.headers.set('Content-Disposition', 'attachment', filename='sxbin_ShareX.sxcu')
|
||||
response.headers.set('Content-Disposition', 'attachment', filename='aCloud_ShareX.sxcu')
|
||||
return response
|
||||
|
||||
@app.route('/api/upload', methods=['POST'])
|
||||
@ -1097,124 +1047,64 @@ def rename_user_file(username):
|
||||
|
||||
@app.route('/upload/file', methods=['POST'])
|
||||
def upload_file():
|
||||
app.logger.info("Starting upload_file function")
|
||||
app.logger.info("Code: if 'file' not in request.files:")
|
||||
if 'file' not in request.files:
|
||||
app.logger.error("No file part in the request")
|
||||
return jsonify({'success': False, 'error': 'No file part'}), 400
|
||||
|
||||
app.logger.info("Code: file = request.files['file']")
|
||||
file = request.files['file']
|
||||
app.logger.info(f"File object: {file}")
|
||||
|
||||
app.logger.info("Code: if file.filename == '':")
|
||||
if file.filename == '':
|
||||
app.logger.error("No selected file")
|
||||
return jsonify({'success': False, 'error': 'No selected file'}), 400
|
||||
|
||||
app.logger.info("Code: if file:")
|
||||
if file:
|
||||
try:
|
||||
app.logger.info(f"Processing file: {file.filename}")
|
||||
|
||||
app.logger.info("Code: filename = secure_filename(file.filename)")
|
||||
filename = secure_filename(file.filename)
|
||||
app.logger.info(f"Secure filename: {filename}")
|
||||
|
||||
app.logger.info("Code: extension = os.path.splitext(filename)[1].lower()")
|
||||
extension = os.path.splitext(filename)[1].lower()
|
||||
app.logger.info(f"File extension: {extension}")
|
||||
|
||||
app.logger.info("Code: vanity = shortuuid.uuid()[:8]")
|
||||
vanity = shortuuid.uuid()[:8]
|
||||
app.logger.info(f"Generated vanity: {vanity}")
|
||||
|
||||
app.logger.info("Code: vanity_with_extension = f'{vanity}{extension}'")
|
||||
vanity_with_extension = f"{vanity}{extension}"
|
||||
app.logger.info(f"Vanity with extension: {vanity_with_extension}")
|
||||
|
||||
app.logger.info("Code: new_filename = vanity_with_extension")
|
||||
new_filename = vanity_with_extension
|
||||
app.logger.info(f"New filename: {new_filename}")
|
||||
|
||||
app.logger.info("Code: file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], new_filename)")
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], new_filename)
|
||||
app.logger.info(f"File path: {file_path}")
|
||||
|
||||
app.logger.info("Code: file.save(file_path)")
|
||||
file.save(file_path)
|
||||
app.logger.info("File saved successfully")
|
||||
|
||||
app.logger.info("Code: user_id = current_user.id if current_user.is_authenticated else None")
|
||||
user_id = current_user.id if current_user.is_authenticated else None
|
||||
app.logger.info(f"User ID: {user_id}")
|
||||
|
||||
app.logger.info("Code: password = request.form.get('password')")
|
||||
password = request.form.get('password')
|
||||
app.logger.info(f"Password: {'Set' if password else 'Not set'}")
|
||||
|
||||
app.logger.info("Code: is_private = 1 if password else 0")
|
||||
is_private = 1 if password else 0
|
||||
app.logger.info(f"Is private: {is_private}")
|
||||
|
||||
app.logger.info("Code: db = get_db()")
|
||||
db = get_db()
|
||||
app.logger.info("Database connection established")
|
||||
|
||||
app.logger.info("Code: cursor = db.cursor()")
|
||||
cursor = db.cursor()
|
||||
app.logger.info("Database cursor created")
|
||||
|
||||
app.logger.info("Inserting file info into database")
|
||||
app.logger.info("Code: vanity_for_db = vanity_with_extension")
|
||||
vanity_for_db = vanity_with_extension
|
||||
app.logger.info(f"Vanity for DB: {vanity_for_db}")
|
||||
|
||||
app.logger.info("Code: cursor.execute(...)")
|
||||
cursor.execute("INSERT INTO content (vanity, type, data, created_at, user_id, is_private, password) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(vanity_for_db, 'file', new_filename, datetime.now(), user_id, is_private, password))
|
||||
app.logger.info("SQL query executed")
|
||||
|
||||
app.logger.info("Code: db.commit()")
|
||||
(vanity_with_extension, 'file', new_filename, datetime.now(), user_id, is_private, password))
|
||||
db.commit()
|
||||
app.logger.info("Database changes committed")
|
||||
|
||||
app.logger.info("Code: scheme = 'https' if request.is_secure else 'http'")
|
||||
scheme = 'https' if request.is_secure else 'http'
|
||||
app.logger.info(f"Using scheme: {scheme}")
|
||||
short_url = url_for('redirect_vanity', vanity=vanity_with_extension, _external=True)
|
||||
short_url = short_url.replace('/download', '')
|
||||
download_url = short_url + '/download'
|
||||
deletion_url = url_for('delete_content', vanity=vanity_with_extension, _external=True)
|
||||
|
||||
app.logger.info("Generating URLs")
|
||||
app.logger.info("Code: short_url = url_for('redirect_vanity', vanity=vanity_with_extension, _external=True, _scheme=scheme)")
|
||||
short_url = f"{scheme}://{request.host}/{vanity_with_extension}"
|
||||
app.logger.info(f"Generated short URL: {short_url}")
|
||||
|
||||
app.logger.info("Code: download_url = short_url + '/download'")
|
||||
download_url = f"{short_url}/download"
|
||||
app.logger.info(f"Generated download URL: {download_url}")
|
||||
|
||||
app.logger.info("Code: deletion_url = url_for('delete_content', vanity=vanity_with_extension, _external=True, _scheme=scheme)")
|
||||
deletion_url = url_for('delete_content', vanity=vanity_with_extension, _external=True, _scheme=scheme)
|
||||
app.logger.info(f"Generated deletion URL: {deletion_url}")
|
||||
|
||||
app.logger.info("Preparing JSON response")
|
||||
response_data = {
|
||||
'success': True,
|
||||
'vanity': vanity_with_extension,
|
||||
'url': short_url,
|
||||
'download_url': download_url,
|
||||
'deletion_url': deletion_url,
|
||||
'filename': new_filename
|
||||
}
|
||||
app.logger.info(f"Response data: {response_data}")
|
||||
|
||||
return jsonify(response_data)
|
||||
# Add debug logging
|
||||
app.logger.info(f"File uploaded: {new_filename}")
|
||||
app.logger.info(f"File path: {file_path}")
|
||||
app.logger.info(f"Short URL: {short_url}")
|
||||
app.logger.info(f"Download URL: {download_url}")
|
||||
|
||||
# Check if the request is from ShareX
|
||||
if 'X-API-Key' in request.headers:
|
||||
return json.dumps({
|
||||
'status': 'success',
|
||||
'url': short_url,
|
||||
'deletion_url': deletion_url,
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'vanity': vanity_with_extension,
|
||||
'url': short_url,
|
||||
'download_url': download_url,
|
||||
'deletion_url': deletion_url,
|
||||
'filename': new_filename
|
||||
})
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error uploading file: {str(e)}")
|
||||
app.logger.exception("Exception traceback:")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
app.logger.error("Unknown error occurred")
|
||||
return jsonify({'success': False, 'error': 'Unknown error occurred'}), 500
|
||||
|
||||
# Add this function to validate passwords
|
||||
@ -1222,20 +1112,6 @@ def is_valid_password(password):
|
||||
banned_passwords = ['info', 'download']
|
||||
return password not in banned_passwords
|
||||
|
||||
@app.route('/reset_api_key', methods=['POST'])
|
||||
@login_required
|
||||
def reset_api_key():
|
||||
new_api_key = secrets.token_urlsafe(32)
|
||||
db = get_db()
|
||||
cursor = db.cursor()
|
||||
cursor.execute("UPDATE users SET api_key = ? WHERE id = ?", (new_api_key, current_user.id))
|
||||
db.commit()
|
||||
return jsonify({'success': True, 'new_api_key': new_api_key})
|
||||
|
||||
@app.route('/api/docs')
|
||||
def api_docs():
|
||||
return render_template('api_docs.html')
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Start the cleanup thread
|
||||
cleanup_thread = threading.Thread(target=delete_old_files)
|
||||
|
@ -1,62 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API Documentation - sxbin</title>
|
||||
<style>
|
||||
/* Add your styles here */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>sxbin API Documentation</h1>
|
||||
|
||||
<h2>Authentication</h2>
|
||||
<p>All API requests require an API key to be sent in the X-API-Key header.</p>
|
||||
|
||||
<h2>Endpoints</h2>
|
||||
|
||||
<h3>Upload File</h3>
|
||||
<pre>
|
||||
POST /api/upload/file
|
||||
Headers:
|
||||
X-API-Key: your_api_key
|
||||
Body: multipart/form-data
|
||||
file: (binary)
|
||||
</pre>
|
||||
<p>Returns: JSON with file URL and deletion URL</p>
|
||||
|
||||
<h3>Upload Pastebin</h3>
|
||||
<pre>
|
||||
POST /api/upload/pastebin
|
||||
Headers:
|
||||
X-API-Key: your_api_key
|
||||
Content-Type: application/json
|
||||
Body:
|
||||
{
|
||||
"content": "Your pastebin content here"
|
||||
}
|
||||
</pre>
|
||||
<p>Returns: JSON with pastebin URL and deletion URL</p>
|
||||
|
||||
<h3>Shorten URL</h3>
|
||||
<pre>
|
||||
POST /api/shorten
|
||||
Headers:
|
||||
X-API-Key: your_api_key
|
||||
Content-Type: application/json
|
||||
Body:
|
||||
{
|
||||
"url": "https://example.com/your-long-url-here"
|
||||
}
|
||||
</pre>
|
||||
<p>Returns: JSON with shortened URL and deletion URL</p>
|
||||
|
||||
<h2>Error Handling</h2>
|
||||
<p>All errors are returned as JSON with an "error" field describing the issue.</p>
|
||||
|
||||
<footer>
|
||||
<p>For more information or support, please contact our team.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -158,7 +158,7 @@
|
||||
<div class="container">
|
||||
<h2>Content</h2>
|
||||
{% if content %}
|
||||
<p>Uploaded by: {{ 'Anonymous' if content.username == 'None' else content.username }}</p>
|
||||
<p>Uploaded by: {{ content.username }}</p>
|
||||
<p>Created at: {{ content.created_at }}</p>
|
||||
|
||||
{% if highlighted_content %}
|
||||
|
@ -1,165 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ filename }} - sxbin</title>
|
||||
<meta property="og:title" content="{{ filename }}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ request.url }}">
|
||||
<meta property="og:description" content="File size: {{ file_size|filesizeformat }} | Uploaded by: {{ username }} | Date: {{ created_at.strftime('%Y-%m-%d %H:%M:%S') }}{% if is_private %} | Password Protected{% endif %}">
|
||||
{% if is_embeddable %}
|
||||
<meta property="og:image" content="{{ raw_url }}">
|
||||
{% endif %}
|
||||
<meta property="og:site_name" content="sxbin">
|
||||
<meta property="theme-color" content="#4CAF50">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #e0e0e0;
|
||||
background-color: #1e1e1e;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
h2 {
|
||||
color: #4CAF50;
|
||||
}
|
||||
.info-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.embed-container {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.embed-container img, .embed-container embed {
|
||||
max-width: 100%;
|
||||
max-height: 600px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.btn-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.btn {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 15px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
margin: 4px 2px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.home-button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
font-size: 24px;
|
||||
color: #4CAF50;
|
||||
text-decoration: none;
|
||||
}
|
||||
.home-button:hover {
|
||||
color: #45a049;
|
||||
}
|
||||
#theme-toggle {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
footer {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
background-color: #2a2a2a;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
.edit-btn, .delete-btn {
|
||||
background-color: #f44336;
|
||||
}
|
||||
.edit-btn:hover, .delete-btn:hover {
|
||||
background-color: #d32f2f;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/" class="home-button">⌂</a>
|
||||
<div class="container">
|
||||
<h2>{{ filename }}</h2>
|
||||
<div class="info-item"><strong>File size:</strong> {{ file_size|filesizeformat }}</div>
|
||||
<div class="info-item"><strong>Uploaded by:</strong> {{ username }}</div>
|
||||
<div class="info-item"><strong>Date:</strong> {{ created_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
|
||||
<div class="info-item"><strong>File type:</strong> {{ filename.split('.')[-1].upper() if '.' in filename else 'Unknown' }}</div>
|
||||
|
||||
{% if is_embeddable %}
|
||||
<div class="embed-container">
|
||||
{% if filename.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.svg')) %}
|
||||
<img src="{{ raw_url }}" alt="{{ filename }}">
|
||||
{% elif filename.lower().endswith('.pdf') %}
|
||||
<embed src="{{ raw_url }}" type="application/pdf" width="100%" height="600px">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="btn-container">
|
||||
<a href="{{ file_url }}/download{% if password %}/{{ password }}{% endif %}" class="btn">Download</a>
|
||||
<a href="{{ raw_url }}" class="btn">View Raw</a>
|
||||
{% if current_user.is_authenticated and current_user.id == user_id %}
|
||||
{% if filename.lower().endswith(('.txt', '.html', '.css', '.js', '.py', '.md')) or '.' not in filename %}
|
||||
<a href="{{ url_for('edit_content', vanity=vanity) }}" class="btn edit-btn">Edit</a>
|
||||
{% endif %}
|
||||
<form action="{{ url_for('delete_content', vanity=vanity) }}" method="post" style="display: inline;">
|
||||
<button type="submit" class="btn delete-btn" onclick="return confirm('Are you sure you want to delete this file?')">Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="theme-toggle">Toggle Theme</button>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
Source code: <a href="https://git.spitkov.hu/cgcristi/aCloud" target="_blank">Spitkov's Git</a> |
|
||||
<a href="https://office.bence.lol/form/#/2/form/view/z5Cf3CL6tZtPjzKsbcEPync6JE3iyMl22h6thUQg1a4/" target="_blank">Suggestions & Bugs</a> |
|
||||
<a href="https://office.bence.lol/kanban/#/2/kanban/view/hx6RTcpN0pR7hc1HHkMzG4awMoMdHjR2zbHjG7Xh+wU/embed/" target="_blank">Todo List</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const body = document.body;
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
body.classList.toggle('light-mode');
|
||||
localStorage.setItem('theme', body.classList.contains('light-mode') ? 'light' : 'dark');
|
||||
});
|
||||
|
||||
// Check for saved theme preference
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme === 'light') {
|
||||
body.classList.add('light-mode');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>sxbin - File Sharing Service</title>
|
||||
<title>aCloud - File Sharing Service</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
@ -110,10 +110,6 @@
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
@ -285,38 +281,16 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.instant-upload-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.instant-upload-result {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: #2a2a2a;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.close-modal:hover {
|
||||
color: #fff;
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@ -407,13 +381,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="instant-upload-overlay">
|
||||
<div class="instant-upload-result">
|
||||
<span class="close-modal">×</span>
|
||||
<h3>File Uploaded</h3>
|
||||
<p>Direct download URL: <a id="directDownloadUrl" href="#" target="_blank"></a></p>
|
||||
<p>Normal URL: <a id="normalUrl" href="#" target="_blank"></a></p>
|
||||
</div>
|
||||
<div class="instant-upload-result">
|
||||
<h3>File Uploaded</h3>
|
||||
<p>Direct download URL: <a id="directDownloadUrl" href="#" target="_blank"></a></p>
|
||||
<p>Normal URL: <a id="normalUrl" href="#" target="_blank"></a></p>
|
||||
<button onclick="closeInstantUploadResult()">Close</button>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
@ -427,7 +399,7 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const message1 = "Welcome to sxbin.gay";
|
||||
const message1 = "Welcome to aCloud.";
|
||||
const message2 = "\nA simple toolbox for file uploading,\nURL shortening and pastebin.";
|
||||
const typewriterTextElement = document.getElementById('typewriter-text');
|
||||
const cursorElement = document.getElementById('cursor');
|
||||
@ -458,8 +430,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
let isFileModalOpen = false;
|
||||
|
||||
function openModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
const button = document.querySelector(`button[onclick="openModal('${modalId}')"]`);
|
||||
@ -484,10 +454,6 @@
|
||||
clearInterval(fadeIn);
|
||||
}
|
||||
}, 30);
|
||||
|
||||
if (modalId === 'fileModal') {
|
||||
isFileModalOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
@ -507,18 +473,11 @@
|
||||
modal.style.display = "none";
|
||||
}
|
||||
}, 30);
|
||||
|
||||
if (modalId === 'fileModal') {
|
||||
isFileModalOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
window.onclick = function(event) {
|
||||
if (event.target.className === "modal") {
|
||||
event.target.style.display = "none";
|
||||
if (event.target.id === 'fileModal') {
|
||||
isFileModalOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -751,9 +710,7 @@
|
||||
// Global drag and drop
|
||||
document.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
if (!isFileModalOpen) {
|
||||
document.querySelector('.global-drop-area').style.display = 'flex';
|
||||
}
|
||||
document.querySelector('.global-drop-area').style.display = 'flex';
|
||||
});
|
||||
|
||||
document.addEventListener('dragleave', function(e) {
|
||||
@ -765,7 +722,7 @@
|
||||
document.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
document.querySelector('.global-drop-area').style.display = 'none';
|
||||
if (!isFileModalOpen && e.dataTransfer.files.length > 0) {
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
instantUploadFile(e.dataTransfer.files[0]);
|
||||
}
|
||||
});
|
||||
@ -785,7 +742,7 @@
|
||||
document.getElementById('directDownloadUrl').textContent = data.download_url;
|
||||
document.getElementById('normalUrl').href = data.url;
|
||||
document.getElementById('normalUrl').textContent = data.url;
|
||||
document.querySelector('.instant-upload-overlay').style.display = 'flex';
|
||||
document.querySelector('.instant-upload-result').style.display = 'block';
|
||||
} else {
|
||||
alert('Error uploading file: ' + data.error);
|
||||
}
|
||||
@ -796,25 +753,10 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Add this new function to close the modal
|
||||
function closeInstantUploadModal() {
|
||||
document.querySelector('.instant-upload-overlay').style.display = 'none';
|
||||
function closeInstantUploadResult() {
|
||||
document.querySelector('.instant-upload-result').style.display = 'none';
|
||||
}
|
||||
|
||||
// Add event listeners for closing the modal
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const closeModalButton = document.querySelector('.close-modal');
|
||||
const instantUploadOverlay = document.querySelector('.instant-upload-overlay');
|
||||
|
||||
closeModalButton.addEventListener('click', closeInstantUploadModal);
|
||||
|
||||
instantUploadOverlay.addEventListener('click', function(event) {
|
||||
if (event.target === instantUploadOverlay) {
|
||||
closeInstantUploadModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Paste functionality for file upload
|
||||
document.addEventListener('paste', function(e) {
|
||||
if (document.getElementById('fileModal').style.display === 'block') {
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - sxbin</title>
|
||||
<title>Login - aCloud</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
@ -97,54 +97,37 @@
|
||||
background-color: #2a2a2a;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
.error-message {
|
||||
background-color: #ff6b6b;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/" class="back-button">←</a>
|
||||
<div class="content">
|
||||
<div class="container">
|
||||
<h2>Login to sxbin</h2>
|
||||
<div id="error-message" class="error-message"></div>
|
||||
<form id="login-form" method="POST">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<label>
|
||||
<input type="checkbox" name="remember"> Remember me
|
||||
</label>
|
||||
<input type="submit" value="Login">
|
||||
<h2>Login</h2>
|
||||
<form method="POST">
|
||||
<div>
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="remember-me">
|
||||
<input type="checkbox" id="remember" name="remember">
|
||||
<label for="remember">Remember me</label>
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<p>Don't have an account? <a href="{{ url_for('register') }}">Register</a></p>
|
||||
<p>Don't have an account? <a href="{{ url_for('register') }}">Register here</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('login-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
fetch('/login', {
|
||||
method: 'POST',
|
||||
body: new FormData(this),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.href = data.redirect;
|
||||
} else {
|
||||
document.getElementById('error-message').textContent = data.error;
|
||||
document.getElementById('error-message').style.display = 'block';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<footer class="footer">
|
||||
<p>
|
||||
Source code: <a href="https://git.spitkov.hu/cgcristi/aCloud" target="_blank">Spitkov's Git</a> |
|
||||
<a href="https://office.bence.lol/form/#/2/form/view/z5Cf3CL6tZtPjzKsbcEPync6JE3iyMl22h6thUQg1a4/" target="_blank">Suggestions & Bugs</a> |
|
||||
<a href="https://office.bence.lol/kanban/#/2/kanban/view/hx6RTcpN0pR7hc1HHkMzG4awMoMdHjR2zbHjG7Xh+wU/embed/" target="_blank">Todo List</a>
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -1,22 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ filename }} - sxbin</title>
|
||||
<meta property="og:title" content="{{ filename }}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ request.url }}">
|
||||
<meta property="og:description" content="File size: {{ file_size|filesizeformat }} | Uploaded by: {{ username }} | Date: {{ created_at.strftime('%Y-%m-%d %H:%M:%S') }}">
|
||||
{% if is_embeddable %}
|
||||
<meta property="og:image" content="{{ file_url }}/raw">
|
||||
{% endif %}
|
||||
<meta property="og:site_name" content="sxbin">
|
||||
<meta property="theme-color" content="#4CAF50">
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
window.location.href = "{{ file_url }}";
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pastebin {{ vanity }} - sxbin</title>
|
||||
<meta property="og:title" content="Pastebin {{ vanity }}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ request.url }}">
|
||||
<meta property="og:description" content="{{ first_lines|truncate(200) }}">
|
||||
<meta property="og:site_name" content="sxbin">
|
||||
<meta property="theme-color" content="#4CAF50">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Pastebin {{ vanity }}</h1>
|
||||
<pre>{{ first_lines }}</pre>
|
||||
<p>Created by: {{ username }}</p>
|
||||
<p>Date: {{ created_at.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
||||
</body>
|
||||
</html>
|
@ -1,24 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Shortened URL - sxbin</title>
|
||||
<meta property="og:title" content="Shortened URL - sxbin">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ request.url }}">
|
||||
<meta property="og:description" content="Shortened URL {% if is_private %}(Password Protected){% endif %} | Created by: {{ username }} | Date: {{ created_at.strftime('%Y-%m-%d %H:%M:%S') }}">
|
||||
<meta property="og:site_name" content="sxbin">
|
||||
<meta property="theme-color" content="#4CAF50">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Shortened URL</h1>
|
||||
<p>Original URL: {{ long_url }}</p>
|
||||
<p>Created by: {{ username }}</p>
|
||||
<p>Date: {{ created_at.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
||||
{% if is_private %}
|
||||
<p>This URL is password protected.</p>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('redirect_vanity', vanity=vanity) }}">Access the URL</a>
|
||||
</body>
|
||||
</html>
|
@ -3,26 +3,104 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Password Protected Content - sxbin</title>
|
||||
<meta property="og:title" content="Password Protected Content - sxbin">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ request.url }}">
|
||||
<meta property="og:description" content="This {{ content_type }} is password protected. Enter the password to view.">
|
||||
<meta property="og:site_name" content="sxbin">
|
||||
<meta property="theme-color" content="#4CAF50">
|
||||
<title>Enter Password</title>
|
||||
<style>
|
||||
/* Add your styles here */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #1a1a1a;
|
||||
color: #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.container {
|
||||
background-color: #2a2a2a;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
width: 300px;
|
||||
}
|
||||
h2 {
|
||||
text-align: center;
|
||||
color: #4CAF50;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
input[type="password"] {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #4CAF50;
|
||||
border-radius: 4px;
|
||||
background-color: #333;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
button[type="submit"] {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
button[type="button"] {
|
||||
background-color: #888;
|
||||
color: white;
|
||||
}
|
||||
.error {
|
||||
color: #ff6b6b;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.alert {
|
||||
background-color: #ff6b6b;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Password Protected Content</h1>
|
||||
<p>This {{ content_type }} is password protected. Please enter the password to view.</p>
|
||||
{% if error %}
|
||||
<p style="color: red;">{{ error }}</p>
|
||||
{% endif %}
|
||||
<form method="POST">
|
||||
<input type="password" name="password" required>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
<div class="container">
|
||||
<h2>This content is password protected</h2>
|
||||
<div id="errorAlert" class="alert">
|
||||
Incorrect password. Please try again.
|
||||
</div>
|
||||
<form method="POST" action="{{ url_for('redirect_vanity', vanity=vanity) }}">
|
||||
<input type="password" name="password" placeholder="Enter password">
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function validatePassword() {
|
||||
var password = document.getElementById('password').value;
|
||||
if (password.trim() === '') {
|
||||
document.getElementById('errorAlert').style.display = 'block';
|
||||
document.getElementById('errorAlert').textContent = 'Please enter a password.';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
{% if error %}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('errorAlert').style.display = 'block';
|
||||
document.getElementById('errorAlert').textContent = '{{ error }}';
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,15 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="dark-mode">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pastebin {{ vanity }} - sxbin</title>
|
||||
<meta property="og:title" content="Pastebin {{ vanity }} - sxbin">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ request.url }}">
|
||||
<meta property="og:description" content="Pastebin{% if is_private %} (Password Protected){% endif %} | Uploaded by: {{ content.username }} | Date: {{ created_at.strftime('%Y-%m-%d %H:%M:%S') }}">
|
||||
<meta property="og:site_name" content="sxbin">
|
||||
<meta property="theme-color" content="#4CAF50">
|
||||
<title>Content</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #1e1e1e;
|
||||
@ -43,7 +37,7 @@
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
h1 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@ -72,31 +66,6 @@
|
||||
border-radius: 4px;
|
||||
padding: 1em;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.highlight pre {
|
||||
color: var(--text-color);
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.highlight .linenos {
|
||||
color: #999;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
border-right: 1px solid var(--highlight-border);
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.highlight .code {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#theme-toggle {
|
||||
@ -114,6 +83,12 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.highlight pre {
|
||||
color: var(--text-color);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@ -148,6 +123,68 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--bg-color);
|
||||
margin: 15% auto;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--highlight-border);
|
||||
width: 300px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modal-content input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border: 1px solid var(--highlight-border);
|
||||
background-color: var(--highlight-bg);
|
||||
color: var(--text-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.modal-buttons button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-ok {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: #888;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.home-button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
@ -169,7 +206,7 @@
|
||||
<a href="/" class="home-button">⌂</a>
|
||||
<div class="container">
|
||||
<h2>Content</h2>
|
||||
<p>Uploaded by: {{ 'Anonymous' if content.username == 'None' else content.username }}</p>
|
||||
<p>Uploaded by: {{ content.username }}</p>
|
||||
<p>Created at: {{ created_at }}</p>
|
||||
|
||||
<div class="highlight">
|
||||
@ -194,6 +231,19 @@
|
||||
|
||||
<button id="theme-toggle">Toggle Theme</button>
|
||||
|
||||
<div id="editPasswordModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3 id="passwordModalTitle">Edit Password</h3>
|
||||
<input type="password" id="newPassword1" placeholder="Enter new password">
|
||||
<input type="password" id="newPassword2" placeholder="Confirm new password">
|
||||
<div class="modal-buttons">
|
||||
<button onclick="closePasswordModal()" class="btn-cancel">Cancel</button>
|
||||
<button onclick="updatePassword()" class="btn-ok">OK</button>
|
||||
<button onclick="removePassword()" class="btn-remove" id="removePasswordBtn">Remove Password</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer style="text-align: center; margin-top: 20px; padding: 10px; background-color: #2a2a2a; color: #f0f0f0;">
|
||||
<p>
|
||||
Source code: <a href="https://git.spitkov.hu/cgcristi/aCloud" target="_blank">Spitkov's Git</a> |
|
||||
@ -227,6 +277,87 @@
|
||||
if (localStorage.getItem('lightMode') === 'true') {
|
||||
html.classList.add('light-mode');
|
||||
}
|
||||
|
||||
function openEditPasswordModal() {
|
||||
document.getElementById('passwordModalTitle').textContent = 'Edit Password';
|
||||
document.getElementById('removePasswordBtn').style.display = 'inline-block';
|
||||
document.getElementById('editPasswordModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function openAddPasswordModal() {
|
||||
document.getElementById('passwordModalTitle').textContent = 'Add Password';
|
||||
document.getElementById('removePasswordBtn').style.display = 'none';
|
||||
document.getElementById('editPasswordModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closePasswordModal() {
|
||||
document.getElementById('editPasswordModal').style.display = 'none';
|
||||
document.getElementById('newPassword1').value = '';
|
||||
document.getElementById('newPassword2').value = '';
|
||||
}
|
||||
|
||||
function updatePassword() {
|
||||
const password1 = document.getElementById('newPassword1').value;
|
||||
const password2 = document.getElementById('newPassword2').value;
|
||||
|
||||
if (password1 !== password2) {
|
||||
alert('Passwords do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('{{ url_for("edit_password", vanity=vanity) }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'update',
|
||||
new_password: password1
|
||||
}),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Password updated successfully');
|
||||
closePasswordModal();
|
||||
location.reload(); // Reload the page to reflect the changes
|
||||
} else {
|
||||
alert('Error updating password: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('An error occurred while updating the password');
|
||||
});
|
||||
}
|
||||
|
||||
function removePassword() {
|
||||
if (confirm('Are you sure you want to remove the password?')) {
|
||||
fetch('{{ url_for("edit_password", vanity=vanity) }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'remove'
|
||||
}),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Password removed successfully');
|
||||
closePasswordModal();
|
||||
location.reload(); // Reload the page to reflect the changes
|
||||
} else {
|
||||
alert('Error removing password: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('An error occurred while removing the password');
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Register - sxbin</title>
|
||||
<title>Register - aCloud</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
@ -89,83 +89,33 @@
|
||||
background-color: #2a2a2a;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
.error-message {
|
||||
background-color: #ff6b6b;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.password-requirement {
|
||||
font-size: 0.9em;
|
||||
margin-top: 5px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.requirement-not-met {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.requirement-met {
|
||||
color: #4CAF50;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/" class="back-button">←</a>
|
||||
<div class="content">
|
||||
<div class="container">
|
||||
<h2>Register for sxbin</h2>
|
||||
<div id="error-message" class="error-message"></div>
|
||||
<form id="register-form" method="POST">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<div id="password-length" class="password-requirement requirement-not-met">At least 5 characters</div>
|
||||
<div id="password-uppercase" class="password-requirement requirement-not-met">At least one uppercase letter</div>
|
||||
<input type="submit" value="Register" id="submit-button" disabled>
|
||||
<h2>Register</h2>
|
||||
<form method="POST">
|
||||
<div>
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
<p>Already have an account? <a href="{{ url_for('login') }}">Login</a></p>
|
||||
<p>Already have an account? <a href="{{ url_for('login') }}">Login here</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const passwordInput = document.getElementById('password');
|
||||
const lengthRequirement = document.getElementById('password-length');
|
||||
const uppercaseRequirement = document.getElementById('password-uppercase');
|
||||
const submitButton = document.getElementById('submit-button');
|
||||
|
||||
passwordInput.addEventListener('input', function() {
|
||||
const password = this.value;
|
||||
const meetsLengthRequirement = password.length >= 5;
|
||||
const meetsUppercaseRequirement = /[A-Z]/.test(password);
|
||||
|
||||
lengthRequirement.className = meetsLengthRequirement ? 'password-requirement requirement-met' : 'password-requirement requirement-not-met';
|
||||
uppercaseRequirement.className = meetsUppercaseRequirement ? 'password-requirement requirement-met' : 'password-requirement requirement-not-met';
|
||||
|
||||
submitButton.disabled = !(meetsLengthRequirement && meetsUppercaseRequirement);
|
||||
});
|
||||
|
||||
document.getElementById('register-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
fetch('/register', {
|
||||
method: 'POST',
|
||||
body: new FormData(this),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.href = data.redirect;
|
||||
} else {
|
||||
document.getElementById('error-message').textContent = data.error;
|
||||
document.getElementById('error-message').style.display = 'block';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<footer class="footer">
|
||||
<p>
|
||||
Source code: <a href="https://git.spitkov.hu/cgcristi/aCloud" target="_blank">Spitkov's Git</a> |
|
||||
<a href="https://office.bence.lol/form/#/2/form/view/z5Cf3CL6tZtPjzKsbcEPync6JE3iyMl22h6thUQg1a4/" target="_blank">Suggestions & Bugs</a> |
|
||||
<a href="https://office.bence.lol/kanban/#/2/kanban/view/hx6RTcpN0pR7hc1HHkMzG4awMoMdHjR2zbHjG7Xh+wU/embed/" target="_blank">Todo List</a>
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -881,7 +881,7 @@
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="openTab(event, 'filesAndFolders')">Files and Folders</button>
|
||||
<button class="tab" onclick="openTab(event, 'myUploads')">My Uploads</button>
|
||||
<button class="tab" onclick="openTab(event, 'apiKeyShareX')">API Key & ShareX</button>
|
||||
<button class="tab" onclick="openTab(event, 'shareXConfig')">ShareX Config</button>
|
||||
</div>
|
||||
|
||||
<div id="filesAndFolders" class="tab-content active">
|
||||
@ -954,7 +954,7 @@
|
||||
{% elif upload.type == 'url' %}
|
||||
Shortened URL:
|
||||
{% endif %}
|
||||
<a href="{{ upload.url }}" target="_blank">
|
||||
<a href="{{ url_for('redirect_vanity', vanity=upload.vanity) }}" target="_blank">
|
||||
{% if upload.type == 'file' %}
|
||||
{{ upload.data }}
|
||||
{% else %}
|
||||
@ -985,33 +985,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="apiKeyShareX" class="tab-content">
|
||||
<h3>API Key & ShareX</h3>
|
||||
|
||||
<div class="sharex-config-section">
|
||||
<h4>ShareX Configuration</h4>
|
||||
<p>Download the ShareX configuration file to easily integrate with our service:</p>
|
||||
<a href="{{ url_for('generate_sharex_config') }}" class="btn">Download ShareX Config</a>
|
||||
</div>
|
||||
|
||||
<div class="api-key-section">
|
||||
<h4>Your API Key</h4>
|
||||
<p>API Key: <span id="apiKey" class="blurred">{{ current_user.api_key }}</span></p>
|
||||
<button onclick="showResetApiKeyModal()" class="btn">Reset API Key</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset API Key Modal -->
|
||||
<div id="resetApiKeyModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Reset API Key</h2>
|
||||
<p>Are you sure you want to reset your API key? This action cannot be undone.</p>
|
||||
<p>Your old ShareX configuration will no longer work. You'll need to generate a new one after resetting.</p>
|
||||
<div class="modal-buttons">
|
||||
<button onclick="resetApiKey()" class="btn btn-danger">Reset API Key</button>
|
||||
<button onclick="closeResetApiKeyModal()" class="btn">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="shareXConfig" class="tab-content">
|
||||
<h3>ShareX Configuration</h3>
|
||||
<p>Click the button below to download your ShareX configuration file:</p>
|
||||
<a href="{{ url_for('generate_sharex_config') }}" class="btn" download="aCloud_ShareX.sxcu">Download ShareX Config</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1612,22 +1589,6 @@
|
||||
.file-group-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.blurred {
|
||||
filter: blur(5px);
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.blurred:hover {
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
.api-key-section, .sharex-config-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: var(--highlight-bg);
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@ -2227,38 +2188,6 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function showResetApiKeyModal() {
|
||||
document.getElementById('resetApiKeyModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeResetApiKeyModal() {
|
||||
document.getElementById('resetApiKeyModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function resetApiKey() {
|
||||
fetch('/reset_api_key', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('apiKey').textContent = data.new_api_key;
|
||||
alert('API Key has been reset. Please update your applications and download a new ShareX configuration.');
|
||||
} else {
|
||||
alert('Error resetting API Key: ' + data.error);
|
||||
}
|
||||
closeResetApiKeyModal();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('An error occurred while resetting the API Key');
|
||||
closeResetApiKeyModal();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user