From 714c1fbe3332a4884977bf4f27925af2c2b45b9d Mon Sep 17 00:00:00 2001 From: klein panic Date: Tue, 1 Oct 2024 23:06:33 -0400 Subject: [PATCH] finished project - working --- server/__pycache__/db_setup.cpython-311.pyc | Bin 9410 -> 14376 bytes server/__pycache__/preview.cpython-311.pyc | Bin 0 -> 2040 bytes server/__pycache__/rename.cpython-311.pyc | Bin 0 -> 1969 bytes server/__pycache__/security.cpython-311.pyc | Bin 2126 -> 3534 bytes server/app.py | 140 ++++++++++++++++---- server/app.py.bak | 87 +++++++----- server/db_setup.py | 68 +++++++++- server/preview.py | 37 ++++++ server/rename.py | 40 ++++++ server/security.py | 65 +++++++-- server/transfer_service.db | Bin 16384 -> 16384 bytes 11 files changed, 359 insertions(+), 78 deletions(-) create mode 100644 server/__pycache__/preview.cpython-311.pyc create mode 100644 server/__pycache__/rename.cpython-311.pyc create mode 100644 server/preview.py create mode 100644 server/rename.py diff --git a/server/__pycache__/db_setup.cpython-311.pyc b/server/__pycache__/db_setup.cpython-311.pyc index 26b264ed6304af64279c77cf5b7c5d6db07b20bf..faca429b94a335cafd3864bf908e1eb9a9049447 100644 GIT binary patch literal 14376 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 delta 1930 zcmZ2caLAKyIWI340|NttT=$so@N>Y&k0|NsG0|P^G%jAdbGW=q!Vk=5EgnVESV-@?rAU64< zp_rN&tJsCmum>ER{hVE#9~jt$c|J0*bMk%x5g)*W2m>eAWNi+~dJzUr-hS>b?oQqw z-VWXmx)4*qMzM;01R44TM8J%PN}zFn{P=+;f{lyKw0dj`$u|z0X<#v2M%KeUR~*5> zz_6UznT>gsl9L|mK^}W2arQ&pJWhJ7hs0T&q`41CGlST2j3Bl=n61R)%*K33kHyJ| z^NmgG{5ZesIKFr4A%*B3~m(iJvJsF%_!07`LdCUw944-qD7$)ZkN;1_jPEO!b zne5ALq*=?v$xy-xRmi}=P{NI1XR*Q9C43B%3l+sD8z>4+-lM_6i=?53dGZD(kXd{j zrVO<#H7pA_CqFb67iXwpN@H5a#K5o`?i5Cb8s^D`LMkkb3?<@|cT1>F))1bg3C=bB{ zGD|95i;D7#G+A$PloqEJrbG{yXNpODXqiVKk$7yL3W@?>4%$-2OkwfUH63S)g8 z0|Ub@H5Xong9^+p49o|Wm7FzL53xEjI192LX613#U_C6z;w;L2SdIz|JI6 z1d4_tIgkpt36mcxNwI<=z+m!0!_dv4$`Q=M*pwYmljBlhU|=u>=}rclze>wwvxT}i zBcsyh0F76SjI5KbwN&&Gt^^qaigTztxe`-S;!6v1@)J{vG#MBetUxAZf=t@HN-Kle zn?OJ$=HwKC5`Qr$v%vh$4fZ?O%;F*)1_lOukf9o2L#y>TnQcLC&}6>F39&amGX*J8 z!ovWRBzaR(b5cuEVGh)1U|?|Be9$n9v0e!z%@H4;o0ypwAFs&=$udG9QLg0typq(4 zlAO#WNNxh>Ay91qPS-`C2q^*uZV@Qxz`h2%vIu1SEspqjPr1^^nY!#w~1 diff --git a/server/__pycache__/preview.cpython-311.pyc b/server/__pycache__/preview.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ec814587152cc13c72f9a28f4190d24264d9d09 GIT binary patch literal 2040 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 Xa4R%$d|**v)cwGKNfaqCFfafB*?+ks literal 0 HcmV?d00001 diff --git a/server/__pycache__/security.cpython-311.pyc b/server/__pycache__/security.cpython-311.pyc index 3033317a6f433bd9c5ac3c97493a7772f62992c2..cb17c8d6a8d92b6f9bd4566e499848b263a04667 100644 GIT binary patch literal 3534 zcmZ3^%ge>Uz`!tn%bzqOE(V6jAPx*OK^dPT7#J9)Go&!2Fy=5sL1@M(#uSDWrW~eR z<|t-HkQj3gOD<~^Yc5+9TP}MPJ0n91OB6>6YYRgZX9`;jLljpEdkaGpcM3-fLljR6 zXA46VZwg~DgC^HYkTrgqjJMc}QVUB{i%T?_Z*iojmc*A9rxx9k$jnPFO3h8pD~Zp^ zPtVMYPb?`(%`GS?z9mwWTAYe5`jVM}fgu@WA`F8BK=`v3*cT;?FbM{REI2!baTy~6 z!)iD`m_d`NN{dTDK|#SiH7~U&u_U!vAyL85OgE{rBvqj(F)t-QSD`pDr$i5|=%oMy z1A`{hEvEe9TkNF}>6HwhL00{8*3Zb#P1VoNNzKgDPtH$C4G-22s4U6I&okD~EXc^u zOV!QE%qy+XEh$ROD^5!-(l1UeDg#r=rA3(~m3jr0w|LW`&WHzDTEx%5z`()4z)&p2 zz`)SJ@Rfsshp)$KLQ1_p2_oWcl&8paYNmCKkI7*@mi zMItqf3*bDEK`^|Gfq`K)jKfgNSi%nHFl4d7*foqb48-WKVqjoEje!Lq7sHH!)2Oxv zGiWmUX|mno$}G;zD=taQOHRGTTwGFgi!C(|6wtTWGZKq4ax#-{u@z?|8kw5i;><{` zNXbkGB{HU490iHR#pU@$DYsZafm#gmr-DKe9|Hr!Enbk$c$k7BNd^W6ka)2VIJ}>- z3rldaXeVqgtjKree_&kZNcM zr7+eo%w|Yon#+tDnv4u7%)tybOfk$147JR)EVZmPtR?VRVqnMyhjtM*vskd0#Tv|j z9N#sJH7uyM^Eh7Vp znGm0HU~wI1FhdD9UUQIqUc*|&#lVol1tM$NQEcD_hb2!9`)r03-np#k?oQ!L;fKmG zr!fUHXbM!>gA0SO#GK3&aEVcpk*WYH#ng)xl8aJPQu9hO6LX3c64Mhw=~bb)B)=#% zMIj}zBoR`KRI!B?rxq!wR|%>sq~xa-E9B*uD5O?o7MJL0GPzZWfMwAns+80fG7^gw zQY(^EQ&Un?6mk(f&R$Vp8sPE|QqE zJ~1UXGcU8aq$sf@zeul&KcFbTEE8l2D0^5bRPpO51VcOl6_8X1nV+7TqL7)VP+Xdv zoLXF*R+^Jjsi(;lQYGjED#DS(^fYtxy})NQq2dCc5d>Z2FuuZJe1XIG2EWb)ew_z8rWaW>8~i7P%&_~w#=s#qSLPDC z>IHUHutsN)M&}EBAas$#^en zBC890Aas$#`U;2j1rF;Q{2CYdHJ)-PEReX!p>>5r>jHO9nXq?VzEA%ziC#zVB%GNv%qFqSbCF{d!qGUdsoFhhCFwM=>9DJ)PP zYb{frKnfd_$5zXf$C<(o<#E(9EX4 zC%AA(%S=eyE^oczR;)FSm_g_P8?%;Z#s%)GSxqTIxi%=|osw4(f6urg?aNg*Q@ zq_`MTh}_}=r{Ki&)Vz{gY?%R|7RD`h$GnuH{LGYF?BSVtDf#8aRa{VpLV)uvX5Yl* zTdY2yR!9{um{RafOwP#6OReGy&CAZqFV9nO0lTA$Clune6osTpg(@}$4bK28gSG}FK|0v;C5UQy+h=Rt{sE}B3T^qB)tSk(u1TcFo|e%7BHqTmVm_|L>9Ob1hE%@ z0tLiC!6{5B%&66D3d`gWAcE5+78) zg3}VjQK%06#bJ}1pHiBWYFA{*z`y{ilZsa{Ffe>zW@Kc%!NA!7hBp}a8o=-dgX9HN zbc4b70&etxU!Z~K0}BhI)CUGQ!NbDnzzE{QNgf%-NX8EgNTiJjqZ8u?1|(94ossPW J1113uBmk&cdWHZ1 literal 2126 zcmZ3^%ge>Uz`*b^;!m0)D+9x05C?{tpp4HR3=9m@8B!Qh7;_kM8KW3;nWC5&L42kh z<|yVAh7{%;mR!~-)?BtIwp{ioc1DI2mMD%C))s~+&J?y5hA6HS_7;XH?i8kA22GBa zAY(O|Zm}1o7M7+Km)v46PAx9Z%+K@FWWL3bo>~%LTAW&ROCmEbxhOR^HLoN-CqF$i zFFvuPBsI67r1+LdQEG82y68)01_p*?n3F+PurM$%fCN7qFoK+4!Uz(C;2MTyj0_B` z;Ucw6HB7}!DU87kHB2$g3=Fl*wJf!)HLN8_=4G>j^cFFJJW&E>gZK;#Ss**W%o@f8 zAUnZqWMUZu1H)>#0IJOkkW7LrVL;VU%T~iy!;%elOOap+FG3x zNkQ>f4Qmw_149ZMh@8!k!akQd4IE8=noMq0BB8~pMG7hTsl^I;`6UXe6`92)dLRiW z-^7Z{+|pbHaNH=s;zl90A~`iRB{fA)As{C;u{c#BIX|x?F}XycBqLQJF(o%MFSEF$ zD6u5JNKca~q)N~S6txP)rOC;u#l>l*IeKnYqMmtWi8+}m3du#ODXDoSnTa{YdI~Q= zkyRxIvq_^^Q=up|H!(9WGcR3FlcNX}j<Ni0dc#a~>KUzC~xPMu&?Fafw?K5$?l5-LB)0jVkRFa^aR+Y}(+m$QCG zer~FMc1~($o<7J~;lcU=l_eSZdB*yg1sVBysk%9td8HM)B}IvO#c8QU`o*b5Wne0~ zv?#NrQm>%$7B4uY5=&CSt}2pXU|`^2U|=X-%fP_Uz;IVUVur*;0i`PfN)4VjxJ5oN zFtY02;Mcjpuk*m%;UbGkga3s11$htHMd!*~VpqMuu6lz%>H>e%Q!&{oc?%+z$1jS% zsA_ga)$F2}`4utq>tc?V#2hb*IbRWTZt#D=E;d8|61(OFcFnsSGBYABa;RP5P`kjP zc7tEz0>8#n4uu607df=9aA;lN(7M4dazQM8ZO)Fki+bKC5-;lcUl2=&BL4IX9O<7y zNj4coz%VGRKw0Z^8@TjA&c0~bgCT{nh9QMX5~h=(jG>4pg}H_yg#{rtn<0gDE^{zL z3R?|B3OkB8M-4*?CyEFc7QO7j44T}2x7acRKyh=63!E?#(^KXQd1O? zDit8|3L2gPRtmS6G7Ek&YF65vkqQF?gET1SQy?)fqw($%3-9 z3@A&p6oK4xi#;PXF(tLA_!e_|YRN6`qSV~{lGOObl$0V(#v+hHq_8bg0_g@3AY5z+ z2~9zXDe~6^RW1ptEQr})c~Q{zilA+S*Hb>xD}0JK#1uZTGV`!~V1N*;Y~Y~L`nmrU>5xV diff --git a/server/app.py b/server/app.py index 6fb9e6d..1d1713f 100644 --- a/server/app.py +++ b/server/app.py @@ -1,10 +1,13 @@ -from flask import Flask, request, render_template, redirect, url_for, session, send_from_directory +from flask import Flask, request, render_template, redirect, url_for, session, send_from_directory, jsonify, send_file from flask_talisman import Talisman from functools import wraps import os from security import validate_user from data_handler import save_link, save_file, retrieve_uploads, handle_download, get_file_path from datetime import datetime +from zipfile import ZipFile +from preview import generate_preview # Import the preview module +from rename import rename_file # Import the rename module app = Flask(__name__, template_folder='../templates') app.secret_key = os.urandom(24) @@ -35,6 +38,8 @@ def login_required(f): def ensure_login(): if 'username' not in session and request.endpoint not in ('login', 'static'): return redirect(url_for('login')) + elif request.endpoint == 'login' and 'username' in session: + session.clear() @app.route('/') @login_required @@ -69,15 +74,18 @@ def upload_link(): save_link(uploader, link) return redirect(url_for('index')) -@app.route('/upload/file', methods=['POST']) +@app.route('/upload/files', methods=['POST']) @login_required -def upload_file(): - if 'file' not in request.files: +def upload_files(): + if 'files' not in request.files: return redirect(url_for('index')) - file = request.files['file'] + files = request.files.getlist('files') uploader = session['username'] - save_file(uploader, file) + + for file in files: + save_file(uploader, file) + return redirect(url_for('index')) @app.route('/uploads') @@ -104,14 +112,12 @@ def view_uploads(): def download_link(link_id): upload = handle_download(link_id) - if upload[2] == 'link': + if upload and upload[2] == 'link': link_content = upload[3] - x = 1 while os.path.exists(os.path.join(DOWNLOADS_DIRECTORY, f"link_{x}.txt")): x += 1 filename = f"link_{x}.txt" - filepath = os.path.join(DOWNLOADS_DIRECTORY, filename) with open(filepath, 'w') as f: f.write(link_content) @@ -119,7 +125,6 @@ def download_link(link_id): response = send_from_directory(DOWNLOADS_DIRECTORY, filename, as_attachment=True) handle_download(link_id, delete_only=True) - return response return "Link Not found", 404 @@ -137,13 +142,9 @@ def download_all_links(): for link in links: f.write(link[3] + "\n") - # Serve the combined links file from the Downloads directory response = send_from_directory(DOWNLOADS_DIRECTORY, filename, as_attachment=True) - - # Now delete all the link entries from the database after serving for link in links: handle_download(link[0], delete_only=True) - return response else: return redirect(url_for('view_uploads')) @@ -153,23 +154,22 @@ def download_all_links(): def download(upload_id): upload = handle_download(upload_id) - if not upload: - print(f"Error: No upload found with ID {upload_id}") - return "The requested file does not exist or you do not have permission to access it.", 404 - - if upload[2] == 'file': + if upload and upload[2] == 'file': file_path = get_file_path(upload[3]) - print(f"Trying to download file: {file_path}") - if not os.path.isfile(file_path): - print(f"Error: File not found at {file_path}") - return "File not found", 404 - - response = send_from_directory(os.path.dirname(file_path), os.path.basename(file_path), as_attachment=True) + return redirect(url_for('view_uploads'), code=404) + + response = send_from_directory( + directory=os.path.dirname(file_path), + path=os.path.basename(file_path), + as_attachment=True, + #download_name=os.path.basename(file_path) # Ensures the download retains the current name + download_name=upload[3] + ) handle_download(upload_id, delete_only=True) - - return response + return response + return "The requested file does not exist or you do not have permission to access it.", 404 @app.route('/delete_link/', methods=['GET']) @login_required @@ -183,6 +183,88 @@ def delete_file(file_id): handle_download(file_id, delete_only=True) return redirect(url_for('view_uploads')) -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, ssl_context='adhoc') +# Add routes for downloading all photos +@app.route('/download_all_photos', methods=['GET']) +@login_required +def download_all_photos(): + photos = [upload for upload in retrieve_uploads() if upload[2] == 'file' and upload[3].lower().endswith(('.jpg', '.jpeg', '.png', '.gif'))] + if photos: + zip_filename = f"photos_{datetime.now().strftime('%m-%d-%Y')}.zip" + zip_filepath = os.path.join(DOWNLOADS_DIRECTORY, zip_filename) + + # Create a zip file containing all photos + with ZipFile(zip_filepath, 'w') as zipf: + for photo in photos: + file_path = get_file_path(photo[3]) + zipf.write(file_path, os.path.basename(file_path)) + handle_download(photo[0], delete_only=True) # Delete each photo entry + + return send_from_directory(DOWNLOADS_DIRECTORY, zip_filename, as_attachment=True) + else: + return redirect(url_for('view_uploads')) + +# Add similar routes for videos and misc +@app.route('/download_all_videos', methods=['GET']) +@login_required +def download_all_videos(): + videos = [upload for upload in retrieve_uploads() if upload[2] == 'file' and upload[3].lower().endswith(('.mp4', '.mkv', '.avi'))] + + if videos: + zip_filename = f"videos_{datetime.now().strftime('%m-%d-%Y')}.zip" + zip_filepath = os.path.join(DOWNLOADS_DIRECTORY, zip_filename) + + with ZipFile(zip_filepath, 'w') as zipf: + for video in videos: + file_path = get_file_path(video[3]) + zipf.write(file_path, os.path.basename(file_path)) + handle_download(video[0], delete_only=True) # Delete each video entry + + return send_from_directory(DOWNLOADS_DIRECTORY, zip_filename, as_attachment=True) + else: + return redirect(url_for('view_uploads')) + +@app.route('/download_all_misc', methods=['GET']) +@login_required +def download_all_misc(): + misc_files = [upload for upload in retrieve_uploads() if upload[2] == 'file' and upload not in videos + photos] + + if misc_files: + zip_filename = f"misc_{datetime.now().strftime('%m-%d-%Y')}.zip" + zip_filepath = os.path.join(DOWNLOADS_DIRECTORY, zip_filename) + + with ZipFile(zip_filepath, 'w') as zipf: + for item in misc_files: + file_path = get_file_path(item[3]) + zipf.write(file_path, os.path.basename(file_path)) + handle_download(item[0], delete_only=True) # Delete each misc entry + + return send_from_directory(DOWNLOADS_DIRECTORY, zip_filename, as_attachment=True) + else: + return redirect(url_for('view_uploads')) + +# Route to handle file renaming +@app.route('/rename/', methods=['POST']) +@login_required +def rename(upload_id): + data = request.get_json() # Expecting JSON data from the frontend + new_name = data.get('new_name', '').strip() + + if not new_name: + return jsonify({"error": "New name not provided"}), 400 + + success, message = rename_file(upload_id, new_name) + + if success: + return jsonify({"success": True}), 200 + else: + return jsonify({"error": message}), 400 + +# Route to handle file previews +@app.route('/preview/', methods=['GET']) +@login_required +def preview(upload_id): + return generate_preview(upload_id) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=True, ssl_context='adhoc') diff --git a/server/app.py.bak b/server/app.py.bak index 1fb32f9..49b02c2 100644 --- a/server/app.py.bak +++ b/server/app.py.bak @@ -4,19 +4,23 @@ from functools import wraps import os from security import validate_user from data_handler import save_link, save_file, retrieve_uploads, handle_download, get_file_path +from datetime import datetime app = Flask(__name__, template_folder='../templates') -app.secret_key = os.urandom(24) # Generate a more secure secret key +app.secret_key = os.urandom(24) talisman = Talisman(app, content_security_policy={ 'default-src': ["'self'"], - 'script-src': ["'self'", "'unsafe-inline'"] # Allow inline scripts + 'script-src': ["'self'", "'unsafe-inline'"] }) UPLOAD_DIRECTORY = "../assets" if not os.path.exists(UPLOAD_DIRECTORY): os.makedirs(UPLOAD_DIRECTORY) -# Login required decorator +DOWNLOADS_DIRECTORY = os.path.expanduser("~/Downloads") +if not os.path.exists(DOWNLOADS_DIRECTORY): + os.makedirs(DOWNLOADS_DIRECTORY) + def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): @@ -65,15 +69,18 @@ def upload_link(): save_link(uploader, link) return redirect(url_for('index')) -@app.route('/upload/file', methods=['POST']) +@app.route('/upload/files', methods=['POST']) @login_required -def upload_file(): - if 'file' not in request.files: +def upload_files(): + if 'files' not in request.files: return redirect(url_for('index')) - file = request.files['file'] + files = request.files.getlist('files') uploader = session['username'] - save_file(uploader, file) + + for file in files: + save_file(uploader, file) + return redirect(url_for('index')) @app.route('/uploads') @@ -81,7 +88,6 @@ def upload_file(): def view_uploads(): uploads = retrieve_uploads() - # Categorizing uploads links = [upload for upload in uploads if upload[2] == 'link'] videos = [upload for upload in uploads if upload[2] == 'file' and upload[3].lower().endswith(('.mp4', '.mkv', '.avi'))] photos = [upload for upload in uploads if upload[2] == 'file' and upload[3].lower().endswith(('.jpg', '.jpeg', '.png', '.gif'))] @@ -99,24 +105,23 @@ def view_uploads(): @app.route('/download_link/', methods=['GET']) @login_required def download_link(link_id): - uploader = session['username'] - upload = handle_download(link_id, uploader) + upload = handle_download(link_id) - if upload[2] == 'link': + if upload and upload[2] == 'link': link_content = upload[3] - - # Create a unique filename x = 1 - while os.path.exists(f"link_{x}.txt"): + while os.path.exists(os.path.join(DOWNLOADS_DIRECTORY, f"link_{x}.txt")): x += 1 filename = f"link_{x}.txt" - - # Save the link content to the file - with open(filename, 'w') as f: + filepath = os.path.join(DOWNLOADS_DIRECTORY, filename) + with open(filepath, 'w') as f: f.write(link_content) - # Serve the file - return send_from_directory(directory=os.getcwd(), filename=filename, as_attachment=True) + response = send_from_directory(DOWNLOADS_DIRECTORY, filename, as_attachment=True) + + handle_download(link_id, delete_only=True) + return response + return "Link Not found", 404 @app.route('/download_all_links', methods=['GET']) @login_required @@ -124,26 +129,48 @@ def download_all_links(): links = [upload for upload in retrieve_uploads() if upload[2] == 'link'] if len(links) > 1: - with open("links_data.txt", 'w') as f: + current_date = datetime.now().strftime("%m-%d-%Y") + filename = f"links_{current_date}.txt" + + links_file_path = os.path.join(DOWNLOADS_DIRECTORY, filename) + with open(links_file_path, 'w') as f: for link in links: f.write(link[3] + "\n") - return send_from_directory(directory=os.getcwd(), filename="links_data.txt", as_attachment=True) + response = send_from_directory(DOWNLOADS_DIRECTORY, filename, as_attachment=True) + for link in links: + handle_download(link[0], delete_only=True) + return response else: return redirect(url_for('view_uploads')) @app.route('/download/', methods=['GET']) @login_required def download(upload_id): - uploader = session['username'] - upload = handle_download(upload_id, uploader) - - if upload[2] == 'link': - return f"{upload[3]}" - elif upload[2] == 'file': - # Integrate get_file_path here + upload = handle_download(upload_id) + + if upload and upload[2] == 'file': file_path = get_file_path(upload[3]) - return send_from_directory(os.path.dirname(file_path), os.path.basename(file_path), as_attachment=True) + if not os.path.isfile(file_path): + return "File not found", 404 + + response = send_from_directory(os.path.dirname(file_path), os.path.basename(file_path), as_attachment=True) + handle_download(upload_id, delete_only=True) + return response + + return "The requested file does not exist or you do not have permission to access it.", 404 + +@app.route('/delete_link/', methods=['GET']) +@login_required +def delete_link(link_id): + handle_download(link_id, delete_only=True) + return redirect(url_for('view_uploads')) + +@app.route('/delete_file/', methods=['GET']) +@login_required +def delete_file(file_id): + handle_download(file_id, delete_only=True) + return redirect(url_for('view_uploads')) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, ssl_context='adhoc') diff --git a/server/db_setup.py b/server/db_setup.py index aed3081..d8bb482 100644 --- a/server/db_setup.py +++ b/server/db_setup.py @@ -1,18 +1,20 @@ # server/db_setup.py import sqlite3 import hashlib +import os from contextlib import closing DATABASE = 'transfer_service.db' def initialize_db(): with closing(sqlite3.connect(DATABASE)) as conn, conn, closing(conn.cursor()) as c: - # Create users table + # Create users table with salt c.execute(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, + salt TEXT NOT NULL, login_attempts INTEGER DEFAULT 0 ) ''') @@ -28,21 +30,66 @@ def initialize_db(): ) ''') + # Check if the 'salt' column exists in the users table and add it if missing + c.execute("PRAGMA table_info(users)") + columns = [column[1] for column in c.fetchall()] + if 'salt' not in columns: + c.execute("ALTER TABLE users ADD COLUMN salt TEXT") + update_existing_users_with_salts() # Update existing users with salts + conn.commit() +def generate_salt(): + return os.urandom(16).hex() + +def hash_password(password, salt): + return hashlib.sha256((password + salt).encode()).hexdigest() + +def update_existing_users_with_salts(): + """ + Updates existing users to include a unique salt and rehashes their passwords. + """ + with closing(sqlite3.connect(DATABASE)) as conn, closing(conn.cursor()) as c: + c.execute('SELECT id, password FROM users') + users = c.fetchall() + + for user_id, password in users: + salt = generate_salt() + hashed_password = hash_password(password, salt) + c.execute('UPDATE users SET password = ?, salt = ? WHERE id = ?', (hashed_password, salt, user_id)) + + conn.commit() + print("Updated existing users with salts.") + def add_user(username, password): - hashed_password = hashlib.sha256(password.encode()).hexdigest() + # Generate a unique salt for each user + salt = os.urandom(16) + hashed_password = hashlib.sha256(salt + password.encode()).hexdigest() + try: with closing(sqlite3.connect(DATABASE)) as conn, conn, closing(conn.cursor()) as c: - c.execute('INSERT INTO users (username, password) VALUES (?, ?)', (username, hashed_password)) + c.execute('INSERT INTO users (username, password, salt) VALUES (?, ?, ?)', (username, hashed_password, salt)) conn.commit() print(f"User '{username}' added successfully.") except sqlite3.IntegrityError: print(f"User '{username}' already exists.") +def delete_user(username): + """ + Deletes a user from the database based on the provided username. + """ + try: + with closing(sqlite3.connect(DATABASE)) as conn, conn, closing(conn.cursor()) as c: + c.execute('DELETE FROM users WHERE username = ?', (username,)) + conn.commit() + print(f"User '{username}' deleted successfully.") + except sqlite3.Error as e: + print(f"Error deleting user '{username}': {e}") + def get_user(username): with closing(sqlite3.connect(DATABASE)) as conn, closing(conn.cursor()) as c: - c.execute('SELECT username, password, login_attempts FROM users WHERE username = ?', (username,)) + # Select only password, salt, and login_attempts + c.execute('SELECT password, salt, login_attempts FROM users WHERE username = ?', (username,)) return c.fetchone() def reset_login_attempts(username): @@ -70,8 +117,17 @@ def delete_upload(upload_id): c.execute('DELETE FROM uploads WHERE id = ?', (upload_id,)) conn.commit() +def update_upload_filename(upload_id, new_name): + with closing(sqlite3.connect(DATABASE)) as conn, closing(conn.cursor()) as c: + c.execute('UPDATE uploads SET content = ? WHERE id = ?', (new_name, upload_id)) + conn.commit() + if __name__ == '__main__': initialize_db() + # Example of initializing users (only run manually) -# add_user('iphone_user', 'your_secure_password') -# add_user('laptop_user', 'your_secure_password') + # add_user('iphone_user', 'your_secure_password') + # add_user('laptop_user', 'your_secure_password') + # Example of deleting user + # delete_user('iphone_user') + # delete_user('laptop_user') diff --git a/server/preview.py b/server/preview.py new file mode 100644 index 0000000..692b8b5 --- /dev/null +++ b/server/preview.py @@ -0,0 +1,37 @@ +# preview.py +import os +from flask import send_file, jsonify +import sqlite3 +import mimetypes + +UPLOAD_DIRECTORY = "../assets" + +def generate_preview(upload_id): + conn = sqlite3.connect('transfer_service.db') + c = conn.cursor() + + c.execute('SELECT file_type, content FROM uploads WHERE id = ?', (upload_id,)) + upload = c.fetchone() + + if not upload: + conn.close() + return jsonify({"error": "File not found"}), 404 + + file_type, filename = upload + file_path = os.path.join(UPLOAD_DIRECTORY, filename) + + if not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + # Detect the MIME type + mime_type, _ = mimetypes.guess_type(filename) + + # Handle different file types for preview + if file_type == "link": + return jsonify({"link": filename}), 200 + + # Send the file with the correct MIME type + if file_type == "file" and filename.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')): + return send_file(file_path, mimetype='image/jpeg', download_name=os.path.basename(file_path)) + + return jsonify({"error": "Preview not supported for this file type"}), 400 diff --git a/server/rename.py b/server/rename.py new file mode 100644 index 0000000..b0ea355 --- /dev/null +++ b/server/rename.py @@ -0,0 +1,40 @@ +# rename.py +import os +import sqlite3 + +UPLOAD_DIRECTORY = "../assets" +DATABASE = 'transfer_service.db' # Define the DATABASE path + +def rename_file(upload_id, new_name): + conn = sqlite3.connect(DATABASE) + c = conn.cursor() + + # Retrieve the upload record based on the ID + c.execute('SELECT * FROM uploads WHERE id = ?', (upload_id,)) + upload = c.fetchone() + + if not upload: + conn.close() + return False, "File not found in database" + + old_filename = upload[3] + old_path = os.path.join(UPLOAD_DIRECTORY, old_filename) + new_path = os.path.join(UPLOAD_DIRECTORY, new_name) + + if os.path.exists(new_path): + conn.close() + return False, "A file with the new name already exists" + + # Rename the file in the filesystem + try: + os.rename(old_path, new_path) + except OSError as e: + conn.close() + return False, f"Error renaming file: {e}" + + # Update the filename in the database + c.execute('UPDATE uploads SET content = ? WHERE id = ?', (new_name, upload_id)) + conn.commit() + conn.close() + + return True, "File renamed successfully" diff --git a/server/security.py b/server/security.py index 7811360..95f1cb6 100644 --- a/server/security.py +++ b/server/security.py @@ -1,40 +1,79 @@ -# server/security.py -from flask import request, session +import os # Import for generating random salts import hashlib +from flask import request from db_setup import get_user, increment_login_attempts, reset_login_attempts MAX_ATTEMPTS = 3 +def generate_salt(): + """ + Generates a 16-byte random salt. + """ + return os.urandom(16) + +def hash_password(password, salt): + # Convert the salt to bytes if it's a string + if isinstance(salt, str): + salt = salt.encode() + return hashlib.sha256(salt + password.encode()).hexdigest() + def validate_user(username, password): + """ + Validates the user's credentials against stored data. + """ user_data = get_user(username) if not user_data: + print(f"User '{username}' does not exist.") return False, "User does not exist." - stored_username, stored_password, login_attempts = user_data + stored_password, salt, login_attempts = user_data + # Check if the maximum login attempts have been reached if login_attempts >= MAX_ATTEMPTS: + print(f"User '{username}' has exceeded max login attempts.") return False, "Maximum login attempts exceeded. Please contact the administrator." - hashed_password = hashlib.sha256(password.encode()).hexdigest() - + # Hash the provided password with the salt + hashed_password = hash_password(password, salt) + print(f"Provided hash: {hashed_password}, Stored hash: {stored_password}") + if hashed_password == stored_password: reset_login_attempts(username) + print(f"User '{username}' logged in successfully.") return True, "Login successful." else: increment_login_attempts(username) - return False, f"Invalid credentials. {MAX_ATTEMPTS - login_attempts - 1} attempt(s) remaining." + remaining_attempts = MAX_ATTEMPTS - login_attempts - 1 + print(f"Invalid credentials for '{username}'. {remaining_attempts} attempt(s) remaining.") + return False, f"Invalid credentials. {remaining_attempts} attempt(s) remaining." def identify_uploader(): + """ + Identifies the uploader's device information from the request headers. + """ device_info = get_device_info() - if "iPhone" in device_info['user_agent']: - return f"Uploaded by iPhone (IP: {device_info['ip']})" + user_agent = device_info['user_agent'] + + if "iPhone" in user_agent: + device_type = "iPhone" + elif "Android" in user_agent: + device_type = "Android" + elif "Windows" in user_agent: + device_type = "Windows PC" + elif "Mac" in user_agent: + device_type = "Mac" + elif "Linux" in user_agent: + device_type = "Linux Machine" else: - return f"Uploaded by {device_info['isa']} {device_info['os']} (IP: {device_info['ip']})" + device_type = "Unknown Device" + + return f"Uploaded by {device_type} (IP: {device_info['ip']})" def get_device_info(): - user_agent = request.headers.get('User-Agent', 'Unknown') + """ + Extracts device information from the request. + """ return { - "ip": request.remote_addr, - "user_agent": user_agent, + "ip": request.remote_addr or "Unknown IP", + "user_agent": request.headers.get('User-Agent', 'Unknown'), } - diff --git a/server/transfer_service.db b/server/transfer_service.db index 45269165d7013924255c38bd0d513b679636d7e9..be135b7d1a85057fc904373f1b18699b26415286 100644 GIT binary patch delta 287 zcmZo@U~Fh$oFFZj&%nUI!hitG6LpOF^BMG#8F~30GBEQTV&FNnF)xINtFeTUU0ht8 zvDtm{Yrcm(Its;!IVB1qt`Q-cn|JbRFtYIfV0bgJ@x|n`{N@~t{EHa)7x7QrEGY1X zkBO0MV&jUe7)Evm@kU+_o$Q>{%)G?ZL_<>(%S01n%cSJA)MTSXQ&S5ABg3TBG!tXP z)D%+#GgAZ0R0E?_Gjj{0R5KG(0|QHA%QU03WQ(M<6yxT#7lYnNIxktdb-iEWt5x}2 z3=9m6{NEY)zw^J@EU0jse_{ZKB1jPAH4bKRPOz=KAf5mN1OHwAz5Gr5d7A|l68RZJ WCOQgFZnd{&RG8Q(&!{}HQ62#D4NhAC delta 323 zcmZo@U~Fh$oFFY|#lXP8!hirw6LpOFtr+x@8F~4CFfjA(WZ-+aF)xH?;)L&;ck*g5 zay4c$vWtt0Gd3GeKEv~nQFHQHesc~c{!|A3P5h~w1r@mXS@<_G_)l!~uJ>kQV-RoT z<;c!S&CD}2PBAx4OR_XHurRZ*OieaPO-eCKO*J&KOg1n#OEgO}Hc2!zHBU7+H8C?Y zHZ(U%PBu0+HZ)H)N-;=INi_0dWMhzQ