This commit is contained in:
Takeus committed 2025-02-05 13:25:36 +01:00
1 parent 9f9f90422a
commit 4870f0b089
12 files changed
+431 -66

No files matched your search

+13
View File
@@ -16,3 +16,16 @@ class NewAllocationForm(FlaskForm):
host_name = StringField(validators=[Length(max=128)]) host_name = StringField(validators=[Length(max=128)])
host_desc = StringField(validators=[Length(max=128)]) host_desc = StringField(validators=[Length(max=128)])
#host_type = SelectField(choices=[(0,'None'),(1,'Computer'),(2,'Server'),(3,'Virtual Machine'),(4,'Network Device'),(5,'Other')],validators=[DataRequired()]) #host_type = SelectField(choices=[(0,'None'),(1,'Computer'),(2,'Server'),(3,'Virtual Machine'),(4,'Network Device'),(5,'Other')],validators=[DataRequired()])
class NewUserForm (FlaskForm):
username = StringField(validators=[DataRequired(),Length(max=128)])
password = PasswordField(validators=[DataRequired(),Length(max=128)])
is_admin = SelectField(choices=[(0,'No'),(1,'Yes')],validators=[DataRequired()])
class ChangePasswordForm(FlaskForm):
password = PasswordField(validators=[DataRequired(),Length(max=128)])
is_admin = SelectField(choices=[(0,'No'),(1,'Yes')],validators=[DataRequired()])
class ChangeAllocationForm(FlaskForm):
hostname = StringField(validators=[Length(max=128)])
description = StringField(validators=[Length(max=128)])
+87 -11
View File
@@ -1,26 +1,102 @@
from datetime import datetime from datetime import datetime
from flask import Blueprint, request, render_template, flash, url_for, redirect, abort from flask import Blueprint, request, render_template, flash, url_for, redirect, abort
from flask_login import login_required, current_user from flask_login import login_required, current_user, logout_user
from werkzeug.security import generate_password_hash
from app._config import app_version from app._config import app_version
from app.extensions import * from app.extensions import *
from app.models import * from app.models import *
from app.forms import NewUserForm, ChangePasswordForm
admin = Blueprint('admin', __name__) admin = Blueprint('admin', __name__)
@admin.route('/') @admin.route('/', methods=['GET','POST'])
@login_required @login_required
def home(): def home():
if current_user.is_admin:
return redirect(url_for('admin.dash'))
else:
return abort(403)
@admin.route('/dashboard')
@login_required
def dash():
if current_user.is_admin == False: if current_user.is_admin == False:
return abort(403) return abort(403)
return f"Welcome {current_user.username} to the admin dashboard" db_users = User.query.all()
form = NewUserForm()
if form.validate_on_submit() and request.method == 'POST':
# IF user exist
if User.query.filter_by(username=form.username.data).first() != None:
flash("User already exist", 'warning')
return redirect(url_for('admin.home'))
is_admin = False
if int(form.is_admin.data) == 1:
is_admin = True
db.session.add(User(username=form.username.data, password=generate_password_hash(form.password.data), is_admin=is_admin))
db.session.commit()
flash("User Created", 'success')
return redirect(url_for('admin.home'))
return render_template('admin/home.html', title="Admin", app_version=app_version, is_admin=True, navcolor='danger', users=db_users, form=form)
## MODAL ROUTES
@admin.get('/user/<int:user_id>/modal/delete')
@login_required
def user_modal_delete(user_id):
if current_user.is_admin == False:
return abort(403)
user = User.query.get(user_id)
modal = {
'title': 'Delete User',
'body': f'Are you sure you want to delete <b>{user.username}</b> user ?',
'color': 'danger',
'action': 'delete',
'action_text': 'Delete',
'action_url': url_for('admin.delete_user',user_id=user.id)
}
return render_template('panel/_modal.html', modal=modal)
@admin.route('/user/<int:user_id>/modal/edit', methods=['GET','POST'])
@login_required
def user_modal_edit(user_id):
if current_user.is_admin == False:
return abort(403)
user = User.query.get(user_id)
form = ChangePasswordForm()
if form.validate_on_submit() and request.method == 'POST':
user.password = generate_password_hash(form.password.data)
user.is_admin = False
if int(form.is_admin.data) == 1:
user.is_admin = True
db.session.commit()
flash("Informations Changed", 'success')
return redirect(url_for('admin.home'))
modal = {
'title': 'Edit User',
'body': f'Change Informations for <b>{user.username}</b>',
'color': 'primary',
'modal_type': 'form',
'action_text': 'Save',
'form_url': url_for('admin.user_modal_edit',user_id=user.id),
}
return render_template('panel/_modal.html', modal=modal, form=form)
## MODIFICATION ROUTES
@admin.get('/user/<int:user_id>/delete')
@login_required
def delete_user(user_id):
if current_user.is_admin == False:
return abort(403)
if User.query.filter_by(is_admin=True).count() == 1 and User.query.get(user_id).is_admin == True:
flash("You can't delete the last admin user !", 'danger')
return redirect(url_for('admin.home'))
user = User.query.get(user_id)
db.session.delete(user)
db.session.commit()
flash("User Deleted", 'warning')
return redirect(url_for('admin.home'))
+64 -19
View File
@@ -7,7 +7,7 @@ from app._config import app_version
from app.extensions import * from app.extensions import *
from app.models import * from app.models import *
from app.forms import NewPrefixForm, NewAllocationForm from app.forms import NewPrefixForm, NewAllocationForm, ChangeAllocationForm
base = Blueprint('base', __name__) base = Blueprint('base', __name__)
@@ -22,7 +22,28 @@ def home():
@login_required @login_required
def dashboard(): def dashboard():
return render_template('panel/dashboard.html', title="Dashboard", is_dash=True, app_version=app_version) db_networks = Network.query.count()
db_allocations = Allocation.query.count()
db_inventory = Inventory.query.count()
db_users = User.query.count()
available_alloc = 0
for available_ip in Network.query.all():
available_alloc += int(available_ip.hosts)
return render_template(
'panel/dashboard.html',
title="Dashboard",
is_dash=True,
app_version=app_version,
## Variables
db_networks=db_networks,
db_allocations=db_allocations,
db_inventory=db_inventory,
available_alloc=available_alloc,
db_users=db_users,
navcolor='dark'
)
@base.route('/network',methods=['GET','POST']) @base.route('/network',methods=['GET','POST'])
@login_required @login_required
@@ -45,7 +66,7 @@ def networks():
elif form.errors: elif form.errors:
flash(form.errors, 'danger') flash(form.errors, 'danger')
return render_template('panel/networks.html', title="Prefixes", is_networks=True, app_version=app_version, prefixes=networks, form=form, Allocation=Allocation) return render_template('panel/networks.html', title="Prefixes", is_networks=True, app_version=app_version, prefixes=networks, form=form, Allocation=Allocation, navcolor='dark')
@base.route('/network/<int:net_id>/allocations',methods=['GET','POST']) @base.route('/network/<int:net_id>/allocations',methods=['GET','POST'])
@login_required @login_required
@@ -53,7 +74,7 @@ def allocations(net_id):
active_page = request.args.get('page', type=int) active_page = request.args.get('page', type=int)
db_network = Network.query.filter_by(id=net_id).first() db_network = Network.query.filter_by(id=net_id).first()
db_allocations = db.paginate(db.select(Allocation).filter_by(net_id=net_id), per_page=15 ) db_allocations = db.paginate(db.select(Allocation).filter_by(net_id=net_id), per_page=12 )
form = NewAllocationForm() form = NewAllocationForm()
if request.method == 'POST' and form.validate_on_submit(): if request.method == 'POST' and form.validate_on_submit():
@@ -76,23 +97,10 @@ def allocations(net_id):
elif form.errors: elif form.errors:
flash(form.errors, 'danger') flash(form.errors, 'danger')
return render_template('panel/allocations.html', title="Allocations", is_networks=True, app_version=app_version, form=form, allocations=db_allocations, network=db_network, Inventory=Inventory, active_page=active_page) return render_template('panel/allocations.html', title="Allocations", is_networks=True, app_version=app_version, form=form, allocations=db_allocations, network=db_network, Inventory=Inventory, active_page=active_page, navcolor='dark')
## MODAL ROUTES ## MODAL ROUTES
@base.get('/network/<int:net_id>/allocations/<string:ipv4>/del_modal')
@login_required
def allocation_del_modal(net_id,ipv4):
modal = {
'title': 'Delete Allocation',
'body': f'Are you sure you want to delete <b>{ipv4}</b> allocation ?',
'color': 'warning',
'action': 'delete',
'action_text': 'Delete',
'action_url': url_for('base.allocation_delete',net_id=net_id,ipv4=ipv4)
}
return render_template('panel/_modal.html', modal=modal, net_id=net_id, ipv4=ipv4, old_page=request.args.get('page', type=int))
@base.get('/network/<int:net_id>/del_modal') @base.get('/network/<int:net_id>/del_modal')
@login_required @login_required
def network_del_modal(net_id): def network_del_modal(net_id):
@@ -107,7 +115,44 @@ def network_del_modal(net_id):
} }
return render_template('panel/_modal.html', modal=modal, net_id=net_id) return render_template('panel/_modal.html', modal=modal, net_id=net_id)
## DELETE ROUTES @base.get('/network/<int:net_id>/allocations/<string:ipv4>/del_modal')
@login_required
def allocation_del_modal(net_id,ipv4):
modal = {
'title': 'Delete Allocation',
'body': f'Are you sure you want to delete <b>{ipv4}</b> allocation ?',
'color': 'warning',
'action': 'delete',
'action_text': 'Delete',
'action_url': url_for('base.allocation_delete',net_id=net_id,ipv4=ipv4)
}
return render_template('panel/_modal.html', modal=modal, net_id=net_id, ipv4=ipv4, old_page=request.args.get('page', type=int), pagination_active=True)
@base.route('/network/<int:net_id>/allocations/<string:ipv4>/edit_modal', methods=['GET','POST'])
@login_required
def allocation_edit_modal(net_id, ipv4):
db_alloc = Allocation.query.filter_by(ipv4=ipv4).first()
form = ChangeAllocationForm()
if form.validate_on_submit() and request.method == 'POST':
db_alloc.hostname = str(form.hostname.data)
db_alloc.description = str(form.description.data)
db.session.commit()
flash("Allocation Updated", 'success')
return redirect(url_for('base.allocations',net_id=net_id,page=request.args.get('page', type=int)))
modal = {
'title': 'Update Allocation',
'body': f'Change informations for <b>{db_alloc.ipv4}</b> allocation.',
'color': 'primary',
'modal_type': 'form',
'action_text': 'Save',
'form_url': url_for('base.allocation_edit_modal',net_id=db_alloc.net_id,ipv4=db_alloc.ipv4),
}
return render_template('panel/_modal.html', modal=modal, form=form, old_page=request.args.get('page', type=int), pagination_active=True)
## MODIFICATION ROUTES
@base.get('/network/<int:net_id>/allocations/<string:ipv4>/delete') @base.get('/network/<int:net_id>/allocations/<string:ipv4>/delete')
@login_required @login_required
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+20
View File
@@ -0,0 +1,20 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/chart.js@4.4.7/dist/chart.umd.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
/*!
* Chart.js v4.4.7
* https://www.chartjs.org
* (c) 2024 Chart.js Contributors
* Released under the MIT License
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Go},get Decimation(){return Qo},get Filler(){return ma},get Legend(){return ya},get SubTitle(){return ka},get Title(){return Ma},get Tooltip(){return Ba}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;a<r;a++)e.call(i,t[a],a);else if(o(t))for(l=Object.keys(t),r=l.length,a=0;a<r;a++)e.call(i,t[l[a]],l[a])}function f(t,e){let i,s,n,o;if(!t||!e||t.length!==e.length)return!1;for(i=0,s=t.length;i<s;++i)if(n=t[i],o=e[i],n.datasetIndex!==o.datasetIndex||n.index!==o.index)return!1;return!0}function g(t){if(n(t))return t.map(g);if(o(t)){const e=Object.create(null),i=Object.keys(t),s=i.length;let n=0;for(;n<s;++n)e[i[n]]=g(t[i[n]]);return e}return t}function p(t){return-1===["__proto__","prototype","constructor"].indexOf(t)}function m(t,e,i,s){if(!p(t))return;const n=e[t],a=i[t];o(n)&&o(a)?b(n,a,s):e[t]=g(a)}function b(t,e,i){const s=n(e)?e:[e],a=s.length;if(!o(t))return t;const r=(i=i||{}).merger||m;let l;for(let e=0;e<a;++e){if(l=s[e],!o(l))continue;const n=Object.keys(l);for(let e=0,s=n.length;e<s;++e)r(n[e],t,l,i)}return t}function x(t,e){return b(t,e,{merger:_})}function _(t,e,i){if(!p(t))return;const s=e[t],n=i[t];o(s)&&o(n)?x(s,n):Object.prototype.hasOwnProperty.call(e,t)||(e[t]=g(n))}const y={"":t=>t,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)<i}function B(t){const e=Math.round(t);t=V(t,e,t/1e3)?e:t;const i=Math.pow(10,Math.floor(z(t))),s=t/i;return(s<=1?1:s<=2?2:s<=5?5:10)*i}function W(t){const e=[],i=Math.sqrt(t);let s;for(s=1;s<i;s++)t%s==0&&(e.push(s),e.push(t/s));return i===(0|i)&&e.push(i),e.sort(((t,e)=>t-e)).pop(),e}function N(t){return!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;s<n;s++)o=t[s][i],isNaN(o)||(e.min=Math.min(e.min,o),e.max=Math.max(e.max,o))}function $(t){return t*(C/180)}function Y(t){return t*(180/C)}function U(t){if(!a(t))return;let e=1,i=0;for(;Math.round(t*e)/e!==t;)e*=10,i++;return i}function X(t,e){const i=e.x-t.x,s=e.y-t.y,n=Math.sqrt(i*i+s*s);let o=Math.atan2(s,i);return o<-.5*C&&(o+=O),{angle:o,distance:n}}function q(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function K(t,e){return(t-e+A)%O-C}function G(t){return(t%O+O)%O}function Z(t,e,i,s){const n=G(t),o=G(e),a=G(i),r=G(o-n),l=G(a-n),h=G(n-o),c=G(n-a);return n===o||n===a||s&&o===a||r>l&&h<c}function J(t,e,i){return Math.max(e,Math.min(i,t))}function Q(t){return J(t,-32768,32767)}function tt(t,e,i,s=1e-6){return t>=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]<e);let s,n=t.length-1,o=0;for(;n-o>1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return n<i||n===i&&t[s+1][e]===i}:s=>t[s][e]<i),st=(t,e,i)=>et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;s<n&&t[s]<e;)s++;for(;n>s&&t[n-1]>i;)n--;return s>0||n<t.length?t.slice(s,n):t}const ot=["push","pop","shift","splice","unshift"];function at(t,e){t._chartjs?t._chartjs.listeners.push(e):(Object.defineProperty(t,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[e]}}),ot.forEach((e=>{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){Line truncated
/*!
* @kurkle/color v0.3.2
* https://github.com/kurkle/color#readme
* (c) 2023 Jukka Kurkela
* Released under the MIT License
*/function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e<i?6:0):e===n?(i-t)/s+2:(t-e)/s+4}(e,i,s,h,n),r=60*r+.5),[0|r,l||0,a]}function zt(t,e,i,s){return(Array.isArray(e)?t(e[0],e[1],e[2]):t(e,i,s)).map(Mt)}function Ft(t,e,i){return zt(Lt,t,e,i)}function Vt(t){return(t%360+360)%360}function Bt(t){const e=Tt.exec(t);let i,s=255;if(!e)return;e[5]!==i&&(s=e[6]?vt(+e[5]):Mt(+e[5]));const n=Vt(+e[2]),o=+e[3]/100,a=+e[4]/100;return i="hwb"===e[1]?function(t,e,i){return zt(Rt,t,e,i)}(n,o,a):"hsv"===e[1]?function(t,e,i){return zt(Et,t,e,i)}(n,o,a):Ft(n,o,a),{r:i[0],g:i[1],b:i[2],a:s}}const Wt={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Nt={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};let Ht;function jt(t){Ht||(Ht=function(){const t={},e=Object.keys(Nt),i=Object.keys(Wt);let s,n,o,a,r;for(s=0;s<e.length;s++){for(a=r=e[s],n=0;n<i.length;n++)o=i[n],r=r.replace(o,Wt[o]);o=parseInt(Nt[a],16),t[r]=[o>>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),Line truncated
//# sourceMappingURL=chart.umd.js.map
+110
View File
@@ -0,0 +1,110 @@
{% extends 'panel/_base.html' %}
{% block content %}
{% include 'panel/_nav.html' %}
{% include 'panel/_flash.html' %}
<div class="container">
<div class="row">
<div class="col-12">
<h1 class="display-4">Admin Panel</h1>
<p class="lead">Welcome {{current_user.username}} to the admin dashboard.</p>
</div>
</div>
<div class="row">
<div class="col-8">
<div class="card">
<div class="card-header">
<h5 class="card-title">User Management</h5>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Admin User</th>
<th>Last Connection</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{user.id}}</td>
<td>{{user.username}}</td>
<td>{{user.is_admin}}</td>
<td>{% if user.dat_last != None %}{{user.dat_last.strftime('%Y-%m-%d %H:%M:%S')}}{%else%}N/A{%endif%}</td>
<td>
<button
hx-get="{{ url_for('admin.user_modal_edit',user_id=user.id) }}"
hx-trigger="click"
hx-target="#modal-edit"
data-bs-toggle="modal"
data-bs-target="#modal-edit"
class="btn btn-sm btn-warning"
> Edit
</button>
<button
hx-get="{{ url_for('admin.user_modal_delete',user_id=user.id) }}"
hx-trigger="click"
hx-target="#modal-del"
data-bs-toggle="modal"
data-bs-target="#modal-del"
class="btn btn-sm btn-danger"
> Delete
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="col-4">
<div class="card">
<div class="card-header">
<h5 class="card-title">Add User</h5>
</div>
<div class="card-body">
<form method="POST">
{{ form.csrf_token }}
<div class="mb-3">
<label class="form-label">Username</label>
{{ form.username(class="form-control", placeholder="johndoe") }}
</div>
<div class="mb-3">
<label class="form-label">Password</label>
{{ form.password(class="form-control", placeholder="superpassword") }}
</div>
<div class="mb-3">
<label class="form-label">Is Admin?</label>
{{ form.is_admin(class="form-control") }}
</div>
<button type="submit" class="btn btn-secondary w-100">Submit</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- MODAL -->
<div id="modal-del"
class="modal modal-blur fade"
style="display: none"
aria-hidden="false"
tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
<div class="modal-content"></div>
</div>
</div>
<div id="modal-edit"
class="modal modal-blur fade"
style="display: none"
aria-hidden="false"
tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
<div class="modal-content"></div>
</div>
</div>
{% include 'panel/_footer.html' %}
{% endblock %}
+1
View File
@@ -0,0 +1 @@
<footer class="test-center" style="text-align: center;">Flask IPAM - 2025 - {{app_version}}</footer>
+30 -5
View File
@@ -1,3 +1,31 @@
{% if modal.modal_type == 'form' %}
<div class="modal-dialog modal-dialog-centered">
<form method="post" action="{{modal.form_url}}{% if pagination_active %}?page={{old_page}}{%endif%}">
{{ form.csrf_token }}
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-{{modal.color}}">{{ modal.title }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>
{{ modal.body|safe }}<br><br>
{% for entry in form %}
{% if entry.id != 'csrf_token' %}
{{ entry.label(class="form-label") }}<br>
{{ entry(class="form-control") }}
{% endif %}
{% endfor %}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-{{modal.color}}">{{modal.action_text}}</button>
</div>
</div>
</form>
</div>
{% else %}
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -9,11 +37,8 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
{% if modal.action == 'delete' %} <a href="{{modal.action_url}}{% if pagination_active %}?page={{old_page}}{%endif%}" class="btn btn-{{modal.color}}">{{modal.action_text}}</a>
<a href="{{modal.action_url}}?page={{old_page}}" class="btn btn-{{modal.color}}">{{modal.action_text}}</a>
{% else %}
<button type="button" class="btn btn-primary">Save changes</button>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endif %}
+7 -7
View File
@@ -1,6 +1,6 @@
<nav class="navbar navbar-expand-lg bg-dark bg-body-tertiary" data-bs-theme="dark"> <nav class="navbar navbar-expand-lg bg-{{navcolor}} bg-body-tertiary" data-bs-theme="dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="#">Flask IPAM</a> <a class="navbar-brand" href="">Flask IPAM</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
@@ -13,7 +13,7 @@
<a class="nav-link {% if is_networks %}active{% endif %}" href="{{ url_for('base.networks') }}"><i class="bi bi-globe"></i> Network(s)</a> <a class="nav-link {% if is_networks %}active{% endif %}" href="{{ url_for('base.networks') }}"><i class="bi bi-globe"></i> Network(s)</a>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false"><i class="bi bi-archive-fill"></i> Inventory</a> <a class="nav-link dropdown-toggle disabled" role="button" data-bs-toggle="dropdown" aria-expanded="false"><i class="bi bi-archive-fill"></i> Inventory</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="#"><i class="bi bi-pc-display"></i> Computers</a></li> <li><a class="dropdown-item" href="#"><i class="bi bi-pc-display"></i> Computers</a></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-server"></i> Servers</a></li> <li><a class="dropdown-item" href="#"><i class="bi bi-server"></i> Servers</a></li>
@@ -22,15 +22,15 @@
<li><a class="dropdown-item" href="#"><i class="bi bi-collection"></i> Others</a></li> <li><a class="dropdown-item" href="#"><i class="bi bi-collection"></i> Others</a></li>
</ul> </ul>
</li> </li>
{% if current_user.admin %} {% if current_user.is_admin %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" href="">Admin</a> <a class="nav-link {% if is_admin %}active{% endif %}" href="{{ url_for('admin.home') }}"><i class="bi bi-person-badge"></i> Admin</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
<form class="d-flex" role="search"> <div class="d-flex">
<a class="btn btn-outline-warning" type="submit" href="{{ url_for('auth.logout') }}">logout</a> <a class="btn btn-outline-warning" type="submit" href="{{ url_for('auth.logout') }}">logout</a>
</form> </div>
</div> </div>
</div> </div>
</nav> </nav>
+22 -4
View File
@@ -32,13 +32,22 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
<a href="" class="btn btn-sm btn-outline-info" style="padding-top:1px;padding-bottom:1px">edit</a> <button
hx-get="{{ url_for('base.allocation_edit_modal',net_id=network.id,ipv4=allocation.ipv4) }}?page={{active_page}}"
hx-trigger="click"
hx-target="#modal-edit"
data-bs-toggle="modal"
data-bs-target="#modal-edit"
class="btn btn-sm btn-outline-info"
style="padding-top:1px;padding-bottom:1px"
> Edit
</button>
<button <button
hx-get="{{ url_for('base.allocation_del_modal',net_id=network.id,ipv4=allocation.ipv4) }}?page={{active_page}}" hx-get="{{ url_for('base.allocation_del_modal',net_id=network.id,ipv4=allocation.ipv4) }}?page={{active_page}}"
hx-trigger="click" hx-trigger="click"
hx-target="#modal-get" hx-target="#modal-del"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#modal-get" data-bs-target="#modal-del"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
style="padding-top:1px;padding-bottom:1px" style="padding-top:1px;padding-bottom:1px"
> Delete > Delete
@@ -81,7 +90,16 @@
</ul> </ul>
</nav> </nav>
<!-- MODAL --> <!-- MODAL -->
<div id="modal-get" <div id="modal-del"
class="modal modal-blur fade"
style="display: none"
aria-hidden="false"
tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
<div class="modal-content"></div>
</div>
</div>
<div id="modal-edit"
class="modal modal-blur fade" class="modal modal-blur fade"
style="display: none" style="display: none"
aria-hidden="false" aria-hidden="false"
+63 -20
View File
@@ -5,44 +5,87 @@
<div class="row"> <div class="row">
<!-- NETWORKS --> <!-- NETWORKS -->
<div class="col-3"> <div class="col-3">
<div class="card text-bg-info mb-3"> <a href="{{ url_for('base.networks') }}" class="card text-bg-info mb-3 text-decoration-none">
<div class="card-header"><i class="bi bi-globe"></i> Total Networks</div> <div class="card-header"><i class="bi bi-globe"></i> <b>Total Networks</b></div>
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Primary card title</h5> <h1 class="card-title text-center">{{db_networks}}</h1>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
</div> </div>
</div> </a>
</div> </div>
<!-- TOTAL IP --> <!-- TOTAL IP -->
<div class="col-3"> <div class="col-3">
<div class="card text-bg-primary mb-3"> <a href="{{ url_for('base.networks') }}" class="card text-bg-secondary mb-3 text-decoration-none" style="margin-bottom:1vh;">
<div class="card-header"><i class="bi bi-broadcast-pin"></i> Total Allocations</div> <div class="card-header"><i class="bi bi-broadcast-pin"></i> <b>Total Allocations</b></div>
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Primary card title</h5> <h1 class="card-title text-center">{{db_allocations}} / {{ available_alloc }}</h1>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
</div> </div>
</div> </a>
</div> </div>
<!-- TOTAL Inventory --> <!-- TOTAL Inventory -->
<div class="col-3"> <div class="col-3">
<div class="card text-bg-secondary mb-3"> <a class="card text-bg-secondary mb-3 text-decoration-none">
<div class="card-header"><i class="bi bi-archive"></i> Total Inventory</div> <div class="card-header"><i class="bi bi-archive"></i> <b>Total Inventory</b></div>
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Primary card title</h5> <h1 class="card-title text-center">{{db_inventory}}</h1>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
</div> </div>
</div> </a>
</div> </div>
<!-- Total Users --> <!-- Total Users -->
<div class="col-3"> <div class="col-3">
<div class="card text-bg-secondary mb-3"> <a class="card text-bg-primary mb-3 text-decoration-none">
<div class="card-header"><i class="bi bi-people"></i> Total Users</div> <div class="card-header"><i class="bi bi-people"></i> <b>Total Users</b></div>
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Primary card title</h5> <h1 class="card-title text-center">{{ db_users }}</h1>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
</div> </div>
</div> </a>
</div> </div>
</div> </div>
<hr>
<div class="row">
<div class="col-2"></div>
<div class="col-4">
<canvas id="allocchart"></canvas>
</div>
<div class="col-4">
<canvas id="deviceschart"></canvas>
</div>
<div class="col-2"></div>
</div>
<hr>
{% include 'panel/_footer.html' %}
</div> </div>
<script src="{{ url_for('static',filename='js/chart.umd.min.js') }}"></script>
<script>
/* Allocations Chart */
const ctx = document.getElementById('allocchart');
var ip_max = JSON.parse('{{ available_alloc | tojson }}');
var ip_min = JSON.parse('{{ db_allocations | tojson }}');
new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Reserved', 'Available'],
datasets: [{
label: 'IP Addresses',
data: [ip_min, ip_max],
borderWidth: 2
}]
},
options: {
}
});
/* Devices Chart */
const ctx1 = document.getElementById('deviceschart');
new Chart(ctx1, {
type: 'doughnut',
data: {
labels: ['Computers', 'Servers', 'VM', 'Network Devices', 'Others'],
datasets: [{
data: [1, 1, 1, 1, 1],
borderWidth: 2
}]
},
options: {
}
});
</script>
{% endblock %} {% endblock %}
+14
View File
@@ -0,0 +1,14 @@
# Sources
---
### Images
logo : https://www.flaticon.com
### CSS
Bootstrap : https://getbootstrap.com
themes : https://bootswatch.com
icons : https://icons.getbootstrap.com
### JS
HTMX : https://htmx.org
CHARTJS : https://www.chartjs.org