From 5bb06670608e06ee83554cc5c8751e3b8965b429 Mon Sep 17 00:00:00 2001 From: Kobe Date: Tue, 10 Jun 2025 09:47:54 +0200 Subject: [PATCH] Details for instances --- routes/__pycache__/main.cpython-313.pyc | Bin 87186 -> 81888 bytes routes/main.py | 310 +++++++----------------- templates/main/instance_detail.html | 173 +++++++++++++ templates/main/instances.html | 3 + 4 files changed, 263 insertions(+), 223 deletions(-) create mode 100644 templates/main/instance_detail.html diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 956972211a5404dd5d707a119adddc06c0aa53d5..e97dc83f0bfd5781d29c2dc4d57e26cdee48b976 100644 GIT binary patch delta 6988 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 W*EM_HQ}KAAgGcYG1IZ|k_5UAL^c*Mv delta 9691 zcmbVS3s_XwwLbgI%$fHC=D{#9Fu;HeFd!f(C@-ywCJJ&GQ%jWa7!kuOXNE*E39-#> zFfS!L?a`*K`n8SPq((!&i*0WkleTG^rjhC7mT{!Uw&vBODX6i=ez&=M?SUCU&DXEz zGym+z+H0-7_u6Z%y~%~orB5A`=`U-wDu#SdjIZ9d&4^Cwe<)Rapk$aW&Mlev73pP^ ziBEC~`1l`G_)(>i^<<8GBwK+RfZ)j7qb5-1s(i zt|XsVg6*1E25IbGBcI|Ejub)=> zZ`!+{(Y0!Z&nyWmJ?wRU8K?kgTNfS+%>fWr8Rb{7{Wa|n|a zJz^dSw0|Z+XYp^heTjRYv;Zh(5rBotcJn&^!U~I=~{d8zXAN6fGbhBLENK#PQ&E< zp+E1lrp9!bC;1XkUmp3iZ z(=q2^U{n*TflDe}ht{_ME&v1x_=vHOr0u*zQZRS!VVS@8Zg0;(TqFEAI(-hodryC- zKs1B(5I(_2?sw$RUw-Jh*_z0qV-kOQzr%c!#SlIl$$6j+z4-WIJ^C|V^2|?=4qtvI zj~JTc+uMnCJ@)Mn(I>e7JG(q_#5V{!fOr6ENJc1`0Q3Z03c&{DFQ!ZYN|{g(YUxH+ zes?3{V6bowA3o?p@8I44Fmlv>b@<}JB3q2(e6e`pp@rJ8w6(XlOQ1V^7Iz&=L6`Ak zhZb8h3Cpl@YiF0Yxv#~)O)!H93l1G>W&ednmK{#x&P+djfm?Sc#n0B{8 z*Lni3B!QYw12oW0;Sqx$A6-%02rbmyHbRN2lntd@PWcYZp5obGu_Je!;-vK0#hi_c zouvXucj<&gP%?J3sl8~Str~!ercxh+>a9c{W5bG`UVrD-=FWDZi7@uDgxul46aTZG z{fr&S`;B+F+rWHT@awDf9JM3%63x8HUl5)Bh=_n;Y zBblof`(G&~rvBV3UC26Oef4>iK34~-LgQQ=@O!^VSARZBjPaN2IR}X8C1Pquj{fp4 zCAyB4Z!MGov+KC>t!e``iXZKfEwHetUr^?IiP+YEZ_UEAH#*^^JS_ILJl z_=I~&n+pkkyy9%Rnl47f>haLo3_Eo)vE9B5D?kGZOH{<@V-ub@Ta9*&*uzI9cg_+I zbO#7yvO2ylA)*JZ`n!C>w@68N1_1PFTm|LARcIckwsgt#S2}Rw69A3&*6? zCx?~&KCeJ`Nl1c$cKrFfrAUiQ-|Ov2g$AlaY;8;+^{X)vP>L-Q4LL3#sRoGon25)Q zHqc&x#UxR3T1(CQ>)9VmM(%w7oP;|L4A0_$EZT+c60fkRBIQlO;6upUU2K@^^7)0%K9a%o_jP&6Ng5ODB#1{_ z#P3PaQS_#GyA-vc*3siqv=HUMkKGXMnQ+e*g%JFL7uy@`~|tRD!C+S~>b2dUNz+ zIqH_63Gpjx!ID0zTCHI0X%`V5^Lw81t}*Dc({1(2k9 zp2l=#R>QzA18f3-m=f0bd;Kk4&E9*xX0rK_XhbJZ=UBpshl)|6oX#Vh1csm`q=fQm zVspNFmKi-$Dkv!E0Rf5s4 za`D)?G;x12D#9Q8H2Cvxric%@ky^rfSg|J=B^sp*iJyxM=4FdmC*xspp~)nEJ&W9D zKJQkm$+J-8i*(?$?mRYy!SC!!#Vd!5_>fLva4=2w87f}U#NZ94WS0Cq)|<@f7+y(= zlS9d3^+F_-@+uF)Emjl$O-CYr>IozM#!oFeUY*bKntYDema*gwX%=tX`&X?*cjFKX zug{nAhJ1ELc_)*`G-cs4y9$sCZ`fwXxBpQkK~1^Xb-x|O;i=x#!T9x&C>1^F1BUgz z4ibA?9DPFXy`AmT7v-?r*WbF`+vbN$V*5@Z66eVxg>`f*nrGIYq(LUwKr;as7rC0g z2%8BBP*^=fMz$!d^?Oqz`xi-(>_9d`V8Gruy|LJE);vgZnQF3>kyn%$b5{6-w1$cO zMwoP7Awyl3`DvaFhSG$aK5Im>K4_!aXq4Tp^Q;9iGpFx3k$ea^&|Fa}B*1(VS**V& z$!d-yF3ocnC7go;*OJY18y}Prr}8xfPG#_-(J^k!3K+A-jfDYY;fdU!v2sXpQEwU7 zr~Y1_8q}u^Nzdzz6Gq$q)nl29$C8!=jb*2b$BdO@n#v1FsRx%FSaN91(H-LjRe^%4 zU{dvv;%$XBWKBM(IG{LWIcj~ue#{=UE*?@muAP*oDvKvHy1i@ntR2_b0~-6=IOm@Q zHN~Nf+#^d5FAZh7k8D1?`9enSct&v`qd4TqKB7IWosvl$mMN_&G5(TFtJPgeX0-ZQ zEHtSX@dm+ePj(($cVOL=Qf7~zk|~mPml=ggKglSx`fFF>@hPFA_SK>lX{$`kYbMp| z6v=BTC94x9zcwOL{&k{^;DA1M%Hfdvz^6q&IK}iNmvZDJzaYMwhl(T|FBffYRH0Sy zvJ$cx@b$3xa95FzSI%mZK$DGE@#-c+go?b4YRV)%QeN95Yl?sBQgVC`=V7v^J6rS& z;~{dMv{-mu86sWm4PE90meDoEe?!a4)8sy^jXd# zo;Q{uH@Twp$m`YZiAj^RN7a-^R#EIrMJ7#C z-pnF5HWMdl&aauu=MBP+Gh;UMeNkLx4ZDlCZuGTcc#f*OV@IDhh@rVgGJ*`^K zC(~&qF-+6q83Nw!QFxRdsfR45hxJH2YLBJ~vRwMjJU;U&WojqWRE|$pBvU7!scov7 zPt>EE#Wu~OZK%eFdyEoo!*V>VH4dhFv^R?K=x!p+k*_aBPR5nKUJ$^S(SYrP(rF@7 zsDjeG9GCiznAJQ2(Jd(k763dB z@O1$C#_|Z1XgdBdjHRnhPWJ4?oCXOG^h%(^4G_!3vzbk#9-SkZ?`a^2B{s2Lq)9nV z#ksbAfA7|<^OqsEeqf`*FnMN4mQ$jz4H9w<-I3!E8}`$D96uJYSg&wQe4>~c&h(Lo3g#qfH&qR0>U&X{r-yXj%qM=8!&dzbB|q zpGZtQnEI{MU}Dbmn@%+Z+|_}^>R~Pvm$cs>jLVqQTN7|s1`-D=Bi;SMIOm-1m8#8;>*}Za&2YGAo1Ds(rFaj)}8~x?$b7{RamR434K3 z2U3fJsU<-}>A0aPV5mCXIA*9CGi)A`g)B+p-NSbesY40L;=u60kmd>}u1!KtED5IJ zdrsO1)5kI@LJRUj?j@o8qEL2zD6cf+S_oCrSXe!gSN2@plv!n}m}FGiiYqoIF>~Bn z_1aAYr$iBrHq)q2Z=3aWX5;y zNRrf%$FRPR1n9ySe#&8ghY@#fAsh(#CX;E9my)zpmamuz4Nit<=GI-@{G5{Hgw;eH zn!u4iY|wHFBu%mG>ooMkktK;H@pR2Cev(lUH(;1?1Ed>*!{Dr=PxHBI7u;vr?Os1*AK}c-o=$(WKrSaC?QF_ zfB628$ttcHUK6s|z*+Ji2Uj**-szjwiSS3GTNO3Qjjp zDj34dF4`w$4B^u~+#NaXkm+f+9?*_ka{|^JlBy-@rs6bK)n!IwP){-%rFzoBWV)sj z=CnnO&N|=d+I#Qo>FRB1pObh+uTVc99g9PGXtd3ZYLRRWu^t%?ykugVERZ;`P3ibg zyX^RMmq|kIAvpIhHr$(_;d*2qS;GQcv&bm^B_E}b`$$@W4R6j@qFhJ`aQ&}M5_wZT zPP3#KJn|Xig8ZVP82|nM%t`tU(L0%gL8u1QL&OQx%LT{2PYw-Ei6_^wN4{F4f!f*5}sA*#eb*C)wIjG^UwmKq~` zGopNfu7I17;$YT2%P?w&=q3MVAu`+JV?9kPh;h-+Bj=4g$=7hO07@l*iWQ$NLK(Bdc^0sWh3F7 z5vNgG2lI^ucKWaenWjLW66ouJ4rGx%nf5TJp%irhf|K1!>Uyda=^=>^C5VOddG8dE z;|K;Ir_8fR$p^m35&a^*LpQTy)PeZI84%CSa~mxj8C*gGJ6Cl&;cd+tF>Vnu8Rtg# zU|en_w&#egi_o2e)6x4qb=-}y+YC*&58wWM(~;D}sVCM3(kg?ds%TV?^rRe4d1=vT z`N{Ip%9E9+7X*vfju+Pl$iKAuplQR<$}fzsOuySs2|pk8?pn2>@nA}prHbqNc@H$&9DZ3)Whmf zR?d;`Bi;L?qIOt2Zb<*VA^nswRJr`+u9v$=BOx3&WCjeGV_9Wy8_LJ(`AMzZuDi;} zwfZRuV_s^TG%-s3UiBXJzM98%7xf85e#o49fsAmiep7Kq|AszTyJ@_(HBj3+)+PjN zePgTrWBwgu3wDa;Qnb`|4lE-b_BVq-Skc{bx3|4h@VTkA&*JSJ7sy@sUarK7JrRTZ!t_xv&IZgPke)W)-rd)nZB&`mu7C6{%=! F`+r$Ga!3FG diff --git a/routes/main.py b/routes/main.py index ff28ce8..bc8c8ce 100644 --- a/routes/main.py +++ b/routes/main.py @@ -503,6 +503,93 @@ def init_routes(main_bp): db.session.rollback() return jsonify({'error': str(e)}), 400 + @main_bp.route('/instances//detail') + @login_required + @require_password_change + def instance_detail(instance_id): + if not os.environ.get('MASTER', 'false').lower() == 'true': + flash('This page is only available in master instances.', 'error') + return redirect(url_for('main.dashboard')) + + instance = Instance.query.get_or_404(instance_id) + + # Check instance status + status_info = check_instance_status(instance) + instance.status = status_info['status'] + instance.status_details = status_info['details'] + + # Fetch company name from instance settings + try: + if instance.connection_token: + # First get JWT token + jwt_response = requests.post( + f"{instance.main_url.rstrip('/')}/api/admin/management-token", + headers={ + 'X-API-Key': instance.connection_token, + 'Accept': 'application/json' + }, + timeout=5 + ) + if jwt_response.status_code == 200: + jwt_data = jwt_response.json() + jwt_token = jwt_data.get('token') + + if jwt_token: + # Then fetch settings with JWT token + response = requests.get( + f"{instance.main_url.rstrip('/')}/api/admin/settings", + headers={ + 'Authorization': f'Bearer {jwt_token}', + 'Accept': 'application/json' + }, + timeout=5 + ) + if response.status_code == 200: + data = response.json() + if 'company_name' in data: + instance.company = data['company_name'] + db.session.commit() + except Exception as e: + current_app.logger.error(f"Error fetching instance settings: {str(e)}") + + return render_template('main/instance_detail.html', instance=instance) + + @main_bp.route('/instances//auth-status') + @login_required + @require_password_change + def instance_auth_status(instance_id): + if not os.environ.get('MASTER', 'false').lower() == 'true': + return jsonify({'error': 'Unauthorized'}), 403 + + instance = Instance.query.get_or_404(instance_id) + + # Check if instance has a connection token + has_token = bool(instance.connection_token) + + # If there's a token, verify it's still valid + is_valid = False + if has_token: + try: + # Try to get a JWT token using the connection token + response = requests.post( + f"{instance.main_url.rstrip('/')}/api/admin/management-token", + headers={ + 'X-API-Key': instance.connection_token, + 'Accept': 'application/json' + }, + timeout=5 + ) + is_valid = response.status_code == 200 + except Exception as e: + current_app.logger.error(f"Error verifying token: {str(e)}") + is_valid = False + + return jsonify({ + 'authenticated': has_token and is_valid, + 'has_token': has_token, + 'is_valid': is_valid + }) + UPLOAD_FOLDER = '/app/uploads/profile_pics' if not os.path.exists(UPLOAD_FOLDER): os.makedirs(UPLOAD_FOLDER) @@ -1401,227 +1488,4 @@ def init_routes(main_bp): headers={ 'Content-Disposition': f'attachment; filename=event_log_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}.csv' } - ) - - @main_bp.route('/settings/email-templates/', methods=['PUT']) - @login_required - def update_email_template(template_id): - if not current_user.is_admin: - return jsonify({'error': 'Unauthorized'}), 403 - - template = EmailTemplate.query.get_or_404(template_id) - - data = request.get_json() - if not data: - return jsonify({'error': 'No data provided'}), 400 - - template.subject = data.get('subject', template.subject) - template.body = data.get('body', template.body) - - try: - db.session.commit() - - # Log the template update - log_event( - event_type='settings_update', - details={ - 'user_id': current_user.id, - 'user_name': f"{current_user.username} {current_user.last_name}", - 'update_type': 'email_template', - 'template_id': template.id, - 'template_name': template.name, - 'changes': { - 'subject': template.subject, - 'body': template.body - } - } - ) - db.session.commit() - - return jsonify({ - 'message': 'Template updated successfully', - 'template': { - 'id': template.id, - 'name': template.name, - 'subject': template.subject, - 'body': template.body - } - }) - except Exception as e: - db.session.rollback() - return jsonify({'error': str(e)}), 500 - - @main_bp.route('/settings/mails') - @login_required - def mails(): - if not current_user.is_admin: - flash('You do not have permission to access settings.', 'error') - return redirect(url_for('main.index')) - - # Get filter parameters - status = request.args.get('status', '') - date_range = request.args.get('date_range', '7d') - user_id = request.args.get('user_id', '') - template_id = request.args.get('template_id', '') - page = request.args.get('page', 1, type=int) - per_page = 10 - - # Build query - query = Mail.query - - # Apply filters - if status: - query = query.filter_by(status=status) - if user_id: - query = query.filter_by(recipient=user_id) - if template_id: - query = query.filter_by(template_id=template_id) - if date_range: - if date_range == '24h': - cutoff = datetime.utcnow() - timedelta(hours=24) - elif date_range == '7d': - cutoff = datetime.utcnow() - timedelta(days=7) - elif date_range == '30d': - cutoff = datetime.utcnow() - timedelta(days=30) - else: - cutoff = None - if cutoff: - query = query.filter(Mail.created_at >= cutoff) - - # Get paginated results - mails = query.order_by(Mail.created_at.desc()).paginate(page=page, per_page=per_page) - total_pages = mails.pages - current_page = mails.page - - # Get all users for the filter dropdown - users = User.query.order_by(User.username).all() - - # Get all email templates - email_templates = EmailTemplate.query.filter_by(is_active=True).all() - - # Check if this is an AJAX request - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - return render_template('settings/tabs/mails.html', - mails=mails, - total_pages=total_pages, - current_page=page, - status=status, - date_range=date_range, - user_id=user_id, - template_id=template_id, - users=users, - email_templates=email_templates, - csrf_token=generate_csrf()) - - # For full page requests, render the full settings page - site_settings = SiteSettings.get_settings() - company_form = CompanySettingsForm() - - return render_template('settings/settings.html', - primary_color=site_settings.primary_color, - secondary_color=site_settings.secondary_color, - active_tab='mails', - site_settings=site_settings, - mails=mails, - total_pages=total_pages, - current_page=page, - status=status, - date_range=date_range, - user_id=user_id, - template_id=template_id, - users=users, - email_templates=email_templates, - form=company_form, - csrf_token=generate_csrf()) - - @main_bp.route('/settings/mails/') - @login_required - def get_mail_details(mail_id): - if not current_user.is_admin: - return jsonify({'error': 'Unauthorized'}), 403 - - mail = Mail.query.get_or_404(mail_id) - return jsonify({ - 'id': mail.id, - 'recipient': mail.recipient, - 'subject': mail.subject, - 'body': mail.body, - 'status': mail.status, - 'created_at': mail.created_at.isoformat(), - 'sent_at': mail.sent_at.isoformat() if mail.sent_at else None, - 'template': { - 'id': mail.template.id, - 'name': mail.template.name - } if mail.template else None - }) - - @main_bp.route('/settings/mails/download') - @login_required - def download_mails(): - if not current_user.is_admin: - flash('Only administrators can download mail logs.', 'error') - return redirect(url_for('main.dashboard')) - - # Get filter parameters - status = request.args.get('status') - date_range = request.args.get('date_range', '7d') - user_id = request.args.get('user_id') - - # Calculate date range - end_date = datetime.utcnow() - if date_range == '24h': - start_date = end_date - timedelta(days=1) - elif date_range == '7d': - start_date = end_date - timedelta(days=7) - elif date_range == '30d': - start_date = end_date - timedelta(days=30) - else: - start_date = None - - # Build query - query = Mail.query - - if status: - query = query.filter_by(status=status) - if start_date: - query = query.filter(Mail.created_at >= start_date) - if user_id: - query = query.filter(Mail.recipient == User.query.get(user_id).email) - - # Get all mails - mails = query.order_by(Mail.created_at.desc()).all() - - # Create CSV - output = StringIO() - writer = csv.writer(output) - - # Write header - writer.writerow([ - 'Created At', - 'Recipient', - 'Subject', - 'Status', - 'Template', - 'Sent At' - ]) - - # Write data - for mail in mails: - writer.writerow([ - mail.created_at.strftime('%Y-%m-%d %H:%M:%S'), - mail.recipient, - mail.subject, - mail.status, - mail.template.name if mail.template else '-', - mail.sent_at.strftime('%Y-%m-%d %H:%M:%S') if mail.sent_at else '-' - ]) - - output.seek(0) - - return Response( - output, - mimetype='text/csv', - headers={ - 'Content-Disposition': f'attachment; filename=mail_log_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}.csv' - } ) \ No newline at end of file diff --git a/templates/main/instance_detail.html b/templates/main/instance_detail.html new file mode 100644 index 0000000..7c6c964 --- /dev/null +++ b/templates/main/instance_detail.html @@ -0,0 +1,173 @@ +{% extends "common/base.html" %} +{% from "components/header.html" import header %} + +{% block title %}{{ instance.name }} - DocuPulse{% endblock %} + +{% block content %} +{{ header( + title="Instance Details", + description=instance.name + " for " + instance.company, + icon="fa-server", + buttons=[ + { + 'text': 'Back to Instances', + 'url': '/instances', + 'icon': 'fa-arrow-left', + 'class': 'btn-secondary' + } + ] +) }} + +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
Activity Status
+

+ + {{ instance.status|title }} + + {% if instance.status_details %} + {{ instance.status_details }} + {% endif %} +

+
+
+
+
+
+ + +
+
+
+
+
+
+ +
+
+
+
Authentication Status
+

+ + {{ 'Authenticated' if instance.connection_token else 'Not Authenticated' }} + + {% if not instance.connection_token %} + This instance needs to be authenticated + {% endif %} +

+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/main/instances.html b/templates/main/instances.html index 294677b..dc68d1e 100644 --- a/templates/main/instances.html +++ b/templates/main/instances.html @@ -92,6 +92,9 @@ +